HaddingtonDynamics / Dexter

GNU General Public License v3.0
374 stars 85 forks source link

Job engine to run DDE jobs on Dexter #60

Open JamesNewton opened 5 years ago

JamesNewton commented 5 years ago

Starting this issue to record development history so we know why something was done in the future. The current status (only available on the very latest images) is here:
https://github.com/HaddingtonDynamics/Dexter/wiki/DDE/#job-engine-on-dexter

Goal: Make it easy for DDE on a PC to write jobs to Dexter which are then run ON Dexter. These are one time jobs, with DDE in control of dispatch, so you don't have to SSH in, or depend on SAMBA or anything else. The functionality can even be integrated into DDE.

And this allows us to start the job ourselves, it doesn't have to be started automatically when Dexter fires up. Experts can figure out how to add an /etc/rc.local

First, we must have DDE on Dexter. In this case, we don't need to make a distributable package, we just want to run the source. And having the source directly run means we can develop on Dexter and also use parts of it in other ways (see Job Engine below). So instead of installing the Electron package, we just install the source and use npm to pull in the dependencies. Note: This requires the 16.04 version of the operating system on Dexter, and the Dexter must be connected to the internet (e.g. plugged into a router, or wifi)

$ cd /root/Documents
$ git clone https://github.com/cfry/dde
$ cd dde
$ npm i
$ npm run start

Note: If you need to go back to a prior version, use git checkout v3.5.2 or whatever. v3.0.7 is known to work as well. Also, the node run start may not work until after you comment out the serial stuff (see below).

Of course, the GUI part of the app will only be visible with an X-Server running and since Dexter does not have a video adapter, this must be a remote the X-Windows Desktop. On the current images, an icon is provided to launch DDE from the desktop when logged in via X-Windows. The program takes a while to load (need faster SD Card and interface?) but operation isn't horribly slow.

A "dde_apps" folder for the GUI run of DDE is created under the "/root" folder (alongside Documents, not in it) for the DDE application. Setting the dexter0 ip address to localhost in the /root/dde_apps/dde_init.js file allows local connection of DDE to DexRun.

To run DDE jobs without the full DDE GUI interface, e.g. via SSH, you can start them from ~/Documents/dde with the command:
node core define_and_start_job job_file.dde

There are a few things to tweek before that will work:

From /root/Documents/dde, nano core/serial.js and comment out the line: const SerialPort = require('serialport'). There is some mismatch between the component that manages serial ports on our OS vs others.

Job Engine Initialization: When run for the first time, the job engine creates a dde_init.js file in the /root/Documents/dde_apps folder. (note this is different than for the GUI DDE on Dexter which is in /root/dde_init.js). The job engine defaults to simulate, so the jobs don't actually make the robot move until the dde_init file is edited to add ,simulate: false after the IP address in the definition of dexter0. The IP address is set to localhost so it will work no matter what IP address Dexter is actually assigned.

/root/Documents/dde_apps/dde_init.js:

persistent_set("ROS_URL", "localhost:9090") //required property, but you can edit the value.
persistent_set("default_dexter_ip_address", "localhost") //required property but you can edit the value.
persistent_set("default_dexter_port", "50000") //required property, but you can edit the value.
new Dexter({name: "dexter0", simulate: false}) //dexter0 must be defined.

Keep in mind the version of DDE on Dexter may need to be updated. It's 3.0.7 on the initial release of the 16.04 image.

You may want to use node core define_and_start_job /srv/samba/share/job_file.dde

When you 'run a job' as defined above, it sets window.platform to "node". If you are in dde, window.platform == "dde" will evaluate to true. That means you can customize any code written based on this "platform" i.e.

if(window.platform == "node")      { /* hey I'm in node. */ }
else if (window.platform == "dde") { /* we're in dde! */ }

The system software takes advantage of this. One important case is that the "out" function is defined as:

function out(val="", color="black", temp=false, code=null){
    if(window.platform == "node") { console.log(val) }
    else { /* do formatting and print to DDE's Output pane */ }
}

Thus when running on node, 'out' only pays attention to its first arg, and it sends the first arg directly to the console.

On the development image, (for the next release) there is an updated /etc/systemd/system path and service which looks for any changes in the /srv/samba/share/job folder and when seen, executes the following script "RunJob" (also in that folder):

#!/usr/bin/env bash

cd /root/Documents/dde
for i in /srv/samba/share/job/*.dde; do
        [ -f "$i" ] || break
        echo "running $i"
        sudo node core define_and_start_job $i >> $i.$(date +%Y%m%d_%H%M%S).log
        # must use sudo or node doesnt know who the user is and cant find the dde_apps folder.
        rm $i
done

which then fires off the job engine, does the job, and puts the resulting output in a YYMMDD_HHMMSS.log file.

For example:

new Job({name: "test_job_engine", do_list: [
    Dexter.write_to_robot(
        "new Job({name: \"my_job\", do_list: [Dexter.move_all_joints([30, 45, 60, 90, 120]), Dexter.move_all_joints([0, 0, 0, 0, 0])]})"
        , "job/try.dde"
        )
    ]})

makes Dexter move, and then the file /job/try.dde.20190327_233018.log contains:

in file: /root/Documents/dde/core/index.js
top of run_node_command with: /usr/bin/node,/root/Documents/dde/core,define_and_start_job,/srv/samba/share/job/try.dde
top of node_on_ready
operating_system: linux
dde_apps_dir: /root/Documents/dde_apps
        loading persistent values from /root/Documents/dde_apps/dde_persistent.json
Done loading persistent values.
load_files called with: dde_init.js
        loading file: /root/Documents/dde_apps/dde_init.js
Done loading file: /root/Documents/dde_apps/dde_init.js
cmd_name: define_and_start_job args: /srv/samba/share/job/try.dde
load_files called with: /srv/samba/share/job/try.dde
        loading file: /srv/samba/share/job/try.dde
Done loading file: /srv/samba/share/job/try.dde
Job: my_job pc: 0 <progress style='width:100px;' value='0' max='2'></progress> of 2. Last instruction sent: {instanceof: move_all_joints 30,45,60,90,120}&nbsp;&nbsp;<button onclick='inspect_out(Job.my_job)'>Inspect</button>
Creating Socket for ip_address: localhost port: 50000 robot_name: dexter0
Now attempting to connect to Dexter: dexter0 at ip_address: localhost port: 50000 ...
Succeeded connection to Dexter: dexter0 at ip_address: localhost port: 50000
Job: my_job pc: 0 <progress style='width:100px;' value='0' max='2'></progress> of 2. Last instruction sent: {instanceof: move_all_joints 30,45,60,90,120}&nbsp;&nbsp;<button onclick='inspect_out(Job.my_job)'>Inspect</button>
Job: my_job pc: 1 <progress style='width:100px;' value='1' max='2'></progress> of 2. Last instruction sent: {instanceof: move_all_joints 0,0,0,0,0}&nbsp;&nbsp;<button onclick='inspect_out(Job.my_job)'>Inspect</button>
Job: my_job pc: 2 <progress style='width:100px;' value='2' max='3'></progress> of 3. Last instruction sent: Dexter.get_robot_status &nbsp;&nbsp;<button onclick='inspect_out(Job.my_job)'>Inspect</button>
top of Job.stop_for_reason with reason: Finished all do_list items.
Job: my_job pc: 3 <progress style='width:100px;' value='3' max='3'></progress> of 3. Done.&nbsp;&nbsp;<button onclick='inspect_out(Job.my_job)'>Inspect</button>
Done with job: my_job

I'm happy about the ability to get a job onto Dexter and run it /on Dexter/ using nothing more than DDE. I like that the job file is deleted after it's run. I like that systemd is /apparently/ smart enough not to start the script until the file is done being written (apparently?) and I like that the output is retained.

What I don't like is that it's retained forever. If people use that alot, it's going to fill up the folder with junk. I see a few easy solutions and some harder ones.

  1. When a new job is run, log files from the prior jobs are erased. e.g. add rm /srv/samba/share/job/*.log to the RunJob script.
  2. When DDE "picks up" the log file, it could write back a zero length string to that file name, and DexRun could be changed to make it just delete the file when it is written a zero length string. (I really don't like that idea, it has MANY problems.
  3. Implement shelling out to bash when read_from_robot is called with a <filename> string. This on the TODO list anyway, sometime after slaying the dragon. (note: it's on READ not write, because the output of the bash shell gets buffered back to DDE, you write the file, then read the result, which actually triggers running the file). I don't want to do that yet, because it's complex.

Which reminds me, there is currently no way to know what the log file name will be, so DDE can't really read back the result. Since the job file gets whacked anyway, I think the log file should NOT include the date and time and just be .log. So the test.dde job would result in a test.dde.log file, DDE can read that, and (as per option 1) next time you write test.dde, the old test.dde.log file gets whacked automatically and replaced with a new one. That seems like a simple solution, no?

Last problem, DDE doesn't know when the job is finished. So the log file keeps expanding, and shouldn't be read_from_robot'ed until it's done. I think the solution to that is to always write the log into the file with date and time, then when the job has finished, rename that file to the .log name. DDE can poll and when it returns more than a null string, it can be sure it's getting the entire string.

JamesNewton commented 5 years ago

Ok, a few changes:

  1. There is a sub folder under job called run, and that run folder is what is watched and triggers the service, not the job folder. That allows things in job to be edited / changed without re-triggering the service.

  2. There is a sub folder under job called logs where log files end up.

  3. The systemd files were changed as follows:
    ddejob.path

    [Unit]
    Description=DDEJob
    [Path]
    PathModified=/srv/samba/share/job/run
    [Install]
    WantedBy=multi-user.target

ddejob.service

[Unit]
Description=DDEJob
[Service]
ExecStart=/srv/samba/share/job/RunJobs

and then started with

sudo systemctl enable ddejob.path
sudo systemctl start ddejob.path

Note: if you edit those, do a

sudo systemctl daemon-reload

The RunJob script is:

#!/usr/bin/env bash

# first remove prior job logs and junk

cd /root/Documents/dde
for i in /srv/samba/share/job/run/*.dde; do
        [ -f "$i" ] || break
        jobname=$(basename -- "$i")
        echo "running $jobname"
        echo "Logfile for $jobname on $(date +%Y%m%d_%H%M%S)">/srv/samba/share/job/logs/$jobname.tmp
        sudo node core define_and_start_job $i >> /srv/samba/share/job/logs/$jobname.tmp
        # must use sudo or node doesnt know who the user is and cant find the dde_apps folder.
        rm /srv/samba/share/job/logs/*.log
        mv /srv/samba/share/job/logs/$jobname.tmp /srv/samba/share/job/logs/$jobname.log
        # copy large log files only after they are fully written so dde doesnt seem them until complete
        rm $i
        # remove the job file
done

so now the DDE job:

new Job({name: "test_job_engine", do_list: [
    Dexter.write_to_robot(
        "new Job({name: \"my_job\", do_list: [Dexter.move_all_joints([30, 45, 60, 90, 120]), Dexter.move_all_joints([0, 0, 0, 0, 0])]})"
        , "job/run/try.dde"
        )
    ]})

has the same result, but we can add code to read_from_robot("my_job_log", "job/logs/try.dde.log") and when that returns nothing, just loop until it returns the result.

Seems to work quite nicely.

cfry commented 5 years ago

Rather than write the file to

"job/run/try.dde"

How about writing it to

"job/run/my_job.dde"

If the script is just looking for any files

in the job/run folder,'

then it will be able to find it

AND in case something goes wrong,

we can at least easily identify

what job it is.

Same thing for

job/logs/try.dde.log

IE DDE will know the name of the job

it wants to check, so can ask explicitly

for job/logs/my_job.dde.log

But if something is left hanging around, etc.

then we'll be able to debug easier.


I've been doing a bunch of design work on the

DDE side to support this today.

I'd like to be able to send a cmd

to Dexter to stop such a job.

Something like:

Dexter.write_to_robot("stop_job my_job")

To implement, I guess you have to

save away the process id of the node

process associated to the job name.

Then if you receive

"stop_job my_job"

do

kill -9 my_job_process_id

because, as we all know,

a -8 kill sometimes just isn't strong enough :-)

On Thu, Mar 28, 2019 at 8:52 PM JamesNewton notifications@github.com wrote:

Ok, a few changes:

1.

There is a sub folder under job called run, and that run folder is what is watched and triggers the service, not the job folder. That allows things in job to be edited / changed without re-triggering the service. 2.

There is a sub folder under job called logs where log files end up. 3.

The systemd files were changed as follows:

ddejob.path

[Unit] Description=DDEJob [Path] PathModified=/srv/samba/share/job/run [Install] WantedBy=multi-user.target

ddejob.service

[Unit] Description=DDEJob [Service] ExecStart=/srv/samba/share/job/RunJobs

and then started with

sudo systemctl enable ddejob.path sudo systemctl start ddejob.path

Note: if you edit those, do a

sudo systemctl daemon-reload

The RunJob script is:

!/usr/bin/env bash

first remove prior job logs and junk

cd /root/Documents/dde for i in /srv/samba/share/job/run/*.dde; do [ -f "$i" ] || break jobname=$(basename -- "$i") echo "running $jobname" echo "Logfile for $jobname on $(date +%Y%m%d_%H%M%S)">/srv/samba/share/job/logs/$jobname.tmp sudo node core define_and_start_job $i >> /srv/samba/share/job/logs/$jobname.tmp

must use sudo or node doesnt know who the user is and cant find the dde_apps folder.

    rm /srv/samba/share/job/logs/*.log
    mv /srv/samba/share/job/logs/$jobname.tmp /srv/samba/share/job/logs/$jobname.log
    # copy large log files only after they are fully written so dde doesnt seem them until complete
    rm $i
    # remove the job file

done

so now the DDE job:

new Job({name: "test_job_engine", do_list: [ Dexter.write_to_robot( "new Job({name: \"my_job\", do_list: [Dexter.move_all_joints([30, 45, 60, 90, 120]), Dexter.move_all_joints([0, 0, 0, 0, 0])]})" , "job/run/try.dde" ) ]})


has the same result, but we can add code to `read_from_robot("my_job_log", "job/logs/try.dde.log")` and when that returns nothing, just loop until it returns the result.

Seems to work quite nicely.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<https://github.com/HaddingtonDynamics/Dexter/issues/60#issuecomment-477823752>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABITfRgFjP8nrmmgTRCzrphJZrQdpKYYks5vbWO1gaJpZM4cPDa7>
.
JamesNewton commented 5 years ago

This has been included on the prior image and on the upcoming one.

The ability to run BASH shells is a much better / faster way to trigger job engine code if needed, as enabled by #20 (be sure to keep polling via "r 1 `" until the job engine completes")

Also, if DDE gains the ability to SSH into Dexter, that may provide an even better interface.

In any case, it's there, it works, it's based on DDE 3.0.7 but that will be updated in the future.

JamesNewton commented 4 years ago

Updating DDE on Dexter for Interactive Jobs from Browser

Changes made in DDE 3.5.6 cause this to not work on Dexter because of a package for camera control that has problems compiling. DDE 3.5.2 works but you must check it out and do an npm run rebuild

For 3.5.2 I connected to Dexter via SSH and did:

cd /root/Documents
mv ./dde ./dde_OLD
git clone https://github.com/cfry/dde
cd dde
git checkout v3.5.2
npm install

Browser Job Engine Interface

To enable full job control from the browser of Job Engine jobs, the following is also needed:

With these changes, you should be able to access /jobs.html at Dexters IP address and start, stop, and communicate with jobs running on the robot. It's probably a good idea to edit the index.html file in the share/www folder to add a link to that page. e.g.: <li><a href="/jobs.html">Job Engine</a> Run DDE jobs locally on Dexter</li>

with this new setup, code like the following, file name dexter_user_interface.dde in the share/dde_apps folder runs nicely and allows bidirectional communications and control from the browser.

//////// Job Example 7g: Dexter User Interface
//Interactivly control Dexter's joints.
function dexter_user_interface_cb(vals){
    debugger;
    let maj_array = [vals.j1_range, vals.j2_range, vals.j3_range, vals.j4_range,
                     vals.j5_range, vals.j6_range, vals.j7_range]
    let instr = Dexter.move_all_joints(maj_array)
    Job.insert_instruction(instr, {job: vals.job_name, offset: "end"})
}
function init_dui(){
  show_window({title: "Dexter User Interface",
               width: 300,
               height: 220,
               y: 20,
               job_name: this.name, //important to sync the correct job.
               callback: dexter_user_interface_cb,
               content:`
Use the below controls to move Dexter.<br/>
J1: <input type="range"  name="j1_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
J2: <input type="range"  name="j2_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
J3: <input type="range"  name="j3_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
J4: <input type="range"  name="j4_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
J5: <input type="range"  name="j5_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
J6: <input type="range"  name="j6_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
J7: <input type="range"  name="j7_range"  value="0"  min="-100" max="100" data-oninput="true"/><br/>
`
})}

new Job({
    name: "dexter_user_interface",
    when_stopped: "wait",
    do_list: [init_dui
]})
JamesNewton commented 4 years ago

Serial Port

We generally recommend connecting your serial devices to the PC controlling Dexter rather than direct to Dexter because support for serial ports has been difficult with the dde "job engine" on the robot. DDE manages serial devices on the PC just fine, but for some strange reason, we can't seem to rebuild the serial module for the version of node in use. When we try to require("serialport") we get the following error messages:

/root/Documents/dde/node_modules/bindings/bindings.js:91
        throw e
        ^

Error: The module '/root/Documents/dde/node_modules/@serialport/bindings/build/Release/bindings.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 57. This version of Node.js requires
NODE_MODULE_VERSION 67. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
    at Object.Module._extensions..node (internal/modules/cjs/loader.js:750:18)
    at Module.load (internal/modules/cjs/loader.js:620:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:560:12)
    at Function.Module._load (internal/modules/cjs/loader.js:552:3)
    at Module.require (internal/modules/cjs/loader.js:657:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at bindings (/root/Documents/dde307/node_modules/bindings/bindings.js:84:48)
    at Object.<anonymous> (/root/Documents/dde307/node_modules/@serialport/bindings/lib/linux.js:1:98)
    at Module._compile (internal/modules/cjs/loader.js:721:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:732:10)

npm rebuild does not help.

npm install serialport does not help.

However, if we start in a fresh folder, and npm install serialport we can write little serial programs that work just great, in that folder. Something in the dde file setup is causing the serialport module to stay stuck in the wrong version.

A complete HACK to solve this is the following:

cd /root/Document
mkdir temp
cd temp
npm install serialport
cd /root/Document/dde/node_modules
mv @serialport @save
mv serialport save
cp -r /root/Documents/temp/node_modules/@serialport .
cp -r /root/Documents/temp/node_modules/serialport .

having done that, serial ports now work in the Job Engine folder (/root/Documens/dde). Here is a sample node.js program that works with some simple Arduino code which just echos back text:

const SerialPort = require('serialport');
const Readline = require('@serialport/parser-readline')

const port_path = "/dev/ttyUSB0"
let cmd = "41"
let port = {}

function port_listener(line) {
        console.log(">"+line)
        port.close() //done, close so code ends
        }

function port_IO(path) {
        console.log("trying port:" + path)
        port = new SerialPort(path, {
                baudRate: 9600
                })
        //Make a line parser. Several other types exist. see:
        //https://serialport.io/docs/api-parsers-overview
        const parser = new Readline()
        port.pipe(parser)
        parser.on('data', port_listener)

        port.on('open', function() {
                port.flush() //dump prior data
                port.write(cmd)
                console.log('<'+cmd)
                })

        port.on('error', function(err) {
                console.log('Error: ', err.message)
                })
        }

SerialPort.list().then(ports => {
  console.log("Found serial devices:");
  ports.forEach(function(port) {
    console.log("---"); //separator
    console.log("path:"+port.path);
    console.log("pnpId:"+port.pnpId);
    console.log("mfgr:"+port.manufacturer);
    if (port.path == port_path) {//found our port
        port_IO(port_path)
        }
  });
  if (!port_path && ports.length == 1) {
        //no path set, 1 found: guess!
        port_IO(ports[0].path)
        }

});
console.log("Start Serial")

To work with Serial devices in a .DDE Job Engine job, we must use the low level serial support in DDE (Serial Robots are not yet fully supported). The following code works well with an OpenMV camera which has been programmed to return data about barcodes, tags, and anything orange.

/* To find the port on Dexter, start with device disconnected, ssh in and enter:
ls /dev/tty*
then connect the device and repeat the command. Look for the difference.
*/
var Open_MV1_path = "/dev/ttyACM1" //"COM5" //Change to device's port name
var Open_MV_options = {
    baudRate: 115200,
    DATABITS: 8,
    STOPBITS: 1,
    PARITY: 0
}

var Open_MV0_path = "/dev/ttyACM0" //in my testing, this is actually an Arduino, 'cause I only have one camera
/*var Open_MV0_options = {
    baudRate: 115200,
    DATABITS: 8,
    STOPBITS: 1,
    PARITY: 0
}*/

new Job({
    name: "OpenMV_Test",
    show_instructions: false,
    inter_do_item_dur: 0,
    when_stopped: function(){serial_disconnect(Open_MV0_path); serial_disconnect(Open_MV1_path);},
    do_list: [
        init,
          main
    ]
  })

function init(){
  let CMD = []
  CMD.push(connectSerial(Open_MV0_path, Open_MV_options))
  CMD.push(connectSerial(Open_MV1_path, Open_MV_options))
  //CMD.push(function(){clear_output()})
  return CMD
}

var clear_flag = true
function main(){
    return Robot.loop(true, function(){
    let obj = get_openmv_obj()
    if(is_orange_blob(obj)){
        if(clear_flag){ 
            //beep({dur: 0.1, frequency: 800, volume: 1})
            clear_flag = false
        //out("Orange Blob: {x: " + obj.x + ", y: " + obj.y + "}")
        console.log("*** Orange Blob: {x: " + obj.x + ", y: " + obj.y + "}")
        set_serial_string(Open_MV0_path, "13L\n")

        }
      }
    else if(is_april_tag(obj)){
        if(clear_flag){
            //beep({dur: 0.1, frequency: 800, volume: 1})
            clear_flag = false
        //out("April Tag: {id: " + obj.id + ", angle: " + Math.round(obj.z_rotation*360/(2*Math.PI)) + "}")
        console.log("*** April Tag: {id: " + obj.id + ", angle: " + Math.round(obj.z_rotation*360/(2*Math.PI)) + "}")
        }
      }
    else if(is_bar_code(obj)){
        if(clear_flag){
            clear_flag = false
        //out("Bar Code: {value: " + obj.payload + "}")
        console.log("*** Bar Code: {value: " + obj.payload + "}")
        set_serial_string(Open_MV0_path, "13H\n")
        }
      }
    else if(is_cam_no(obj)){
        if(clear_flag){
            clear_flag = false
        console.log("*** Camera {value: " + obj.camno + "}")
            //beep({dur: 0.1, frequency: 800, volume: 1})
        //speak({speak_data: "a"}) //??? Stops the serial data coming in (!?)
        }
      }
    else if(is_image(obj)){ //TODO: Returned data isn't a valid object, need "" around hex
        if(clear_flag){
            clear_flag = false
        console.log("*** image {value: " + obj.image_length + "}")
        }
      }
    else{
        if(clear_flag==false){ 
          set_serial_string(Open_MV0_path, "?\n")
          set_serial_string(Open_MV1_path, "?\n")
        }
        clear_flag = true
      }
    })
  }

 /*
    april tag:
    [{"x":50, "y":19, "w":40, "h":40, "id":25, "family":16, "cx":70, "cy":39, "rotation":3.083047, "decision_margin":0.139966, "hamming":0, "goodness":0.000000, "x_translation":-0.540304, "y_translation":1.089546, "z_translation":-6.021238, "x_rotation":3.003880, "y_rotation":6.243485, "z_rotation":3.083047}]
    my_obj.id

    bar code:
    [{"x":259, "y":42, "w":1, "h":1, "payload":"000123ABCXYZ", "type":15, "rotation":0.000000, "quality":1}]
    my_obj.payload

    blob:
    [{"x":54, "y":69, "w":8, "h":16, "pixels":117, "cx":58, "cy":77, "rotation":1.522549, "code":1, "count":1, "perimeter":42, "roundness":0.283638}]
    my_obj.roundness
*/

//********** Serial Code Start **********
serial_port_init() //still required in 3.5.2, not in 3.5.10 or later
//Eval the code below to find serial device com port:
//serial_devices() //in later versions, this is available on DDE PC only
//serial_devices_async() //on Job Engine in later versions (DDE in Dexter)

var serial_delimiter = "\n"

function ourReceiveCallback(info_from_board, path) {
    debugger;
    if(info_from_board) {
        //let str = convertArrayBufferToString(info_from_board.buffer) NO!
        let s = serial_path_to_info_map[path]
        s.buffer += info_from_board.toString() //just accumulate all incoming data
        let split_str = s.buffer.split(s.item_delimiter) //break it up by the delimiter 
        if (split_str.length > 2){ //if we have a complete string between 2 delimiters
            let str = split_str[split_str.length - 2] //break out the last one (we could break out ALL)
            s.buffer = s.item_delimiter+ split_str[split_str.length - 1] //save the rest 
          // notice that we much put the delimiter back because it was removed by the split
          //out(str, "blue", true) //debugging only

            if (str.length > 0){ //protect against empties (unnecessary?)
                s.current = str //save out latest data
                console.log(s.current+"\r")
                 }
            }
        }
     }

function ourReceiveErrorCallback(info_from_board, path) {
  if(info_from_board) {
    out("Serial Error on " + path + ":")
    out(JSON.stringify(info_from_board))
    debugger; //do not remove
  }
}

function get_serial_string(){ 
  return serial_path_to_info_map[Open_MV1_path].current
}

function set_serial_string(path, str){
    serial_path_to_info_map[path].port.write(str + "\n")
}

function connectSerial(serial_path, serial_options){
    return [function (){
        serial_connect_low_level(
        serial_path,      //com number string
        serial_options,  //options (baud, etc...)
        1,                //capture_n_items (unused)
        serial_delimiter, //item_delimiter (unused)
        true,             //trim_whitespace (unused)
        true,             //parse_items (unused)
        false,            //capture_extras (unused)
        ourReceiveCallback,
        ourReceiveErrorCallback
        )
    serial_path_to_info_map[serial_path].buffer = ""
    serial_path_to_info_map[serial_path].current = ""
    }]
}

function serial_disconnect(serial_path) {
    let info = serial_path_to_info_map[serial_path]
    if (info){
        if((info.simulate === false) || (info.simulate === "both")) {
            info.port.close(out)
        }
        delete serial_path_to_info_map[serial_path]
    }
}

function get_openmv_obj(){
    let my_string = get_serial_string()
    //out(my_string, "blue", true)
    if(!my_string){return undefined}
    let my_obj = {}
    try {my_obj = JSON.parse(my_string)[0]}
    catch(error){
    out("Bad data")}
    return my_obj
}

function is_april_tag(obj){
    return obj && obj.family
}

function is_orange_blob(obj){
    return obj && obj.roundness
}

function is_bar_code(obj){
    return obj && obj.payload
}

function is_cam_no(obj){
    return obj && obj.camno
}

function is_image(obj){
    return obj && obj.image_length
}
//********** Serial Code End **********
JamesNewton commented 4 years ago

WebSocket Job Engine Interface (incl non-browser)

To use the websocket interface outside of the browser environment, potentially from any websocket capable environment and not only the browser, via the user_data variables. This requires a few lines being changed in the /srv/samba/share/www/httpd.js file. Specifically

This allows us to send any "kind" of message (or one with no kind) to the job engine. And if we include a ws_message item in the object we send, it will be inserted into the jobs user_data.

For example:

// File: /srv/samba/share/dde_apps/dexter_message_interface.dde
new Job({
    name: "dexter_message_interface",
    when_stopped: "wait",
    inter_do_item_dur: 2,
    show_instructions: false,
    user_data: {ws_message: "hello"},
    do_list: [
        Robot.loop(true, function(){ //in future versions, use Control.loop
            if (this.user_data.ws_message) {
              out(this.user_data.ws_message)
              this.user_data.ws_message = undefined
              }
            })
        ]})

Note:

To use this, a web socket connection must be opened to Dexter on port 3001. e.g. ws://192.168.1.142:3001/

A large number of status and informational strings will be returned from the job engine to the onboard node server. To pick out the ones that were sent back from your job via "out", look for data wrapped in "" tags, with a JSON object and a "kind" of "out_call". The data will be in the "val" attribute. e.g. <for_server>{"kind":"out_call","val":"APP:hello.","color":"black","temp":false,"code":null}</for_server>

The val may be escaped JSON or binary data. For example, this is a returning ROS JointState message where the "val" is, itself, a JSON message: <for_server>{"kind":"out_call","val":"{\"header\":{\"seq\":0,\"stamp\":{\"secs\":1597802744.166809,\"nsecs\":1597802744166809000},\"frame_id\":\"\"},\"name\":[\"J1\",\"J2\",\"J3\",\"J4\",\"J5\",\"J6\",\"J7\"],\"position\":[0,0,0,0,0,-2.5914648733611805,0],\"velocity\":[0,0,0,0,0,0,0],\"effort\":[0,0,0,0,0,0,0]}","color":"black","temp":false,"code":null}</for_server>

Here are some examples of other messages you might get `{"kind":"out_call","val":"(out call) stdin got line: Job.maybe_define_and_server_job_button_click(\"/srv/samba/share/dde_apps/ROS.dde\")\n","color":"black","temp":false,"code":null}

{"kind":"show_job_button","job_name":"helloworld","status_code":"interrupted","button_color":"rgb(255, 123, 0)","button_tooltip":"This Job was interrupted at instruction 3 by:\nUser stopped job\nClick to restart this Job."} {"kind":"out_call","val":"Done with job: helloworld for reason: User stopped job","color":"black","temp":false,"code":null}

In finish_job for job: helloworld id: 1 finish job calling close_readline`

To send data from the program that opened the connection into the job engine job, send a JSON formatted string through the web socket connection, with the name of the job file, and a "ws_message" string. For example: '{"job_name_with_extension": "dexter_message_interface.dde", "ws_message": "goodbye" }' The job should echo that back to you.

In some environments, the web socket connection will time out and be closed automatically. To avoid that, you can send a JSON string with a "kind" and job name of "keep_alive". Like this: {kind: "keep_alive_click", job_name_with_extension: "keep_alive", keep_alive_value: is_checked} This will be processed and won't actually do anything.

For example, using the Python 2.7 that comes with Ubuntu 16.04, I was able to install: https://github.com/websocket-client/websocket-client to add websocket support to Python and then the follow program works to start, and send and receive data from the dexter_message_interface.dde program.

import websocket
try:
    import thread
except ImportError:
    import _thread as thread
import time

def on_message(ws, message):
    print(message)

def on_error(ws, error):
    print(error)

def on_close(ws):
    print("### closed ###")

def on_open(ws):
    def run(*args):
        for i in range(3):
            time.sleep(1)
            ws.send("{\"job_name_with_extension\": \"dexter_message_interface.dde\", \"ws_message\": \"message %d\" }" % i)
            time.sleep(3)
        ws.close()
        print("thread terminating...")
    thread.start_new_thread(run, ())

if __name__ == "__main__":
    #websocket.enableTrace(True)
    ws = websocket.WebSocketApp("ws://192.168.1.142:3001",
                              on_message = on_message,
                              on_error = on_error,
                              on_close = on_close)
    ws.on_open = on_open
    ws.run_forever()
JamesNewton commented 3 years ago

Kamino cloned this issue to HaddingtonDynamics/OCADO