-
Notifications
You must be signed in to change notification settings - Fork 0
/
job.js
1819 lines (1720 loc) · 90.2 KB
/
job.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**Created by Fry on 2/5/16. */
var esprima = require('esprima')
var Job = class Job{
constructor({name=null, robot=Robot.dexter0, do_list=[], keep_history=true, show_instructions=true,
inter_do_item_dur = 0.01, user_data={}, program_counter=0, ending_program_counter="end",
initial_instruction = null, when_stopped = "stop"} = {}){
//program_cpunter is the counter of the next instruction that should be executed.
//so since we're currently "executing" 1 instruction, and after its done,
//we'll be incrementing the pc, then internally we decriment the
//passed in program_counter. If its negative, it means
//computer it from the end, ie -1 means when set_next_do is called,
//it will set the pc to the length of the do_list, hence we'll be done
//with the job. -2 means we want to execute the last instruction of the
//job next, etc.
//save the args
if (Job[name] && Job[name].is_active()) { //we're redefining the job so we want to make sure the
//previous version is stopped.
if (Job[name].robot instanceof Dexter) {Job[name].robot.empty_instruction_queue_now() }
Job[name].stop_for_reason("interrupted", "User is redefining this job.")
let orig_args = arguments[0]
setTimeout(function(){ new Job (orig_args) }, this.inter_do_item_dur * 3)
}
else {
if (!Job.is_plausible_when_stopped_value(when_stopped)) {
dde_error("new Job passed: " + when_stopped + " but that isn't a valid value.")
}
this.orig_args = {do_list: do_list, keep_history: keep_history, show_instructions: show_instructions,
inter_do_item_dur: inter_do_item_dur, user_data: user_data,
program_counter: program_counter, ending_program_counter: ending_program_counter,
initial_instruction: initial_instruction, when_stopped: when_stopped}
this.name = name
this.robot = robot
//setup name
Job.job_id_base += 1
this.job_id = Job.job_id_base
if (this.name == null){ this.name = "job_" + this.job_id }
if (!(robot instanceof Robot)){
if (!Robot.dexter0){
dde_error("Attempt to created Job: " + this.name + " with no valid robot instance.<br/>" +
" Note that Robot.dexter0 is not defined<br/> " +
" but should be in your file: Documents/dde_apps/dde_init.js <br/>" +
" after setting the default ip_address and port.<br/> " +
"To generate the default dde_init.js file,<br/>" +
" rename your existing one and relaunch DDE.")
}
else {
dde_error("Attempt to created Job: " + this.name + " with no valid robot instance.<br/>" +
"You can let the robot param to new Job default to get a correct Robot.dexter.0")
}
}
this.program_counter = program_counter //this is set in start BUT, if we have an unstarted job, and
//instruction_location_to_id needs access to program_counter, this needs to be set
this.highest_completed_instruction_id = -1 //same comment as for program_counter above.
this.sent_from_job_instruction_queue = [] //will be re-inited by start, but needed here
//just in case some instructions are to be inserted before this job starts.
Job[this.name] = this //beware: if we create this job twice, the 2nd version will be bound to the name, not the first.
Job.remember_job_name(this.name)
this.status_code = "not_started" //see Job.status_codes for the legal values
this.add_job_button_maybe()
this.color_job_button()
}
} //end constructor
show_progress_maybe(){
if(this.show_instructions === true) { this.show_progress() }
else if(typeof(this.show_instructions) === "function") {
this.show_instructions.call(this)
}
//else do nothing
}
show_progress(){
var html_id = this.name + this.start_time.getTime()
var cur_instr = this.current_instruction()
if (this.program_counter >= this.do_list.length) { cur_instr = "Done." }
else { cur_instr = "Last instruction sent: " + Instruction.to_string(cur_instr) }
var content = "Job: " + this.name + " pc: " + this.program_counter +
" <progress style='width:100px;' value='" + this.program_counter +
"' max='" + this.do_list.length + "'></progress>" +
" of " + this.do_list.length + ". " +
cur_instr +
" <button onclick='inspect_out(Job." + this.name + ")'>Inspect</button>"
out(content, "#5808ff", html_id)
}
show_progress_and_user_data(){
var html_id = this.name + this.start_time.getTime()
var cur_instr = this.current_instruction()
if (this.program_counter >= this.do_list.length) { cur_instr = "Done." }
else { cur_instr = "Last instruction sent: " + Instruction.to_string(cur_instr) }
var content = "Job: " + this.name + " pc: " + this.program_counter +
" <progress style='width:100px;' value='" + this.program_counter +
"' max='" + this.do_list.length + "'></progress>" +
" of " + this.do_list.length + ". " +
cur_instr +
" <button onclick='inspect_out(Job." + this.name + ")'>Inspect</button>" +
"<br/>"
let has_user_data = false
for(let prop_name in this.user_data){
if(!has_user_data) { //first iteration only
content += "<b>user_data: </b> "
has_user_data = true
}
content += "<i>" + prop_name + "</i>: " + this.user_data[prop_name] + " "
}
if(!has_user_data) { content += "<i>No user data in this job.</i>" }
out(content, "#5808ff", html_id)
}
//Called by user to start the job and "reinitialize" a stopped job
start(options={}){ //sent_from_job = null
if(this.wait_until_this_prop_is_false) { this.wait_until_this_prop_is_false = false } //just in case previous running errored before it could set this to false, used by start_objects
if (["starting", "running", "suspended"].includes(this.status_code)){
dde_error("Attempt to restart job: " + this.name +
" but it has status code: " + this.status_code +
" which doesn't permit restarting.")
}
else{//init from orig_args
this.do_list = Job.flatten_do_list_array(this.orig_args.do_list) //make a copy in case the user passes in an array that they will use elsewhere, which we wouldn't want to mung
this.keep_history = this.orig_args.keep_history
this.show_instructions = this.orig_args.show_instructions
this.inter_do_item_dur = this.orig_args.inter_do_item_dur
this.user_data = // don't do as gets non-enumerables, etc. messes up printing of what should be empty user_data of a job jQuery.extend(true, {}, this.orig_args.user_data) //performs deep copy. This is hard, so use jquery
shallow_copy(this.orig_args.user_data)
this.program_counter = this.orig_args.program_counter //see robot_done_with_instruction as to why this isn't 0,
//its because the robot.start effectively calls set_up_next_do(1), incremening the PC
this.ending_program_counter = this.orig_args.ending_program_counter
this.initial_instruction = this.orig_args.initial_instruction
this.when_stopped = this.orig_args.when_stopped
//first we set all the orig (above), then we over-ride them with the passed in ones
for (let key in options){
if (options.hasOwnProperty(key)){
let new_val = options[key]
//if (key == "program_counter") { new_val = new_val - 1 } //don't do. You set the pc to the pos just before the first instr to execute.
if (key == "do_list") { new_val = Job.flatten_do_list_array(new_val) }
else if (key == "user_data") { new_val = shallow_copy(new_val) }
else if (key == "name") {} //don't allow renaming of the job
else if ((key == "when_stopped") &&
!Job.is_plausible_when_stopped_value(new_val)) {
dde_error("Job.start called with an invalid value for 'when_stopped' of: " +
new_val)
}
this[key] = new_val
}
}
let maybe_symbolic_pc = this.program_counter
this.program_counter = 0 //just temporarily so that instruction_location_to_id can start from 0
const job_in_pc = Job.instruction_location_to_job(maybe_symbolic_pc, false)
if ((job_in_pc != null) && (job_in_pc != this)) {
dde_error("Job." + this.name + " has a program_counter initialization<br/>" +
"of an instruction_location that contains a job that is not the job being started. It shouldn't.")
}
this.program_counter = this.instruction_location_to_id(maybe_symbolic_pc)
//this.robot_status = [] use this.robot.robot_status instead //only filled in when we have a Dexter robot by Dexter.robot_done_with_instruction or a Serial robot
this.rs_history = [] //only filled in when we have a Dexter robot by Dexter.robot_done_with_instruction or a Serial robot
this.sent_instructions = []
this.start_time = new Date()
this.stop_time = null
this.status_code = "starting" //before setting it here, it should be "not_started"
this.stop_reason = null
this.wait_reason = null //not used when waiting for instruction, but used when status_code is "waiting"
this.wait_until_instruction_id_has_run = null
this.highest_completed_instruction_id = -1
//this.iterator_stack = []
if (this.sent_from_job_instruction_queue.length > 0) { //if this job hasn't been started when another job
// runs a sent_to_job instruction to insert into this job, then that
//instruction is stuck in this job's sent_from_job_instruction_queue,
//so that it can be inserted into this job when it starts.
//(but NOT into its original_do_list, so its only inserted the first time this
//job is run.
Job.insert_instruction(this.sent_from_job_instruction_queue, this.sent_from_job_instruction_location)
//this.do_list.splice(this.program_counter, 0, ...this.sent_from_job_instruction_queue)
}
this.sent_from_job_instruction_queue = [] //send_to_job uses this. its on the "to_job" instance and only stores instructions when send_to_job has
//where_to_insert="next_top_level", or when this job has yet to be starter. (see above comment.)
this.sent_from_job_instruction_location = null
if (this.initial_instruction) { //do AFTER the sent_from_job_instruction_queue insertion.
//Instruction.Control.send_to_job.insert_sent_from_job(this, sent_from_job)
Job.insert_instruction(this.initial_instruction, {job: this, offset: "program_counter"})
}
//must be after insert queue and init_instr processing
if ((this.program_counter == 0) &&
(this.do_list.length == 0) &&
((this.when_stopped == "wait") || (typeof(this.when_stopped) == "function"))) {} //special case to allow an empty do_list if we are waiting for an instruction or have a callback.
else if (this.program_counter >= this.do_list.length){ //note that maybe_symbolic_pc can be "end" which is length of do_list which is valid, though no instructions would be executed in that case so we error.
dde_error("While starting job: " + this.name +
"<br/>the programer_counter is initialized to: " + this.program_counter +
"<br/>but the highest instruction ID in the do_list is: " + (this.do_list.length - 1))
}
Job.last_job = this
this.added_items_count = new Array(this.program_counter) //This array parallels and should be the same length as the run items on the do_list.
this.added_items_count.fill(0) //stores the number of items "added" by each do_list item beneath it
//if the initial pc is > 0, we need to have a place holder for all the instructions before it
this.go_state = true
//this.init_show_instructions()
//out("Starting job: " + this.name + " ...")
this.show_progress_maybe()
this.color_job_button()
this.robot.start(this) //the only call to robot.start
}
}
//show_instruction in editor
static start_job_menu_item_action () {
var full_src = Editor.get_javascript()
var start_cursor_pos = Editor.selection_start()
var end_cursor_pos = Editor.selection_end()
var text_just_after_cursor = full_src.substring(start_cursor_pos, start_cursor_pos + 7)
var start_of_job = -1
if (text_just_after_cursor == "new Job") { start_of_job = start_cursor_pos }
else { start_of_job = Editor.find_backwards(full_src, start_cursor_pos, "new Job") }
if (start_of_job == null) {
warning("There's no Job definition surrounding the cursor.")
var selection = Editor.get_javascript(true).trim()
//if (selection.endsWith(",")) { selection = selection.substring(0, selection.length - 1) } //ok to have trailing commas in array new JS
if (selection.length > 0){
if (selection.startsWith("[") && selection.endsWith("]")) {}
else {
selection = "[" + selection + "]"
start_cursor_pos = start_cursor_pos - 1
}
var eval2_result = eval_js_part2(selection)
if (eval2_result.error_type) {} //got an error but error message should be displayed in output pane automatmically
else if (Array.isArray(eval2_result.value)){ //got a do_list!
if (Job.j0 && Job.j0.is_active()) {
Job.j0.stop_for_reason("interrupted", "Start Job menu action stopped job.")
setTimeout(function() {
Job.init_show_instructions_for_insts_only_and_start(start_cursor_pos, end_cursor_pos, eval2_result.value, selection)}
(Job.j0.inter_do_item_dur * 1000 * 2) + 10) //convert from seconds to milliseconds
}
else {
Job.init_show_instructions_for_insts_only_and_start(start_cursor_pos, end_cursor_pos, eval2_result.value, selection)
}
}
else {
shouldnt("Selection for Start job menu item action wasn't an array, even after wrapping [].")
}
}
else {
warning("When choosing the Start menu item with no surrounding Job definition<br/>" +
"you must select exactly those instructions you want to run.")
}
}
else {
Editor.select_javascript(start_of_job)
if (Editor.select_call()){ //returns true if it manages to select the call.
//eval_button_action()
var job_src = Editor.get_javascript(true)
const eval2_result = eval_js_part2(job_src)
if (eval2_result.error_type) { } //got an error but error message should be displayed in Output pane automatically
else {
const job_instance = eval2_result.value
const [pc, ending_pc] = job_instance.init_show_instructions(start_cursor_pos, end_cursor_pos, start_of_job, job_src)
job_instance.start({show_instructions: true, program_counter: pc, ending_program_counter: ending_pc})
}
}
else { warning("Ill-formed Job definition surrounding the cursor.") }
}
}
static init_show_instructions_for_insts_only_and_start(start_cursor_pos, end_cursor_pos, do_list_array, selection){
const job_instance = new Job({name: "j0", do_list: do_list_array})
const begin_job_src = 'new Job ({name: "j0", do_list: '
const job_src = begin_job_src + selection + "})"
const start_of_job = start_cursor_pos - begin_job_src.length//beware, could be < 0
job_instance.init_show_instructions(start_cursor_pos, end_cursor_pos, start_of_job, job_src)
job_instance.start({show_instructions: true})
}
init_show_instructions(start_cursor_pos, end_cursor_pos, start_of_job, job_src){
this.job_source_start_pos = start_of_job //necessary offset to range positions that are in the syntax tree
const syntax_tree = esprima.parse(job_src, {range: true})
const job_props_syntax_array = syntax_tree.body[0].expression.arguments[0].properties
for (var prop_syntax of job_props_syntax_array){
if (prop_syntax.key.name == "do_list"){
this.do_list_syntax_array = prop_syntax.value.elements
return this.instruction_ids_at_selection(start_cursor_pos, end_cursor_pos, start_of_job, syntax_tree)
}
}
dde_error("Job." + this.name + " apparently has no do_list property.")
}
//returns pc to set for starting job that cursor is in, or 0, start at begining,
instruction_ids_at_selection(start_cursor_pos, end_cursor_pos, start_of_job, syntax_tree) {
var start_cursor_pos_in_job_src = start_cursor_pos - start_of_job
var end_cursor_pos_in_job_src = end_cursor_pos - start_of_job
var result_start = null
var result_end = "end"
for(let i = 0; i < this.do_list_syntax_array.length; i++) {
var do_list_item_syntax = this.do_list_syntax_array[i]
//var inst_start_pos = do_list_item_syntax.range[0]
var inst_start_pos = do_list_item_syntax.range[0]
var inst_end_pos = do_list_item_syntax.range[1]
if (result_start === null){
if (start_cursor_pos_in_job_src <= (inst_end_pos + 1)){ //comma at end still in the instr
result_start = i //first time through, cursor before do_list, just start at 0
if (start_cursor_pos == end_cursor_pos) { //no selection
result_end = "end"
break;
}
else if (end_cursor_pos_in_job_src <= (inst_end_pos + 1)){ //there's a selection, but it starts and ends in just one instruction
result_end = i + 1
break;
}
}
}
else { //looking for result_end
if (end_cursor_pos_in_job_src <= (inst_end_pos + 1)){ //comma at end still in the instr
result_end = i + 1
break;
}
}
}
return [result_start, result_end]
}
select_instruction_maybe(cur_do_item){
if(this.show_instructions && this.do_list_syntax_array){
console.log(" now processing instruction: " + stringify_value(cur_do_item))
const orig_instruction_index = this.orig_args.do_list.indexOf(cur_do_item)
if(orig_instruction_index != -1){
const range = this.instruction_text_range(orig_instruction_index)
Editor.select_javascript(range[0], range[1])
}
}
}
instruction_text_range(orig_instruction_index){
const array_elt_syntax_tree = this.do_list_syntax_array[orig_instruction_index]
return [array_elt_syntax_tree.range[0] + this.job_source_start_pos,
array_elt_syntax_tree.range[1] + this.job_source_start_pos]
}
//end show_instruction in editor
//Job BUTTONS______
get_job_button_id(){ return this.name + "_job_button_id"}
get_job_button(){
const the_id = this.get_job_button_id()
var but_elt = window[the_id]
return but_elt
}
add_job_button_maybe(){
var but_elt = this.get_job_button()
if (!but_elt){
const job_name = this.name
const the_id = this.get_job_button_id()
const the_html = '<button style="margin-left:10px; vertical-align:top;" id="' + the_id + '">'+ job_name + '</button>'
$("#jobs_button_bar_id").append(the_html)
but_elt = window[the_id]
but_elt.onclick = function(){
const the_job = Job[job_name]
if (the_job.status_code == "suspended"){
the_job.unsuspend()
}
else if(the_job.is_active()){
if (the_job.robot instanceof Dexter) { the_job.robot.empty_instruction_queue_now() }
the_job.stop_for_reason("interrupted", "User stopped job", false)
}
else {
the_job.start()
}
}
this.color_job_button()
}
}
remove_job_button(){
var but_elt = this.get_job_button()
if(but_elt){ $(but_elt).remove() }
}
set_status_code(status_code){
if (Job.status_codes.includes(status_code)){
this.status_code = status_code
this.color_job_button()
if (status_code != "waiting") { this.wait_reason = null }
}
else {
shouldnt("set_status_code passed illegal status_code of: " + status_code +
"<br/>The legal codes are:</br/>" +
Job.status_codes)
}
}
color_job_button(){
let bg_color = null
let tooltip = ""
switch(this.status_code){
case "not_started":
bg_color = "rgb(204, 204, 204)";
tooltip = "This job has not been started since it was defined.\nClick to start this job."
break; //defined but never started.
case "starting":
bg_color = "rgb(136, 255, 136)";
tooltip = "This job is in the process of starting.\nClick to stop it."
break;
case "running":
if((this.when_stopped == "wait") &&
(this.program_counter == this.instruction_location_to_id(this.ending_program_counter))) {
bg_color = "rgb(255, 255, 102)"; //pale yellow
tooltip = 'This job is waiting for a new last instruction\nbecause it has when_stopped="wait".\nClick to stop this job.'
}
else {
const cur_ins = this.do_list[this.program_counter]
if (Instruction.is_instruction_array(cur_ins)){
const oplet = cur_ins[Dexter.INSTRUCTION_TYPE]
if(oplet == "z") {
bg_color = "rgb(255, 255, 102)"; //pale yellow
tooltip = "Now running 'sleep' instruction."
break;
}
}
bg_color = "rgb(136, 255, 136)";
tooltip = "This job is running.\nClick to stop this job."
}
break;
case "suspended":
bg_color = "rgb(255, 255, 17)"; //bright yellow
tooltip = "This job is suspended.\nClick to unsuspend it.\nAfter it is running, you can click to stop it."
break; //yellow
case "waiting":
bg_color = "rgb(255, 255, 102)"; //pale yellow
tooltip = "This job is waiting for:\n" + this.wait_reason + "\nClick to stop this job."
break; //yellow
case "completed":
if((this.program_counter === this.do_list.length) &&
(this.when_stopped === "wait")){
bg_color = "rgb(255, 255, 102)"; //pale yellow
tooltip = 'This job is waiting for a new last instruction\nbecause it has when_stopped="wait".\nClick to stop this job.'
}
else {
bg_color = "rgb(230, 179, 255)" // purple. blues best:"#66ccff" "#33bbff" too dark //"#99d3ff" too light
tooltip = "This job has successfully completed.\nClick to restart it."
}
break;
case "errored":
bg_color = "rgb(255, 68, 68)";
tooltip = "This job errored with:\n" + this.stop_reason + "\nClick to restart this job."
break;
case "interrupted":
bg_color = "rgb(255, 123, 0)"; //orange
tooltip = "This job was interrupted by:\n" + this.stop_reason + "\nClick to restart this job."
break;
}
const but_elt = this.get_job_button()
if (but_elt.style.backgroundColor !== bg_color) { //cut down the "jitter" in the culor, don't set unnecessarily
but_elt.style.backgroundColor = bg_color
}
but_elt.title = tooltip
}
//end of jobs buttons
is_active(){ return ((this.status_code != "not_started") && (this.stop_reason == null)) }
//called in utils stringify_value used for original_do_list
static non_hierarchical_do_list_to_html(a_do_list){
var result = "<table><tr><th title='The instruction_id is the order of the instruction in the do_list.\nSame as the program counter at send time.'>ID</th>" +
"<th title='The instruction type and its arguments'>Instruction</th></tr>"
for(var i = 0; i < a_do_list.length; i++){
result += "<tr><td>" + i + "</td><td>" + stringify_value(a_do_list[i]) + "</td><td></tr>"
}
result += "</table>"
return "<details><summary>original do_list</summary>" + result + "</details>"
}
do_list_to_html(){
Job.do_list_to_html_set_up_onclick()
return "<details style='display:inline-block'><summary></summary>" +
this.do_list_to_html_aux(0, 1) +
"</details>"
}
static do_list_to_html_set_up_onclick(){
setTimeout(function(){
let elts = document.getElementsByClassName("do_list_item")
for (let i = 0; i < elts.length; i++) { //more clever looping using let elt of elts breaks but only on windows deployed DDE
let elt = elts[i]
elt.onclick = Job.do_list_item_present_robot_status }
}, 500)
}
//runs in UI
static do_list_item_present_robot_status(event){
event.stopPropagation();
let elt = event.target
let [job_name, instruction_id] = elt.dataset.do_list_item.split(" ")
Job.show_robot_status_history_item(job_name, parseInt(instruction_id))
}
instruction_id_to_rs_history_item(id){
for (let item of this.rs_history){
if (item[Dexter.INSTRUCTION_ID] == id) { return item }
}
if (this.keep_history){
shouldnt("Job.instruction_id_to_rs_history_item passed id: " + id + " but couldn't be found in rs_history: " + this.rs_history)
}
else { return null }
}
current_instruction(){
return this.do_list[this.program_counter]
}
is_top_level_do_item(do_item){
return this.orig_args.do_list.includes(do_item)
}
at_sync_point(sync_point_name){
let ins = this.current_instruction()
return ((ins instanceof Instruction.Control.sync_point) &&
(ins.name == sync_point_name))
}
at_or_past_sync_point(sync_point_name){ //presumes that the THIS job HAS an instuction with the named sync point
if(!this.do_list) { return false} //before this job has started so its definately not past any of its sync points.
for(let a_pc = this.program_counter; a_pc >= 0; a_pc--){
let ins = this.do_list[a_pc]
if ((ins instanceof Instruction.Control.sync_point) &&
(ins.name == sync_point_name)) { return true }
}
return false
}
static show_robot_status_history_item(job_name, instruction_id){
let job_instance = Job[job_name]
let rs_history_item = job_instance.instruction_id_to_rs_history_item(instruction_id)
if (rs_history_item) {
let table_html = Dexter.robot_status_to_html_table(rs_history_item)
show_window({content: table_html,
title: "Robot status for " + job_name + ", instruction: " + instruction_id,
width: 800,
height: 380})
}
else {
out("Robot: " + job_instance.robot.name + " in job: " + job_instance.name +
" has not kept robot_status for instruction: " + instruction_id + "." +
"<br/>Job " + job_instance.name + " keep_history is: " + job_instance.keep_history,
"red")
}
}
do_list_to_html_aux(id_to_start_from = 0, indent_level = 0, sub_item_count){
if (!sub_item_count) {
if (this.do_list) { sub_item_count = this.do_list.length}
else { sub_item_count = 0 }
}
let result = ""
let sub_sub_items_processed = 0
for(let sub_item_index = 0; sub_item_index < sub_item_count; sub_item_index++){
let id = id_to_start_from + sub_item_index + sub_sub_items_processed
if (id >= this.do_list.length) {return result}
let item = this.do_list[id]
let new_sub_item_count = this.added_items_count[id]
let class_html = "class='do_list_item' "
let rs_button = ""
if (Instruction.is_instruction_array(item)) { rs_button = " <button data-do_list_item='" + this.name + " " + id + "' + title='Show the robot status as it was immediately after this instruction was run.'" + class_html + ">RS</button> "}
let item_text = "<span title='instruction_id'>id=" + id +
"</span> <span title='Number of sub_instructions added by this instruction below it.'> si=" + new_sub_item_count + "</span>" +
rs_button +
" " + Instruction.text_for_do_list_item(item) //core of the_item
let html_indent = 'style="margin-left:' + (indent_level * 20) + 'px; background-color:' + Instruction.instruction_color(item) + ';"'
let actual_sub_items_grabbed_this_iter
if (new_sub_item_count > 0) {
item_text = "<details " + html_indent + "><summary>" + item_text + "</summary>"
let sub_items_text = this.do_list_to_html_aux(id + 1, indent_level + 1, new_sub_item_count)
item_text = item_text + sub_items_text + "</details>"
actual_sub_items_grabbed_this_iter = (sub_items_text.match(/<div|<details/g) || []).length
sub_sub_items_processed += actual_sub_items_grabbed_this_iter
}
else {
item_text = "<div " + html_indent + ">" + item_text + "</div>"
actual_sub_items_grabbed_this_iter = 0
}
result += item_text
}
return result
}
time_to_string(a_time){
if (a_time){
return a_time.getHours() + ":" + a_time.getMinutes() + ":" + a_time.getSeconds()
}
else { return "null" }
}
stringify(){
let stat_code = this.status_code
if (stat_code == "completed") { stat_code = "<span style='color:#00b300;'>completed</span>" }
else if ((stat_code === "errored") || (stat_code === "interrupted")) {
stat_code = "<span style='color:#cc0000;'>" + stat_code + "</span>"
}
let dur_string = milliseconds_to_human_string(this.stop_time - this.start_time)
let result = "Job <i>name</i>: " + this.name + ", <i>job_id</i>: " + this.job_id + ", <i>simulate</i>: " + this.robot.simulate + "<br/>" +
"<i>start_time</i>: " + this.time_to_string(this.start_time) +
", <i>stop_time</i>: " + this.time_to_string(this.stop_time) +
", <i>dur</i>: " + dur_string + "<br/>" +
"<i>program_counter</i>: " + this.program_counter + ", <i>status_code</i>: " + stat_code + ",<br/>" +
"<i>stop_reason</i>: " + this.stop_reason + ", <i>wait_reason</i>: " + this.wait_reason + "<br/>" +
"<i>wait_until_instruction_id_has_run</i>: " + this.wait_until_instruction_id_has_run + "<br/>" +
"<i>highest_completed_instruction_id</i>: " + this.highest_completed_instruction_id + "<br/>" +
"<i>user_data</i>: " + stringify_value(this.user_data) + ",<br/>" +
Job.non_hierarchical_do_list_to_html(this.orig_args.do_list) +
this.do_list_to_html() +
Dexter.sent_instructions_to_html(this.sent_instructions) +
Dexter.make_show_rs_history_button_html(this.job_id) +
"<fieldset style='background-color:#EEEEEE;'><legend>Robot</legend>" + this.robot.stringify() + "</fieldset>"
return result
}
//takes nested items in array and makes flattened list where the elts are
//a dexter instrution array, a fn, or something else that can be a do_item.
//could possibly return [] which will be ignored by do_next_item
static flatten_do_list_array(arr, result=[]){
for(let elt of arr){
if (Instruction.is_instruction_array(elt)) { result.push(elt) }
else if (Array.isArray(elt)) { //if elt is empty array, tbis works fine too.
Job.flatten_do_list_array(elt, result)
}
else if (elt == null) {} //includes undefined
else if (Instruction.is_control_instruction(elt)) { result.push(elt) }
else if (typeof(elt) === "function") { result.push(elt) }
else if (is_iterator(elt)) { result.push(elt) }
else if ((typeof(elt) === "object") && (typeof(elt.start) == "function")) { result.push(elt) }
else if (elt === "debugger") { result.push(elt) }
else {
throw(TypeError("Job.flatten_do_list_array got illegal item on do list: " + elt))
}
}
return result
}
//can't be an instruction, must be called from a method
//unsuspend is like start, ie it calls start_after_connected which calls send get status
// which calls robot_done_with_instruction which calls set_up_next_do(1)
unsuspend(){
if (this.status_code == "suspended"){
this.set_status_code("running")
this.set_up_next_do(1)
}
}
record_sent_instruction_stop_time(ins_id, stop_time){
if (this.keep_history){
for(let ins of this.sent_instructions){
if (ins[Instruction.INSTRUCTION_ID] === ins_id){
ins[Instruction.STOP_TIME] = stop_time
return
}
}
shouldnt("a_job.record_sent_instruction_stop_time passed ins_id: " + ins_id +
" but couldn't find an instruction with that id in Job." + this.name + ".sent_instructions")
}
}
xyz_for_rs_history(){
let result = []
let rob = this.robot
for(let rs of this.rs_history){
let angles = [rs[Dexter.J1_ANGLE], rs[Dexter.J2_ANGLE], rs[Dexter.J3_ANGLE], rs[Dexter.J4_ANGLE], rs[Dexter.J5_ANGLE]]
let a_xyz = Kin.J_angles_to_xyz(angles, rob.pose)[0]
result.push(a_xyz)
}
return result
}
}
Job.status_codes = ["not_started", "starting", "running", "completed",
"suspended", "waiting", //(wait_until, sync_point)
"errored", "interrupted" //(user stopped manually),
]
Job.global_user_data = {}
Job.job_id_base = 0 //only used for making the job_id.
Job.all_names = [] //maintained in both UI and sandbox/ used by replacement series job names
//note that once we make 1 job instance with a name, that binding of
//Job.the_name never goes away, and that name will always be in the
//the all_names list. But if you redefine a Job (with the same name)
//the old value of that name is gc'd.
Job.remember_job_name = function(job_name){
if (!Job.all_names.includes(job_name)){
Job.all_names.push(job_name)
$("#videos_id").prepend("<option>Job: " + job_name + "</option>")
}
}
Job.forget_job_name = function(job_name){
let i = Job.all_names.indexOf(job_name)
if (i != -1){
Job.all_names.splice(i, 1)
}
}
//we can't send to sandbox or UI, this has to work in both.
//that's why we have Job.remember_job_name().
//used by series replacement
Job.is_job_name = function(a_string){
return Job.all_names.includes(a_string)
}
Job.all_jobs = function(){
let result = []
for(let name of Job.all_names){
result.push(Job[name])
}
return result
}
Job.job_id_to_job_instance = function(job_id){
for(let name of Job.all_names){
if (Job[name].job_id === job_id) {return Job[name]}
}
return null
}
Job.last_job = null
//does not perform when_stopped action on purpose. This is a drastic stop.
Job.stop_all_jobs = function(){
var stopped_job_names = []
for(var j of Job.all_jobs()){
if (j.robot instanceof Dexter) { j.robot.empty_instruction_queue_now() }
if ((j.stop_reason == null) && (j.status_code != "not_started")){
j.stop_for_reason("interrupted", "User stopped all jobs.", false)
stopped_job_names.push(j.name)
j.color_job_button()
}
// j.robot.close() //does not delete the name of the robot from Robot, ie Robot.mydex will still exist, but does disconnect serial robots
//this almost is a good idea, but if there's a job that's stopped but for some reason,
//its serial port is still alive, better to call serial_disconnect_all()
if (j.robot instanceof Dexter) { j.robot.close_robot() } //needed when wanting to start up again, exp with dexter0
}
serial_disconnect_all()
if (stopped_job_names.length == 0){
out("There are no active jobs to stop.")
}
else {
out("Stopped jobs: " + stopped_job_names)
}
}
Job.clear_stopped_jobs = function(){
var cleared_job_names = []
for(var j of Job.all_jobs()){
if ((j.stop_reason != null) || (j.status_code == "not_started")){
delete Job[j.name]
Job.forget_job_name(j.name)
cleared_job_names.push(j.name)
j.remove_job_button()
if (j == Job.last_job) { Job.last_job = null }
}
}
if ((Job.last_job === null) && (Job.all_names.length > 0)){
Job.last_job = last(Job.all_names) //not technically the last job created since
//that was deleted
//and might not even be the last job "redefined".
//but its pretty close and the use of last_job isn't really sensitve to
//being precise so this is pretty good.
}
if (cleared_job_names.length == 0){
out("There are no stopped jobs to clear.")
}
else { out("Cleared jobs: " + cleared_job_names) }
}
//used in making robot_status_history window.
/* this functionality doesn't match its name, and its never called so don't have it!
Job.prototype.highest_sent_instruction_id = function(){
if (this.sent_instructions.length > 0){
return this.sent_instructions[0]
}
else { return null }
}*/
Job.report = function(){
if (Job.all_names.length == 0){
out("Either no jobs have been created in this session,<br/>" +
"or all the jobs have been cleared.<br/>" +
"See the <button>Jobs▼</button> <b>Insert example</b> menu item<br/>" +
"for help in creating a job.")
}
else {
var result = "<table style='border: 1px solid black;border-collapse: collapse;'><tr style='background-color:white;'><th>Job Name</th><th>ID</th><th>Robot</th><th>Start Time</th><th>Stop Time</th><th>Status</th></tr>"
for (var j of Job.all_jobs()){
var start_time = "Not started"
var stop_time = ""
if (j.start_time){
start_time = j.start_time.getHours() + ":" +
j.start_time.getMinutes() + ":" +
j.start_time.getSeconds() + ":" +
j.start_time.getMilliseconds()
stop_time = "ongoing"
}
if (j.stop_time){
stop_time = j.stop_time.getHours() + ":" +
j.stop_time.getMinutes() + ":" +
j.stop_time.getSeconds() + ":" +
j.stop_time.getMilliseconds()
}
var action = 'Job.print_out_one_job,,' + j.name
//var name = "<a href='#' title='Click for details on this job.' class='onclick_via_data' data-onclick='" + action + "'>" + j.name + "</a>"
var job_name = "<a href='#' title='Click for details on this job.' class='onclick_via_data' data-onclick='" + action + "'>" + j.name + "</a>"
result += "<tr/><td>" + job_name + "</td><td>" + j.job_id + "</td><td>" + j.robot.name + "</td><td>" + start_time + "</td><td>" + stop_time + "</td><td>" + j.status() + "</td><tr>"
}
result += "</table>"
out(result)
install_onclick_via_data_fns()
}
}
Job.prototype.print_out = function(){
out(this.stringify())
//setTimeout(function(){install_onclick_via_data_fns()}, 200) //needs to let the html render.
}
Job.print_out_one_job = function(job_name){
var j = Job[job_name]
j.print_out()
}
Job.prototype.status = function (){
if (this.stop_reason) { return this.status_code + ": " + this.stop_reason }
else if (this.wait_reason) { return this.status_code + ": " + this.wait_reason}
else {
let len = this.orig_args.do_list.length
if ( this.do_list) { len = this.do_list.length }
let pc = 0
if (this.program_counter) { pc = this.program_counter }
return this.status_code + ", pc: " + pc + " of " + len
}
}
Job.prototype.finish_job = function(perform_when_stopped=true){ //regardless of more to_do items or waiting for instruction, its over.
if(perform_when_stopped && (this.when_stopped !== "stop")){
let the_job = this //for closure
setTimeout(function(){
if(the_job.when_stopped == "wait") { //even if we somehow stopped in the middle of the do_list,
// we are going to wait for a new instruction to be added
//beware, maybe race condition here with adding a new instruction.
the_job.status_code = "running"
the_job.stop_reason = null
the_job.program_counter = the_job.do_list.length
the_job.color_job_button()
the_job.set_up_next_do(0)
}
else if (typeof(the_job.when_stopped) === "function"){
setTimeout(function () { the_job.when_stopped.call(the_job) },
100)
}
else if (Job.is_plausible_when_stopped_value(the_job.when_stopped)){
setTimeout(function(){
const found_job = Job.instruction_location_to_job(the_job.when_stopped, false)
if (found_job) { the_job = found_job }
the_job.start({program_counter: the_job.when_stopped})
}, 100)
}
else {
shouldnt("Job: " + the_job.name + " has an invalid wnen_stopped value of: " + the_job.when_stopped)
}
},
1)
}
else {
this.robot.finish_job()
this.color_job_button()
this.show_progress_maybe()
//this.print_out()
//out("Done with job: " + this.name +
// " <button onclick='function(){inspect_out(Job." + this.name + ")}'>Inspect</button>")
//inspect_out(this, null, null, null, true) //collapse inspector
}
}
Job.go_button_state = true
Job.set_go_button_state = function(bool){ //called initially from sandbox
pause_id.checked = !bool
Job.go_button_state = bool
}
Job.go = function(){
if (Job.go_button_state){
let any_active_jobs = false
for(let a_job of Job.all_jobs()){
if (a_job.is_active()){
any_active_jobs = true
if (a_job.go_state) {} //user hit go button with go_button_state true and a_job go true. let it run
//a_job.set_up_next_do(a_job.pause_next_program_counter_increment, false)
else { //go_button state is true but a_job go is false so turn it on an run
a_job.go_state = true
a_job.set_up_next_do(a_job.pause_next_program_counter_increment, false)
}
}
}
if (!any_active_jobs) { warning("There are no active jobs.", true) }
}
else { //go_button_state is false
let any_active_jobs = false
for(let a_job of Job.all_jobs()){
if (a_job.is_active()){
any_active_jobs = true
//if (a_job.go_state) {
a_job.set_up_next_do(a_job.pause_next_program_counter_increment, true) //allow once
//}
//else {} //go_button_state is false and a_job go is false, already paused, do nothing
}
}
if (!any_active_jobs) { warning("There are no active jobs.", true) }
}
return "dont_print"
}
//in EVERY call, as of mar 7, 2016 the arg is 1. So probably should just get rid of the arg.
//nope: we need it to be 0 when we have a fn that is "looping" checking for some
//condition to be true, in which case it moves on to incfement by 1, like "sleep" or something.
//this is important because send_to_job might do insert of its instruction "after_pc"
//and we want that to be in a "good" spot, such that the inserted insetruction
//will run next. So we want to keep the incrementing of the PC to be
//in the setTimeout so that when we do a insert "after_pc",
//that inserted instruction is run next.
Job.prototype.set_up_next_do = function(program_counter_increment = 1, allow_once=false, inter_do_item_dur=this.inter_do_item_dur){ //usual arg is 1 but a few control instructions that want to take a breath call it with 0
var job_instance = this
if (Job.go_button_state || allow_once){ //Job.go_button_state being true is the normal case
if ((this.status_code == "errored") || (this.status_code == "interrupted")){
program_counter_increment = 0 //don't increment because we want pc and highest_completed_instruction_id
//the instruction that errored when the job finishes.
}
if (program_counter_increment > 0) {
job_instance.highest_completed_instruction_id = job_instance.program_counter
}
job_instance.program_counter += program_counter_increment
setTimeout(function(){
job_instance.do_next_item()
},
inter_do_item_dur * 1000) //convert from seconds to milliseconds
}
else { //the stepper output
job_instance.pause_next_program_counter_increment = program_counter_increment
job_instance.go_state = false
let suffix = ""
if (job_instance.program_counter == -1) { suffix = " (initing robot status)" }
else if (job_instance.program_counter == 0) { suffix = " (your first instruction)" }
else if (job_instance.program_counter == job_instance.do_list.length - 1) { suffix = " (last instruction)" }
else if (job_instance.program_counter == job_instance.do_list.length - 2) { suffix = " (2nd to last instruction)" }
let out_text = job_instance.name + " paused after program_counter=" + job_instance.program_counter + " of " +
job_instance.do_list.length + suffix + "<br/>"
if(job_instance.program_counter >= 0) {
out_text += "Prev ins: " + Instruction.text_for_do_list_item_for_stepper(this.do_list[job_instance.program_counter])
}
else { out_text += "Prev ins: None" }
out_text += "<br/> Next ins: "
if ((job_instance.program_counter + 1) >= job_instance.do_list.length){
out_text += "None"
}
else {
out_text += Instruction.text_for_do_list_item_for_stepper(this.do_list[job_instance.program_counter + 1])
}
out(out_text, "brown", true)
}
}
Job.prototype.stop_for_reason = function(status_code, //"errored", "interrupted", "completed"
reason_string,
perform_when_stopped = false){
this.stop_reason = reason_string //put before set_status_code because set_status_code calls color_job_button
this.set_status_code(status_code)
if (this.robot.heartbeat_timeout_obj) { clearTimeout(this.robot.heartbeat_timeout_obj) }
this.stop_time = new Date()
if(!perform_when_stopped) { this.when_stopped = "stop"}
}
Job.prototype.do_next_item = function(){ //user calls this when they want the job to start, then this fn calls itself until done
//this.program_counter += 1 now done in set_up_next_do
//if (this.show_instructions){ console.log("Top of do_next_item in job: " + this.name + " with PC: " + this.program_counter)}
if ((this.status_code == "interrupted") || //put before the wait until instruction_id because interrupted is the user wanting to halt, regardless of pending instructions.
(this.status_code == "errored")){
this.finish_job()
}
else if (this.wait_until_instruction_id_has_run || (this.wait_until_instruction_id_has_run === 0)){ //the ordering of this clause is important. Nothing below has to wait for instructions to complete
//wait for the wait instruction id to be done
//the waited for instruction coming back thru robot_done_with_instruction will call set_up_next_do(1)
//so don't do it here. BUT still have this clause to block doing anything below if we're waiting.
}
else if (this.stop_reason){ this.finish_job() } //must be before the below since if we've
//already got a stop reason, we don't want to keep waiting for another instruction.
else if (this.wait_until_this_prop_is_false) { this.set_up_next_do(0) }
else if (this.program_counter >= this.instruction_location_to_id(this.ending_program_counter)) { //this.do_list.length