-
Notifications
You must be signed in to change notification settings - Fork 85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Job engine to run DDE jobs on Dexter #60
Comments
Ok, a few changes:
ddejob.service
and then started with
Note: if you edit those, do a
The RunJob script is:
so now the DDE job:
has the same result, but we can add code to Seems to work quite nicely. |
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 ***@***.***> 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
<#60 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABITfRgFjP8nrmmgTRCzrphJZrQdpKYYks5vbWO1gaJpZM4cPDa7>
.
|
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. |
Updating DDE on Dexter for Interactive Jobs from BrowserChanges 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 For 3.5.2 I connected to Dexter via SSH and did:
Browser Job Engine InterfaceTo 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.: with this new setup, code like the following, file name
|
This file is a more complex interface to the "Job Engine" on Dexter from a users browser via the onboard web server. Requires updated https.js with web socket interface on 3001 to work.
Serial PortWe 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
However, if we start in a fresh folder, and A complete HACK to solve this is the following:
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 ********** |
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. 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 "<for_server>" tags, with a JSON object and a "kind" of "out_call". The data will be in the "val" attribute. e.g. 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: Here are some examples of other messages you might get 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: 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: For example, using the Python 2.7 that comes with Ubuntu 16.04, I was able to install: 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() |
Kamino cloned this issue to HaddingtonDynamics/OCADO |
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)
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, thenode 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 tolocalhost
so it will work no matter what IP address Dexter is actually assigned./root/Documents/dde_apps/dde_init.js
: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.The system software takes advantage of this. One important case is that the "out" function is defined as:
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):
which then fires off the job engine, does the job, and puts the resulting output in a YYMMDD_HHMMSS.log file.
For example:
makes Dexter move, and then the file /job/try.dde.20190327_233018.log contains:
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.
<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.
The text was updated successfully, but these errors were encountered: