From 8d34445c302aa52fdd415a34a68f71dd0527b766 Mon Sep 17 00:00:00 2001 From: cfry Date: Fri, 5 Feb 2021 20:38:43 -0500 Subject: [PATCH] release --- core/dextersim.js | 25 ++- core/index.js | 6 +- core/instruction.js | 16 +- core/je_and_browser_code.js | 16 +- core/main_eval.py | 55 ++++++ core/out.js | 6 +- core/py.js | 266 ++++++++++++++++++++++++++++ core/robot.js | 20 ++- core/storage.js | 23 +-- core/utils.js | 7 +- doc/guide.html | 12 +- doc/ref_man.html | 255 ++++++++++++++++++++++++-- doc/release_notes.html | 37 +++- editor.js | 55 +++--- eval.js | 10 +- index.html | 4 +- js_info.js | 7 +- load_job_engine.js | 2 + package.json | 4 +- ready.js | 33 +++- robot_utilities.js | 92 ++++++++++ simulator/simutils.js | 3 +- splash_screen.js | 2 +- test_suite/RobotStatus_testsuite.js | 4 + test_suite/manual_testsuite.txt | 2 +- test_suite/test_suite.js | 33 ++-- 26 files changed, 857 insertions(+), 138 deletions(-) create mode 100644 core/main_eval.py create mode 100644 core/py.js diff --git a/core/dextersim.js b/core/dextersim.js index cc6f4a9f..2ef49edb 100644 --- a/core/dextersim.js +++ b/core/dextersim.js @@ -42,7 +42,6 @@ DexterSim = class DexterSim{ MaxSpeed: 30, //todo what are the units on dde side and on sim/dex side? StartSpeed: 0 } - this.ready_to_start_new_instruction = true //true at beginning and when we've just completed an instruction but not started another this.sent_instructions_count = 0 this.status = "before_first_send" this.status_mode = 0 //can also be 1, set by "g" command. @@ -55,7 +54,8 @@ DexterSim = class DexterSim{ this.pid_angles_arcseconds = [0,0,0,0,0,0,0] this.velocity_arcseconds_per_second = [0,0,0,0,0,0,0] - this.now_processing_instruction = null // a Dexter can only be doing at most 1 instruction at a time. This is it. + this.ready_to_start_new_instruction = true //true at beginning and when we've just completed an instruction but not started another + this.now_processing_instruction = null // a Dexter can only be doing a move at most 1 instruction at a time. This is it. } static stop_all_sims(){ @@ -267,12 +267,12 @@ DexterSim = class DexterSim{ sim_inst.now_processing_instruction && (sim_inst.ending_time_of_cur_instruction <= the_now)) { //end the cur instruction and move to the next const oplet = sim_inst.now_processing_instruction[Dexter.INSTRUCTION_TYPE] - if ((sim_inst.sim_actual === true) && [/*"F" , "G", "g"*/].includes(oplet)) { //dont do when sim == "both" + //if ((sim_inst.sim_actual === true) && [/*"F" , "G", "g"*/].includes(oplet)) { //dont do when sim == "both" //let rs_copy = sim_inst.robot_status_in_arcseconds.slice() //make a copy to return as some subseqent call to this meth will modify the one "model of dexter" that we're saving in the instance //rs_copy[Dexter.STOP_TIME] = Date.now() //in milliseconds ///Socket.on_receive(rs_copy, sim_inst.robot.name) - sim_inst.ack_reply(sim_inst.now_processing_instruction) - } + // sim_inst.ack_reply(sim_inst.now_processing_instruction) + //} sim_inst.completed_instructions.push(sim_inst.now_processing_instruction) sim_inst.now_processing_instruction = null //Done with cur ins, sim_inst.ready_to_start_new_instruction = true @@ -280,18 +280,19 @@ DexterSim = class DexterSim{ //hits when there's more on the queue to do if(sim_inst.ready_to_start_new_instruction && (sim_inst.instruction_queue.length > 0) && - (sim_inst.status != "closed")) { + (sim_inst.status !== "closed")) { sim_inst.ready_to_start_new_instruction = false sim_inst.process_next_instruction() } - if((sim_inst.instruction_queue.length == 0) && //nothing in the queue, - (sim_inst.status == "closing")) { //and no more coming from DDE. + else if((sim_inst.instruction_queue.length == 0) && //nothing in the queue, + !sim_inst.now_processing_instruction && //we're not currently in the middle of an instruction + (sim_inst.status === "closing")) { //and no more coming from DDE. sim_inst.ready_to_start_new_instruction = false DexterSim.close(sim_inst.robot_name) //cleaner to set "closed" in one place. } - if(sim_inst.status != "closed") { keep_alive = true } + if(sim_inst.status !== "closed") { keep_alive = true } } - if ((keep_alive == false) && (window.platform == "node")) { + if ((keep_alive == false) && (window.platform === "node")) { clearInterval(DexterSim.set_interval_id) //so that nodejs will quit } } @@ -557,9 +558,7 @@ DexterSim = class DexterSim{ (sim_inst.now_processing_instruction == null)){ sim_inst.stop_sim() //also set near bottom of process_next_instruction } - else { //note: now that I automatically put a "g" instruction as the automatic last instruction - //of a job's do_list, we shouldn't be getting this "closing" state because - //the "g" naturally empties the instruction_queue. + else { sim_inst.status = "closing" //setTimeout(function(){DexterSim.close(socket_id)}, 1000) //shouldn't be necessary. //as process_next_instruction will handle final close in this case. diff --git a/core/index.js b/core/index.js index 36e6ed09..ac08f52c 100644 --- a/core/index.js +++ b/core/index.js @@ -1,5 +1,5 @@ -global.dde_version = "3.7.2" -global.dde_release_date = "Jan 13, 2021" +global.dde_version = "3.7.3" +global.dde_release_date = "Feb 5, 2021" console.log("dde_version: " + global.dde_version + " dde_release_date: " + global.dde_release_date + "\nRead electron_dde/core/job_engine_doc.txt for how to use the Job Engine.\n") @@ -157,6 +157,7 @@ var {html_db, is_dom_elt, make_dom_elt, make_html} = require("./html_db.js") var {Messaging, MessStat} = require("./messaging.js") +var {Py} = require("./py.js") // see also je_and_browser_code.js for global vars. global.keep_alive_value = false @@ -237,6 +238,7 @@ global.html_db = html_db global.is_dom_elt = is_dom_elt global.make_html = make_html global.make_dom_elt = make_dom_elt +global.Py = Py run_node_command(process.argv) diff --git a/core/instruction.js b/core/instruction.js index d16cecd8..693f5ce0 100644 --- a/core/instruction.js +++ b/core/instruction.js @@ -529,17 +529,27 @@ Instruction.w_address_names = [ "DIFFERENTIAL_FORCE_TIMEBASE", //"80" "PID_TIMEBASE" //"81" ] -Instruction.w_address_number_to_name = function(num){ +/*Instruction.w_address_number_to_name = function(num){ if(!Instruction.is_valid_w_address(num)) { return "unknown" } let w_address_names = Series.id_to_series("series_w_oplet_address_id").array if (num >= w_address_names.length) { return "unknown" } else { return w_address_names[num] } +}*/ + +//returns undefined for invalid nums +Instruction.w_address_number_to_name = function(num){ + return Instruction.w_address_names[num] } //beware: will return -1 if name is invalid -Instruction.w_address_name_to_number = function(name){ +/*Instruction.w_address_name_to_number = function(name){ let w_address_names = Series.id_to_series("series_w_oplet_address_id").array return w_address_names.indexOf(name) +}*/ + +//beware: will return -1 if name is invalid +Instruction.w_address_name_to_number = function(name){ + return Instruction.w_address_names.indexOf(name) } //user might call this at top level in a do_list so make it's name short. @@ -551,7 +561,7 @@ function make_ins(instruction_type, ...args){ warning("make_ins called with an invalid instruction_type: " + instruction_type + "
make_ins still returning an array using: " + instruction_type) }*/ - let first_arg = args[0] + //let first_arg = args[0] /*if((instruction_type == "w") && !Instruction.is_valid_w_address(first_arg)){ dde_error('make_ins("w" ...) does not support an address of ' + first_arg + '.
Valid addresses are non-negative integers. ' + diff --git a/core/je_and_browser_code.js b/core/je_and_browser_code.js index b95d67c2..7e1c54fa 100644 --- a/core/je_and_browser_code.js +++ b/core/je_and_browser_code.js @@ -326,13 +326,15 @@ static render_show_window(properties){ //let title_id = "sw_title_" + properties.window_index + "_id" let title_elt = show_window_elt.querySelector(".show_window_title") //window[title_id] //onsole.log("render_show_window got title_elt: " + title_elt) - let draggable_value = (properties.draggable? "true": "false") - //onsole.log("render_show_window got draggable_value: " + draggable_value) - title_elt.onmousedown = function(event) { - event.target.parentNode.setAttribute('draggable', draggable_value) //set the whole sw window to now be draggable. - } - title_elt.onmouseup = function(event) { - event.target.parentNode.setAttribute('draggable', 'false') //set the whole sw window to now be draggable. + if(title_elt) { // won't hit if title === "" because that means no title + let draggable_value = (properties.draggable? "true": "false") + //onsole.log("render_show_window got draggable_value: " + draggable_value) + title_elt.onmousedown = function(event) { + event.target.parentNode.setAttribute('draggable', draggable_value) //set the whole sw window to now be draggable. + } + title_elt.onmouseup = function(event) { + event.target.parentNode.setAttribute('draggable', 'false') //set the whole sw window to now be draggable. + } } show_window_elt.ondragstart = function(event) { let show_win_elt = event.target diff --git a/core/main_eval.py b/core/main_eval.py new file mode 100644 index 00000000..343f8157 --- /dev/null +++ b/core/main_eval.py @@ -0,0 +1,55 @@ +import sys +import traceback +import json + +def main(): + print("Python eval process started.\n", flush=True) + while True: + #src = sys.stdin.readlines() #hangs until gets some stdin with a newline + src = sys.stdin.readline() + src = src.replace("{nL}", "\n") + index_of_space = src.index(" ") + callback_id = int(src[0:index_of_space]) + src = src[index_of_space + 1:len(src) - 1] #the -1 takes off the newline on the end of src + #src = src.replace('"', '\\"') + #print("grabbed input: ", src, flush=True) + is_error = False + result = None + is_py_evalable = None + try: + compile(src, "main_eval compile", "eval") #works only for single expressions that return one value + except: #compile for eval errored, so maybe src is not an expression. + is_py_evalable = False + else: + is_py_evalable = True + if is_py_evalable : + try: + result = eval(src) + except Exception as err: + is_error = True + result = traceback.format_exc() + #result = result.replace(", line ", ",
line ") + else: #the normal, working, return a value, eval case + is_error = False + else: + try: + exec(src) + except Exception as err: + is_error = True + result = traceback.format_exc() + #result = result.replace(", line ", ",
line ") + else: + is_error = False + result = None + json_obj = {"from": "Py.eval", "is_error": is_error, "source": src, "callback_id": callback_id, "result": result} + print(json.dumps(json_obj), sep="", flush=True) + +print("Python: main_eval.py file loaded.", "\n", flush=True) +main() + +#if __name__ == "__main__": +# print("just under if", flush=True) +# 2 + 3 +# 5 / 0 + + diff --git a/core/out.js b/core/out.js index 351226aa..7916f7f8 100644 --- a/core/out.js +++ b/core/out.js @@ -70,8 +70,8 @@ function out_eval_result(text, color="#000000", src, src_label="The result of ev src_formatted = "  " + src_formatted + src_formatted_suffix + " " } //if (src_formatted == "") { console.log("_____out_eval_result passed src: " + src + " with empty string for src_formatted and text: " + text)} - text = "
" + src_label + " " + src_formatted + " is..." + text + "
" - append_to_output(text) + let the_html = "
" + src_label + " " + src_formatted + " is..." + text + "
" + append_to_output(the_html) } //$('#js_textarea_id').focus() fails silently if(window["document"]){ @@ -235,7 +235,7 @@ function show_window({content = ``, if (fn_name && (fn_name != "")) { if(fn_name == "callback") { //careful, might be just JS being clever and not the actual name in the fn def fn_name = function_name(callback.toString()) //extracts real name if any - if (fn_name == "") { //nope, no actual name in fn + if ((fn_name == "") || (fn_name == null)) { //nope, no actual name in fn callback = callback.toString() //get the src of the anonymous fn } else { callback = fn_name } diff --git a/core/py.js b/core/py.js new file mode 100644 index 00000000..5523bb51 --- /dev/null +++ b/core/py.js @@ -0,0 +1,266 @@ +/* doc +official: github.com/extrabacon/python-shell +but that doesn't describe passing js args to python code, +however the below does: +https://ourcodeworld.com/articles/read/286/how-to-execute-a-python-script-and-retrieve-output-data-and-errors-in-node-js +https://stackoverflow.com/questions/65876022/npm-python-shell-persistent-process-from-javascript +*/ + +class Py{ + + //called from DDE Eval button + static eval_part2(src){ + Py.eval(src, this.default_callback) + } + + //very much like top level out_eval_result for JS. + static out_eval_result(text, color="#000000", src, src_label="The result of evaling Python: "){ + let existing_temp_elts = [] + if(window["document"]){ + existing_temp_elts = document.querySelectorAll("#temp") + } + for(let temp_elt of existing_temp_elts){ temp_elt.remove() } + + if (color && (color != "#000000")){ + text = "" + text + "" + } + text = format_text_for_code(to_source_code({value: text})) + text = replace_substrings(text, "\n", "
") + let src_formatted = "" + let src_formatted_suffix = "" //but could be "..." + if(src) { + src_formatted = src.trim() + let src_first_newline = src_formatted.indexOf("\n") + if (src_first_newline != -1) { + src_formatted = src_formatted.substring(0, src_first_newline) + src_formatted_suffix = "..." + } + if (src_formatted.length > 50) { + src_formatted = src_formatted.substring(0, 50) + src_formatted_suffix = "..." + } + src_formatted = replace_substrings(src_formatted, "<", "<") + src = replace_substrings(src, "'", "'") + src_formatted = "  " + src_formatted + src_formatted_suffix + " " + } + //if (src_formatted == "") { console.log("_____out_eval_result passed src: " + src + " with empty string for src_formatted and text: " + text)} + let the_html = "
" + src_label + " " + src_formatted + " is..." + text + "
" + append_to_output(the_html) + //$('#js_textarea_id').focus() fails silently + if(window["document"]){ + let orig_focus_elt = document.activeElement + if(orig_focus_elt.tagName != "BUTTON"){ //if user clicks eval button, it will be BUTTON + //calling focus on a button draws a rect around it, not good. + //if user hits return in cmd line, it will be INPUT, + //Its not clear that this is worth doing at all. + orig_focus_elt.focus() + } + } + //if(Editor.get_cmd_selection().length > 0) { cmd_input_id.focus() } + //else { myCodeMirror.focus() } + } + + static init_class(){ //does not init the process, just the Py class + if (operating_system === "win") { Py.python_executable_path = "python" } + else if (operating_system === "mac") { Py.python_executable_path = "python3" } //python gets you python2.7 + else if (operating_system === "linux") { Py.python_executable_path = "python" } + Py.main_eval_py_path = __dirname + "/main_eval.py" //note that __dirname, when inside the job engine core folder, ends in "/core" so don't stick that on the end. + out('Py.python_executable_path set to: ' + Py.python_executable_path + '') + } + //document + static kill(){ + if(this.process){ + Py.process.kill() + } + } + //document + static init(){ + this.kill() + if(!Py.python_executable_path) { + Py.init_class() + } + this.process = spawn(this.python_executable_path, [Py.main_eval_py_path]) //[dde_apps_folder + "/main_eval.py"]); + out("New Python process created.") + this.process.stdout.on('data', (data) => { + let json_str_plus = data.toString() + //because library python code might have some misc print statements in it + //that aren't flushed, data might have those print statements in it.. + //ALSO we might get 2 valid json objects back in one string. + //shouldn't happen according to my understanding of readline, but appears to happen + //Print out the misc print statments to DDE's Output Pane, + //and any actual json objects get sent to their callback. + let begin_of_json_obj_str = '{"from": "Py.eval",' + //json_strings_plus = data.split(begin_of_json_obj_str) + //for(let json_str_plus of json_strings_plus){ //json_str_plus could be + let open_curley_index = json_str_plus.indexOf(begin_of_json_obj_str) + let close_curley_index = json_str_plus.lastIndexOf("}") + let prefix_string + let suffix_string + let json_string + if(close_curley_index === -1) { //no json_string so only a prefix, unless json_str_plus is empty + prefix_string = json_str_plus + json_string = "" + suffix_string = "" + } + else if(open_curley_index === 0){ //we have a json string and MAYBE a suffix + prefix_string = "" + json_string = json_str_plus.substring(0, close_curley_index + 1) + suffix_string = json_str_plus.substring(close_curley_index + 1) // might be empty + } + else {//curley_index is positive, so we have a prefix, a json_string and MAYBE a suffix + prefix_string = json_str_plus.substring(0, open_curley_index) + json_string = json_str_plus.substring(open_curley_index, close_curley_index + 1) + suffix_string = json_str_plus.substring(close_curley_index + 1) + } + //now print_prefix_string and json_string are set up + if(prefix_string.length > 0) { + out("Misc. output from Python: " + prefix_string) + } + if(json_string.length > 0){ + let json_obj + try { + json_obj = JSON.parse(json_string) + } + catch(err){ + out("Data from Python is not proper JSON object: " + json_string) + return + } + if(json_obj.hasOwnProperty("from") && + json_obj.from === "Py.eval") { + let callback_id = json_obj.callback_id + let cb = Py.callbacks[callback_id] + cb(json_obj) + return + } + else { + out("Data from Python is a proper JSON object, but it was not computed for DDE's Py.eval: " + json_string) + } + } + if((suffix_string.length > 0) && (suffix_string !== "\n")) { + out("Misc. output from Python: " + suffix_string) + } + }) + + //this doesn't handle normal errors because I send normal error to stdout with + //a json object of is_error: true. + this.process.stderr.on('data', (data) => { + out("error from Py: " + data) + }) + this.process.on('exit', (err_code) => { + console.log("pyprocess exiting with error code: " + err_code); + }); + this.callbacks = [this.default_callback] //clear out old callbacks, and add back the default. + this.eval('sys.path.append("' + dde_apps_folder + '")') + } + + //callback takes 1 arg, a json_object with is_error, source, result properties + static eval(python_source_code, callback=Py.default_callback){ + if(!this.process || this.process.killed) { + this.init() //will call eval to set up sys.path, so we need a timeout to let the process init before + //evaling python_source_code + setTimeout(function() { + Py.eval_aux(python_source_code, callback) + }, 1000) + } + else { + this.eval_aux(python_source_code, callback) + } + } + + static eval_aux(python_source_code, callback=Py.default_callback){ + if(last(python_source_code) === "\n") { + python_source_code = python_source_code.substring(0, python_source_code.length - 1) //will be added back later + } + let proccessed_src = replace_substrings(python_source_code, "\n", "{nL}") + let existing_cb_id = this.register_callback(callback) + proccessed_src = existing_cb_id + " " + proccessed_src + "\n" //needs \n for python readline() to complete + this.process.stdin.write(proccessed_src); + } + //returns id for the callback, a non-neg integer. + static register_callback(callback){ + let existing_cb_id = Py.callbacks.indexOf(callback) + if (existing_cb_id === -1) { + existing_cb_id = this.callbacks.length + this.callbacks.push(callback) + } + return existing_cb_id + } + + //path should end in .py + static load_file(path, as_name = null, callback=Py.default_callback){ + path = make_full_path(path, false) //don't adjust to OS, keep as slashes. + path = replace_substrings(path, "\\", "/") + this.eval("sys.path", + function(json_obj){ + let folder_array = json_obj.result + let last_slash_pos = path.lastIndexOf("/") + let dir = path.substring(0, last_slash_pos) //excludes last slash + let file_name_sans_dir = path.substring(last_slash_pos + 1) + if(file_name_sans_dir.endsWith(".py")){ + file_name_sans_dir = file_name_sans_dir.substring(0, file_name_sans_dir.length - 3) + } + let py_code = "" + if(!folder_array.includes(dir)){ + py_code = 'sys.path.append("' + dir + '")\n' + } + py_code += "import " + file_name_sans_dir + if(as_name) { + py_code += " as " + as_name + } + Py.eval(py_code, callback) + }) + } + + //called from File menu item "Load..." when user choses a .py file. + static load_file_ask_for_as_name(path){ + open_doc(python_user_interface_doc_id) + let last_slash_pos = path.lastIndexOf("/") + let file_name_sans_dir = path.substring(last_slash_pos + 1) + if(file_name_sans_dir.endsWith(".py")){ + file_name_sans_dir = file_name_sans_dir.substring(0, file_name_sans_dir.length - 3) + } + show_window({title: 'Load Python file', + content: '
' + + 'For importing:

' + + 'use "as name":

' + + ' ' + + '' + + '

', + height: 200, + callback: "Py.load_file_ask_for_as_name_cb" + }) + } + + static load_file_ask_for_as_name_cb(vals){ + if(vals.clicked_button_value === "Cancel") {} + else if (vals.clicked_button_value === "OK") { + let as_name = vals.as_name + let path = vals.path + Py.load_file(path, as_name) + } + } + + //prints to output pane + static default_callback(json_obj){ + let text = json_obj.result + let color = "#000000" //don't use "black" because that affects Py.out_eval_result + if(json_obj.is_error){ + let pos = text.indexOf(".py") + if(pos > 0) { //happens for syntax errors but not for others. + text = text.substring(pos + 6) //cut off the long, meaningless path name + } + //text = "Error: " + text //redundant with "SyntaxError" or "NameError" + color = "red" + } + Py.out_eval_result(text, color, json_obj.source) + } +} +module.exports.Py = Py + +Py.process = null //doc +Py.python_executable_path = null +Py.main_eval_py_path = null +Py.callbacks = [] + +var {replace_substrings} = require("./utils") +var spawn = require('child_process').spawn diff --git a/core/robot.js b/core/robot.js index 7c750e31..a8f82e35 100644 --- a/core/robot.js +++ b/core/robot.js @@ -1654,7 +1654,18 @@ Dexter.exit = function(...args){ return make_ins("x", ...args) } Dexter.prototype.exit = function(...args){ args.push(this); return Dexter.exit(...args) } -Dexter.empty_instruction_queue_immediately = function() { return make_ins("E") } +Dexter.empty_instruction_queue_immediately = function(){ + //return make_ins("E") + let num = Instruction.w_address_name_to_number("RESET_PID_AND_FLUSH_QUEUE") + if(num >= 0) { + return [ Dexter.write_fpga(num, 1), //this flushes the queue + Dexter.write_fpga(num, 0) //this resets the ode to normal so Dexter can accept new instructions + ] + } + else { + shouldnt("Dexter.empty_instruction_queue_immediately could not find w name: RESET_PID_AND_FLUSH_QUEUE.") + } +} Dexter.prototype.empty_instruction_queue_immediately = function(...args){ args.push(this); return Dexter.empty_instruction_queue_immediately(...args) } Dexter.empty_instruction_queue = function() { return make_ins("F") } @@ -2154,7 +2165,7 @@ Dexter.instruction_type_to_function_name_map = { D:"pid_move_to_straight", d:"dma_read", e:"cause_dexter_error", //fry - E:"empty_instruction_queue_immediately", //new Sept 1, 2016 + //E:"empty_instruction_queue_immediately", //new Sept 1, 2016 F:"empty_instruction_queue", //new Sept 1, 2016 G:"get_robot_status_immediately", //new Sept 1, 2016. Deprecated Dec 8, 2020 g:"get_robot_status", //fry @@ -2363,7 +2374,10 @@ Dexter.prototype.set_link_lengths_using_node_server = function(job_to_start){ */ Dexter.prototype.set_link_lengths_using_node_server = function(job_to_start){ - let path = "http://192.168.1.142/edit?edit=/srv/samba/share/Defaults.make_ins" + let ip = job_to_start.robot.ip_address + let path = //"http://" + ip + "/edit?edit=/srv/samba/share/Defaults.make_ins" + "http://192.168.1.142/edit?edit=/srv/samba/share/Defaults.make_ins" + let options = {uri: path} //, timeout: 1000} let content = get_page(path) if(content.startsWith("Error: ")) { warning("set_link_lengths_using_node_server with path: " + path + diff --git a/core/storage.js b/core/storage.js index 6982d93c..f6fe3d09 100644 --- a/core/storage.js +++ b/core/storage.js @@ -331,7 +331,7 @@ function read_file_async_from_dexter_using_node_server(dex_instance, path, callb let colon_pos = path.indexOf(":") path = path.substring(colon_pos + 1) // path comes in as, for example, "Dexter.dexter0:foo.txt if(path.startsWith("/")) { - path = path.substring(1) //because of crazy node server editor's code + //path = path.substring(1) //because of crazy node server editor's code } else { //doesn't start with slash, meaning relative to server default path = "/dde_apps/" + path // on the node webserver, starting with / means ?srv/samba/share/ @@ -797,7 +797,9 @@ function add_default_file_prefix_maybe(path){ path = path.substring(8) return dde_apps_folder + path } - else if(path.startsWith("./")) { return "dde_apps/" + path.substring(2) } + else if(path.startsWith("./")) { //return "dde_apps/" + path.substring(2) + return dde_apps_folder + path.substring(1) + } else if (path.startsWith("../")) { let core_path = path.substring(3) let last_slash_pos = dde_apps_folder.lastIndexOf("/") @@ -1042,23 +1044,6 @@ function load_files(...paths) { //it won't get done BUT need dde_error to print out the loading file message. dde_error(file_mess) } - /* - let prev_loading_file = window["loading_file"] - window["loading_file"] = resolved_path - let result_obj = eval_js_part2 is not part of core/job engine so - // we can't use it here. - // eval_js_part2(content, false) // warning: calling straight eval often - //doesn't return the value of the last expr in the src, but my eval_js_part2 usually does. - //window.eval(content) - window["loading_file"] = prev_loading_file //when nested file loading, we need to "pop the stack" - if(result_obj.error_message){ - dde_error("While loading the file: " + resolved_pathresolved_path + - "
the file exists, but contains the JavaScript error of:
" + - err.message) - } - else { result = result_obj.value - out("Done loading file: " + resolved_path, "green") - }*/ out("Done loading file: " + resolved_path, "green") } return result diff --git a/core/utils.js b/core/utils.js index 8c4c1a70..7a372554 100644 --- a/core/utils.js +++ b/core/utils.js @@ -469,7 +469,7 @@ module.exports.is_json_date = is_json_date function starts_with_one_of(a_string, possible_starting_strings){ for (let str of possible_starting_strings){ - if (a_string.startsWith(str)) return true + if (a_string.startsWith(str)) { return true } } return false } @@ -1200,11 +1200,12 @@ module.exports.function_param_names_and_defaults_lit_obj = function_param_names_ function shallow_copy(obj){ //copies only enumerable, own properites. Used in //copying Job's user_data at start let result = obj - if(Array.isArray(obj)){ + if(result === null) {} //typeof returns "object" for null + else if(Array.isArray(obj)){ result = [] for (let elt of obj) { result.push(elt) } } - else if (typeof(obj) == "object"){ + else if (typeof(obj) == "object"){ //typeof returns "object" for null result = {} for(let name of Object.keys(obj)){ result[name] = obj[name] diff --git a/doc/guide.html b/doc/guide.html index dff02cbf..c3cebbce 100644 --- a/doc/guide.html +++ b/doc/guide.html @@ -8,8 +8,8 @@
About This is Dexter Development Environment
- version: 3.7.2
- released: Jan 13, 2021 + version: 3.7.3
+ released: Feb 5, 2021

DDE helps you create, debug, and send software to a Dexter robot. You can use any JavaScript augmented with DDE-specific functions to help find out about, @@ -1395,7 +1395,8 @@
Step 3
Ignore the parts that use the Underscore library.
A more formal (and long) tutorial.
-
JS for Python Programmers +
JS for Python Programmers + DDE supports calling Python from JavaScript and DDE's user interface. Python and JavaScript are two of the most popular general purpose programming languages in use today. Though they differ in many details, for describing the instructions that are the core of DDE's Jobs, they are amazingly similar for the most common operations. Especially with DDE's tools for learning @@ -1968,7 +1969,9 @@
Step 3
Syntax Checker DDE uses an automatic syntax checker named ESLint. It - indicates syntax errors in the Editor pane as soon as you enter them. + indicates syntax errors in the Editor pane as red dots in the left margin. + The tooltip on a red dot gives you details about the error. +

There are many different styles of JS coding and different versions of JS so its hard to get such errors correct. Even if you have syntax errors indicated, your code may run fine. As a first pass though, try @@ -2277,5 +2280,4 @@

Boot

latest Release Notes
and the Known Issues to help hone your suggestions. -
\ No newline at end of file diff --git a/doc/ref_man.html b/doc/ref_man.html index a3ba0b4d..6af95554 100644 --- a/doc/ref_man.html +++ b/doc/ref_man.html @@ -3774,7 +3774,7 @@ instruction so that you can have one job that feeds instructions to multiple serial robots as well as multiple Dexters. -
  • do_list Default: [] (an empty array). An array of instructions to be sent to the job's robot when the +
  • do_list Default: [] (an empty array). The do list of a Job, represented as an array of instructions to be sent to the job's robot when the Job is run. There are a number of different kinds of instructions, including user-defined JavaScript functions that can call arbitrary JS code.

    @@ -3935,7 +3935,8 @@ Warning: When there is more than one Job running on a robot, a Job can't expect Dexter to be where it told it to go, as another Job might have changed its position.
  • -
  • if_robot_status_error the value is a do_list item that +
  • if_robot_status_error + The value is a do_list item that is automatically put on the do_list when a Dexter robot_status comes back to DDE with an error (i.e. a non-zero error code.)

    @@ -3951,6 +3952,8 @@ It prints a warning message to the Output pane and returns a Control.error instruction, stopping the Job.

    + If the instruction is null, the Job will ignore the error and just keep going. +

    You can see the possible error codes.
  • @@ -4187,9 +4190,14 @@
    Robot - A robot is a machine that can execute the instructions in its instruction set. In general, + A robot is a machine that can execute the instructions in its instruction set. + There are lots of different kinds of instructions. + In general, each kind of robot will have its own instruction set. The primary - difference between robots is the difference in their instruction sets. + difference between robots is the difference in their instruction sets, + though there are some kinds of instructions that can be run on any kind of Robot. + Those "common instructions" opperate completely within DDE as they have + no need to be sent to a special piece of hardware.

    All Jobs have a default robot property. You can access the robot property from a running job like so:
    @@ -5705,7 +5713,15 @@ returns the mat.
    - +
    null, undefined + A null or undefined on the do_list does nothing. It is a "no-op". + You wouldn't explicitly put one of these on a do_list, but you may have a + function on the do_list that is used for a side-effect. It can have a + return null statement in it or no return at all + (which returns undefined). +
    + +
    Brain Brain is a virtual robot used to manage other robots or do purely software tasks. @@ -10639,7 +10655,7 @@

    Cover DDE Window

    a given release starts out empty, the release itself will contain the previous fixes, plus likely some new, larger amounts of code. -
    download_all +
    download_all This method downloads patch files from github into the folder Documents/dde_patches/patches_for_(your dde version number). Each patch file will have a small amount of code. The file's @@ -10657,7 +10673,7 @@

    Cover DDE Window

    every time they launch DDE.
    -
    load +
    load Load into the DDE JavaScript environment, the patch files for the release of DDE that you're running.

    @@ -10747,8 +10763,227 @@

    Cover DDE Window

    eval PatchDDE.load_dde_2_to_3()
    +
    +
    Python + DDE is a JavaScript-based development environment, BUT it facilitates + calling Python 3 code. +

    + Behind the scenes, DDE creates a process to run Python. + Python code will be evaluated in that process until you kill, it, init a fresh one, + or relaunch DDE. Thus you can set global variables or def's in it, import files into + it and expect those modifications to be there when you attempt to get values or call + functions. +

    Similarities + between JavaScript and Python. + +

    Installation + To use Python from DDE, the Python 3 interpreter must be installed on your computer.
    + MacOS has, for years, included Python 2.7.
    + In the terminal, type: python
    + More modern versions of MacOS include Python 3.
    + In the terminal, type: python3 + to get a read_eval_print loop for Python. +

    + To install Python on other Operating Systems see + Python.org +

    +
    User Interface +
      +
    • When you are editing a file who's name ends in ".py", + clicking will evaluate the selection (or whole editor text if + no selection) as Python code.
      + The result will appear in the Output pane. +

      +

    • +
    • + When you select from the command line menu, + and hit ENTER in the command line, the command line text will be evaluated as Python code.
      + The result will appear in the Output pane. +

      +

    • +
    • + The File menu/Load ... item let's you choose a file. + If you choose one with a ".py" extension, + you will be prompted to enter an "as name". + If you then click , + that is the equivalent of the python code:
      + import /the/file.py as somename
      + You'll see confirmation in the Output pane. +
    • +
    +
    +
    Py.python_executable_path + This variable contains a string that is the path to the Python3 interpreter on your computer. + It defaults to something that may just work on your computer, but if it doesn't, + you can set this variable before Py.init is called, + (or Py.eval is called the first time). +

    + If you can type in to a terminal some string like python3, + or perhaps python and it puts you in a Python3 read_eval_print loop, + then that string will very likely work for the value of this variable. +

    + Examples:
    + Py.python_executable_path
    + Py.python_executable_path = "python3"
    +

    +
    Py.eval + The Py class holds utilites for evaluating Python code embedded in JavaScript code. + The most important function is Py.eval.
    + Parameters:
    + python_source_code Required. A string of Python source code to be + evaluated in the Python process.
    + callback A function. Default: Py.default_callback + The callback is called when Python is done evaluating the source code. + It is called with one argument, a JSON object with fields: + json_obj = {"from": "Py.eval", "is_error": is_error, "source": src, "callback_id": callback_id, "result": result} -
    - -
    \ No newline at end of file +
      +
    • from Always "Py.eval". Allows you to distinguish this JSON object from others.
    • +
    • is_error Boolean.
    • +
    • source The string of python_source_code that Py.eval was called with.
    • +
    • callback_id (internal) The id of the callback that Py.eval was called with.
    • +
    • result The result of the evaluation of the python_source_code. + This will be JSON, ie null, a boolean, a number, a string, an array or + nested object of those types. + If evaluating python_source_code returns a value, it will be of that type. + Otherwise, if the python_source_code returns Python's None, it will be + null. + Python's None is more similar to JS undefined than it is to + null, but JSON can't represent undefined. +
    • +
    + If you launch DDE and call Py.eval, a new Python process will be created + for evaluating the Python code. That process will remain to run more Python code until + you kill it, init a new Python process, or quit DDE. +

    + Examples:

    + Py.eval("len([4, 5])")

    + + Py.eval('foo = "hi from py"')
    + Py.eval('foo')

    + + Py.eval('import math')
    + Py.eval('math.pi')
    +

    Py.eval("2 + 3",
    +        function(json_obj){
    +          out(json_obj.source + " => " + json_obj.result)
    +       })    
    +
    
    +new Job({
    +  name: "my_job",
    +  user_data: {j2: null},
    +  do_list: [
    +    function(){ Py.eval("import math") },
    +    Control.wait_until(1),
    +    function(){
    +      let the_job = this
    +      Py.eval("math.pi * 10",
    +               function(json_obj){
    +                 the_job.user_data.j2 = json_obj.result
    +               })
    +    },
    +    Control.wait_until(function() {
    +                         out("this.user_data.j2: " + this.user_data.j2)
    +                         return this.user_data.j2 }),
    +    function() {
    +      return Dexter.move_all_joints(0, this.user_data.j2)
    +    }
    +  ]
    +})    
    + Like the other examples, the last example isn't practical, but understanding it will help you + integrate Python dynamically into a running job. +
      +
    1. First we initialize user_data.j2 to null. It's going to hold the joint angle that Python computes. +
    2. +
    3. We import math to set up our Python environment. + Replace math with packages of your choice. +
    4. +
    5. Control.wait_until(1) pauses the Job for 1 second, + giving Python a chance to initialize and load the math package. + This 1 second is longer than the rest of the job put together. + It could be cut down, or even moved before the Job if you like. + The import doesn't need to be executed after the first time the Job is run. +
    6. +
    7. The next function does the real work, evaluating math.pi * 10 in Python. + We COULD have done this particular computation before, and outside of, the Job, + but we want to show that this can be done while running the Job. You might want + to pass in arguments that are computed while the Job is running, for instance. +

      + The callback function is a closure around the_job. + Since JS won't close over + this properly, we must create a local variable via + let the_job = this + before defining the inner function. +

      + That inner function is the 2nd argument to + Py.eval i.e. our callback. Once Python has completed the multiply, + this callback will be called with a json_object containing the result. + The callback sets Job.my_job.user_data.j2 + to the result from Python. +
    8. +
    9. Control.wait_until calls the function in its first argument + until it returns a truthy value. + When our callback changes the value of user_data.j2 + from null to 31.4, Control.wait_until is + done waiting and we move on to the next instruction. +

      + In this wait_until function, we print out the value of user_data.j2 + just so you can see that the code is still working, and exactly when + it has a good value. During this time, the Job button will be yellow. +

      + For such a trivial computation, there might be no null print outs, + and you might not even need a Control.wait_until. + But if you tell Python to perform a long computation, Control.wait_until + pauses the Job until we have the results back from Python. +
    10. +
    11. Our final Job instruction retrieves the value from user_data and uses it + as the angle to move joint 2 to. + We must wrap our call to Dexter.move_all_joints in a function, + because its crucial argument, this.user_data.j2 + must be evaled when this instruction is run, not at Job definition time. +
    12. +
    +
    +
    Py.load_file + Imports a file into DDE's Python process.
    + Parameters:
    + path Required. A string of the file to load. If it doesn't start with slash, + it will have dde_apps_folder prepended to it. + If it starts with "../", it will have the parent folder of dde_apps_folder + prepended to it.
    + as_name Default: null. indicating that the + file name (sans folder, sans extension) will be used as the as_name, + just like import foo means the same thing as + import foo as foo
    + callback Same as "callback" for Py.eval_doc_id +

    + Example:
    + Py.load_file("hello.py") which means the same as
    + Py.load_file("/Users/Fry/Documents/dde_apps/hello.py") (or whatever + folder dde_apps_folder evals to.)
    + Py.load_file("hello.py", "howdy") (uses "howdy" as the "as name".) +

    +
    Py.kill + Ends the Python process. The Python process shouldn't take up much resources + when idle. One reason to kill it is to get rid of the state currently + in the Python process so the next time Py.eval is called, + it will start afresh. +
    +
    Py.init + This function +
      +
    • Kills the running Python process if any
    • +
    • Creates a new Python process
    • +
    • Puts dde_apps_folder on Python's + sys.path list +
    • Makes the new Python process ready to accept eval commands from DDE.
    • +
    + It is called automatically the first time you call Py.eval + after launching DDE or killing the Python process, + so there isn't much need to call this function explicitly. + After calling this function, your first call to Py.eval will + be a fraction of a second faster. +
    + + \ No newline at end of file diff --git a/doc/release_notes.html b/doc/release_notes.html index eec18e1a..4581af3e 100644 --- a/doc/release_notes.html +++ b/doc/release_notes.html @@ -5,10 +5,43 @@ } .doc_details summary { font-weight: 600; } +
    v 3.7.3, Feb 5, 2021 +Highlights: New Python interface. New easy way to turn on/off the Startup operations +in Dexter like the PHUI interface. Bug fixes for Linux, node server interface, show_window +
      +
    • set_link_lengths_using_node_server now gets it's ip address from the robot of the "job_to_start"
    • +
    • set_link_lengths_using_node_server now sets a timeout for its attempt at 1 second, making it fail faster.
    • +
    • Now linux keybindings are like Windows keybindings, + not like Mac as they previously were.
    • +
    • Fixed bug in switching to a file on the menu of files.
    • +
    • Minor bug fix in show_window callback function handling for anonymous functions.
    • +
    • Minor bug fix in show_window. Now passing in title: "" + will create a window with no title bar without erroring.
    • +
    • Extended the documentation for new Job parameter, + if_robot_status_error to include how to ignore an error. + Extended the description of the different kinds of instructions to include null and undefined, + under Ref Man/Robot and Ref Man/Robot/Robot Instructions.
    • +
    • Fixed read_file_async for node server to leave in beginning slash of path.
    • +
    • New menu item: Jobs menu/dexter tools/start options to "edit" Dexter's autoexec.jobs file + controlling Dexter start up operations. + Let's you turn on and off PHUI mode amongst other operations.
    • +
    • Improved User Guide/Debugging/Syntax Checker, first paragraph.
    • +
    • Fixed click_help doc scrolling for patch system methods.
    • +
    • New class Py for running Python code from within DDE JavaScript code. + DDE user interface access with: +
        +
      • Eval button
      • +
      • Command Line
      • +
      • File menu/Load...
      • +
      + Documentation in RefMan/Python +
    +
    +
    v 3.7.2, Jan 13, 2021 -Highlights: Improved robustness of connecting DDE to Dexter includoing error handling +Highlights: Improved robustness of connecting DDE to Dexter including error handling and no longer uses ping (to aid Linux installations and speedup). - Simplied Misc Pane header/Simulation panel header by removing the extra "Dexter" menu. + Simplified Misc Pane header/Simulation panel header by removing the extra "Dexter" menu. robot_status: now 4 different possibilities: g0 (the original), g1, g2, and g_other for statuses that are not yet well defined, but we can still look at them in the robot status dialog. TestSuite enhancements. diff --git a/editor.js b/editor.js index 7257e4fc..04ac14f3 100644 --- a/editor.js +++ b/editor.js @@ -38,33 +38,32 @@ Editor.init_editor = function(){ //indent each line to the line above it when you hit Return indentUnit: 4, //default is 2. Using 4 makes it same size as tab, then cmd(mac or ctrl(PC) open square will unindent by this amount. extraKeys: //undo z and select_all (a) work automaticaly with proper ctrl and cmd bindings for win and mac - ((operating_system === "win") ? - {"Alt-Left": Series.ts_or_replace_sel_left, - "Alt-Right": Series.ts_or_replace_sel_right, - "Shift-Alt-Right": Series.ts_sel_shift_right, //no non ts semantics decided to cut this as is uncommonly used and shift right is "continue selection" in normal test editor and conde mirror AND alt_shift_right too hairy to remember. - "Alt-Up": Series.ts_or_replace_sel_up, - "Alt-Down": Series.ts_or_replace_sel_down, - "Ctrl-E": eval_button_action, //the correct Cmd-e doesn't work - "Ctrl-J": Editor.insert_new_job, - "Ctrl-O": Editor.open_on_dde_computer, - "Ctrl-N": Editor.edit_new_file, - "Ctrl-R": Editor.move_to_instruction, - "Ctrl-S": Editor.save //windows - } : //Mac - {"Alt-Left": Series.ts_or_replace_sel_left, - "Alt-Right": Series.ts_or_replace_sel_right, - "Shift-Alt-Right": Series.ts_sel_shift_right, //no non ts semantics see above for why cuttong this - "Alt-Up": Series.ts_or_replace_sel_up, - "Alt-Down": Series.ts_or_replace_sel_down, - "Cmd-E": eval_button_action, //the correct Cmd-e doesn't work - "Cmd-J": Editor.insert_new_job, - "Cmd-N": Editor.edit_new_file, - "Cmd-O": Editor.open_on_dde_computer, - "Cmd-R": Editor.move_to_instruction, - "Cmd-S": Editor.save //mac - }) - - + ((operating_system === "mac") ? //the "Alt" key on a Mac is labeled "option" on mac keyboard. + {"Alt-Left": Series.ts_or_replace_sel_left, + "Alt-Right": Series.ts_or_replace_sel_right, + "Shift-Alt-Right": Series.ts_sel_shift_right, //no non ts semantics see above for why cuttong this + "Alt-Up": Series.ts_or_replace_sel_up, + "Alt-Down": Series.ts_or_replace_sel_down, + "Cmd-E": eval_button_action, //the correct Cmd-e doesn't work + "Cmd-J": Editor.insert_new_job, + "Cmd-N": Editor.edit_new_file, + "Cmd-O": Editor.open_on_dde_computer, + "Cmd-R": Editor.move_to_instruction, + "Cmd-S": Editor.save //mac + }: //"win" and "linux" + {"Alt-Left": Series.ts_or_replace_sel_left, + "Alt-Right": Series.ts_or_replace_sel_right, + "Shift-Alt-Right": Series.ts_sel_shift_right, //no non ts semantics decided to cut this as is uncommonly used and shift right is "continue selection" in normal test editor and conde mirror AND alt_shift_right too hairy to remember. + "Alt-Up": Series.ts_or_replace_sel_up, + "Alt-Down": Series.ts_or_replace_sel_down, + "Ctrl-E": eval_button_action, //the correct Cmd-e doesn't work + "Ctrl-J": Editor.insert_new_job, + "Ctrl-N": Editor.edit_new_file, + "Ctrl-O": Editor.open_on_dde_computer, + "Ctrl-R": Editor.move_to_instruction, + "Ctrl-S": Editor.save //windows + } + ) }); undo_id.onclick = Editor.undo set_menu_string(undo_id, "Undo", "z") @@ -877,7 +876,7 @@ Editor.edit_file = function(path, content){ //path could be "new buffer" } //cur buff is a new buffer, and the target path is not else if (Editor.get_javascript().trim().length == 0) { //don't ask about deleting the new buf, just do it; - ditor.remove_new_buffer_from_files_menu() //get rid of the current "new buffer" + Editor.remove_new_buffer_from_files_menu() //get rid of the current "new buffer" const path_already_in_menu = Editor.set_files_menu_to_path(new_path) if (!path_already_in_menu) { Editor.add_path_to_files_menu(new_path) } if(content) { diff --git a/eval.js b/eval.js index 4b38f85c..8c2d11ec 100644 --- a/eval.js +++ b/eval.js @@ -44,17 +44,20 @@ var latest_eval_button_click_source = null //Only called by eval_button_action //when this is called, there is no selection, so either we're evaling the whole editor buffer //or the whole cmd line. +//beware, the code *might* be HTML or python at this point function eval_js_part1(step=false){ //tricky: when button is clicked, Editor.get_any_selection() doesn't work, //I guess because the button itself is now in focus, //so we grab the selection on mousedown of the the Eval button. //then use that here if its not "", otherwise, Editor.get_javascript("auto"), getting the whol editor buffer let src + let src_comes_from_editor = false if(previous_active_element && previous_active_element.parentNode && previous_active_element.parentNode.parentNode && previous_active_element.parentNode.parentNode.CodeMirror){ src = Editor.get_javascript("auto") //if sel in editor, get it, else get whole editor + src_comes_from_editor = true } //let sel_obj = window.getSelection() else if (selected_text_when_eval_button_clicked.length > 0) { @@ -84,7 +87,7 @@ function eval_js_part1(step=false){ latest_eval_button_click_source = src if (src.trim() == ""){ open_doc(learning_js_doc_id) - warning("There is no JavaScript to execute.
    See Learning JavaScript " + + warning("There is no code to execute.
    See Learning JavaScript " + "in the Documentation pane for help.") } else{ @@ -97,6 +100,11 @@ function eval_js_part1(step=false){ if(html_db.string_looks_like_html(src)){ render_html(src) } + else if(Editor.current_file_path.endsWith(".py") && + src_comes_from_editor + ){ + Py.eval_part2(src) + } else { eval_js_part2((step? "debugger; ": "") + src) //LEAVE THIS IN RELEASED CODE } diff --git a/index.html b/index.html index 8416afb2..82d5e2be 100644 --- a/index.html +++ b/index.html @@ -658,7 +658,8 @@
  • Calibrate Dexter...
  • Ping Dexter...
  • Show errors.log
  • - +
  • Start Options
  • +
  • Run Job on Dexter
  • Messaging...
  • @@ -754,6 +755,7 @@
    -->