openc3-cosmos-script-engine-cstol 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/cstol_script_engine.py +146 -54
- data/public/store_img.png +0 -0
- metadata +13 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ef43bd11a38493eccfc313dccee63034ead05937930eaed62e5c164bd4b68a4
|
|
4
|
+
data.tar.gz: e63152c3a168bf6da0b6d70b3e3aa9f05cb2c974c631cbd4e1ac5f36908618a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eab9091c50b1867c2d4ac4b2794f72abb67bd48352ccbd01cfa14fb442be43314c1766e73ba418cef8445d6f596d7855b54306c6b5a58cddfcca900698450107
|
|
7
|
+
data.tar.gz: d23e20164925cafe6a9efee37d6348b2277a1996a53fe8d05f0b31dd6fa4612635a0e34c1ffdcf893af8a5c297b175ece81a747250459ce747a97e298a3e6ece
|
data/lib/cstol_script_engine.py
CHANGED
|
@@ -25,12 +25,18 @@ import shlex
|
|
|
25
25
|
import datetime
|
|
26
26
|
import math
|
|
27
27
|
import os
|
|
28
|
-
from openc3.script.exceptions import CheckError,
|
|
28
|
+
from openc3.script.exceptions import CheckError, StopScriptError
|
|
29
29
|
from openc3.script_engines.script_engine import ScriptEngine
|
|
30
30
|
from openc3.script import wait, wait_expression, ask_string, clear_screen, clear_all_screens, set_tlm, display_screen, \
|
|
31
31
|
connect_interface, disconnect_interface, send_raw, start, cmd, get_target_file, tlm, ask_string, set_line_delay, step_mode, run_mode
|
|
32
32
|
|
|
33
33
|
class CstolVariables:
|
|
34
|
+
# NOTE: special_variables is INTENTIONALLY a class-level (shared) dictionary.
|
|
35
|
+
# Special variables ($$ variables) are global to the engine and must persist
|
|
36
|
+
# across every script run and every CstolVariables instance. Unlike
|
|
37
|
+
# local_variables (which __init__ resets per instance), this state is meant to
|
|
38
|
+
# be shared by all instances. Do NOT move this into __init__ or copy it per
|
|
39
|
+
# instance - that would break the intended global behavior.
|
|
34
40
|
special_variables = {
|
|
35
41
|
"$$OWLT": 0.0,
|
|
36
42
|
"$$ERROR": "NO_ERROR",
|
|
@@ -43,8 +49,23 @@ class CstolVariables:
|
|
|
43
49
|
|
|
44
50
|
def __init__(self):
|
|
45
51
|
self.local_variables = {}
|
|
52
|
+
self.if_stack = []
|
|
46
53
|
self.loop_stack = []
|
|
47
54
|
|
|
55
|
+
def set_local_variable(self, name, value):
|
|
56
|
+
"""
|
|
57
|
+
Sets a local variable. Variable names are case insensitive, so they are
|
|
58
|
+
normalized to upper case before being stored.
|
|
59
|
+
"""
|
|
60
|
+
self.local_variables[name.upper()] = value
|
|
61
|
+
|
|
62
|
+
def get_local_variable(self, name):
|
|
63
|
+
"""
|
|
64
|
+
Gets a local variable by name (case insensitive).
|
|
65
|
+
Returns None if the variable does not exist.
|
|
66
|
+
"""
|
|
67
|
+
return self.local_variables.get(name.upper(), None)
|
|
68
|
+
|
|
48
69
|
def set_special_variable(self, name, value):
|
|
49
70
|
"""
|
|
50
71
|
Sets a special variable, which is a variable that starts with '$$'.
|
|
@@ -52,8 +73,9 @@ class CstolVariables:
|
|
|
52
73
|
"""
|
|
53
74
|
if not name.startswith('$$'):
|
|
54
75
|
raise ValueError(f"Special variable names must start with '$$': {name}")
|
|
76
|
+
name = name.upper()
|
|
55
77
|
self.special_variables[name] = value
|
|
56
|
-
match name
|
|
78
|
+
match name:
|
|
57
79
|
case "$$CLP_STP_INTERVAL":
|
|
58
80
|
set_line_delay(value)
|
|
59
81
|
self.special_variables["$$STEP_INTERVAL"] = value
|
|
@@ -82,7 +104,8 @@ class CstolVariables:
|
|
|
82
104
|
"""
|
|
83
105
|
if not name.startswith('$$'):
|
|
84
106
|
raise ValueError(f"Special variable names must start with '$$': {name}")
|
|
85
|
-
|
|
107
|
+
name = name.upper()
|
|
108
|
+
match name:
|
|
86
109
|
case "$$CURRENT_TIME":
|
|
87
110
|
return datetime.datetime.now(datetime.timezone.utc).timestamp()
|
|
88
111
|
case "$$SC_TIME":
|
|
@@ -240,7 +263,19 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
240
263
|
i += 3 # Skip the next 2 tokens since we combined them
|
|
241
264
|
|
|
242
265
|
else:
|
|
243
|
-
|
|
266
|
+
# Recombine multi-part operator tokens
|
|
267
|
+
token = tokens[i]
|
|
268
|
+
if token in self.KNOWN_TOKENS and i + 1 < len(tokens):
|
|
269
|
+
next_token = tokens[i + 1]
|
|
270
|
+
if ((token == '*' and next_token == '*') or
|
|
271
|
+
(token == '<' and next_token == '=') or
|
|
272
|
+
(token == '>' and next_token == '=') or
|
|
273
|
+
(token == '/' and next_token == '=')):
|
|
274
|
+
reconstructed_tokens.append(token + next_token)
|
|
275
|
+
i += 2 # Skip the next token
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
reconstructed_tokens.append(token)
|
|
244
279
|
i += 1
|
|
245
280
|
|
|
246
281
|
return reconstructed_tokens
|
|
@@ -280,7 +315,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
280
315
|
# Handle local variables
|
|
281
316
|
if token.startswith('$'):
|
|
282
317
|
# Get the actual value of local variable
|
|
283
|
-
value = self.variables.
|
|
318
|
+
value = self.variables.get_local_variable(token)
|
|
284
319
|
if value is None:
|
|
285
320
|
raise ValueError(f"Unknown variable: {token}")
|
|
286
321
|
if isinstance(value, str):
|
|
@@ -312,19 +347,19 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
312
347
|
continue
|
|
313
348
|
|
|
314
349
|
# Handle radix notation integers
|
|
315
|
-
|
|
350
|
+
# Allow hex digits (A-F) for all bases so X#FF and H#DEADBEEF parse;
|
|
351
|
+
# int() with the proper base validates the digits for each radix.
|
|
352
|
+
matches = re.match(r'^([BODXHbodxh])#([0-9A-Fa-f]+)$', token)
|
|
316
353
|
if matches is not None:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
case 'O':
|
|
321
|
-
final_tokens.append(f'0o{token[2:]}')
|
|
322
|
-
case 'D':
|
|
323
|
-
final_tokens.append(f'{token[2:]}')
|
|
354
|
+
radix_char = matches.group(1).upper()
|
|
355
|
+
digits = matches.group(2)
|
|
356
|
+
match radix_char:
|
|
324
357
|
case 'H':
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
358
|
+
# Hex byte buffer (raw bytes rather than an integer)
|
|
359
|
+
final_tokens.append(str(int(digits, 16).to_bytes((len(digits) + 1) // 2, 'big')))
|
|
360
|
+
case _:
|
|
361
|
+
base = {'B': 2, 'O': 8, 'D': 10, 'X': 16}[radix_char]
|
|
362
|
+
final_tokens.append(str(int(digits, base)))
|
|
328
363
|
previous_number = True
|
|
329
364
|
continue
|
|
330
365
|
|
|
@@ -359,7 +394,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
359
394
|
token = final_tokens[i]
|
|
360
395
|
|
|
361
396
|
# Check for "RAW" pattern first: "RAW", "TARGET_NAME", "ITEM_NAME"
|
|
362
|
-
if (token == '"RAW"' and i + 2 < len(final_tokens) and
|
|
397
|
+
if (token.upper() == '"RAW"' and i + 2 < len(final_tokens) and
|
|
363
398
|
final_tokens[i + 1].startswith('"') and final_tokens[i + 1].endswith('"') and
|
|
364
399
|
final_tokens[i + 2].startswith('"') and final_tokens[i + 2].endswith('"')):
|
|
365
400
|
|
|
@@ -388,7 +423,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
388
423
|
return ' '.join(processed_tokens)
|
|
389
424
|
|
|
390
425
|
# Combined regex pattern to match both formats
|
|
391
|
-
TIMESTAMP_PATTERN = r'(?:(\d{4})?/(\d{1,3})?-)?(\d{
|
|
426
|
+
TIMESTAMP_PATTERN = r'(?:(\d{4})?/(\d{1,3})?-)?(\d{0,2}):(\d{0,2}):(\d{1,2}\.?\d*)'
|
|
392
427
|
|
|
393
428
|
# Will convert a CSTOL Clock Time or Delta Time into floating point seconds
|
|
394
429
|
def parse_timestamp(self, timestamp, now = None):
|
|
@@ -398,6 +433,10 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
398
433
|
matches = re.match(self.TIMESTAMP_PATTERN, timestamp)
|
|
399
434
|
if matches:
|
|
400
435
|
year, day_of_year, hour, minute, second = matches.groups()
|
|
436
|
+
if len(hour) == 0:
|
|
437
|
+
hour = 0
|
|
438
|
+
if len(minute) == 0:
|
|
439
|
+
minute = 0
|
|
401
440
|
result = {
|
|
402
441
|
'hour': int(hour),
|
|
403
442
|
'minute': int(minute),
|
|
@@ -475,6 +514,29 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
475
514
|
expressions.append(expression)
|
|
476
515
|
return expressions
|
|
477
516
|
|
|
517
|
+
def _split_wait_timeout(self, tokens):
|
|
518
|
+
"""Split WAIT body tokens into (condition_tokens, timeout_tokens | None).
|
|
519
|
+
|
|
520
|
+
Scans from the end of the token list looking for the last ``OR``
|
|
521
|
+
that is immediately followed by ``FOR`` or ``UNTIL``. That ``OR``
|
|
522
|
+
marks the boundary between the boolean condition and the timeout
|
|
523
|
+
clause.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
(condition_tokens, timeout_tokens)
|
|
528
|
+
*timeout_tokens* includes the ``FOR``/``UNTIL`` keyword and the
|
|
529
|
+
value that follows. If no ``OR FOR`` / ``OR UNTIL`` sequence is
|
|
530
|
+
found the full token list is returned as the condition and
|
|
531
|
+
*timeout_tokens* is ``None``.
|
|
532
|
+
"""
|
|
533
|
+
# Walk backwards so we find the last OR FOR / OR UNTIL (before the timeout).
|
|
534
|
+
# Earlier OR tokens are boolean operators inside the condition.
|
|
535
|
+
for i in range(len(tokens) - 1, 0, -1):
|
|
536
|
+
if tokens[i - 1].upper() == "OR" and tokens[i].upper() in ("FOR", "UNTIL"):
|
|
537
|
+
return tokens[:i - 1], tokens[i:]
|
|
538
|
+
return tokens, None
|
|
539
|
+
|
|
478
540
|
def split_vs_tokens_on_colon(self, tokens):
|
|
479
541
|
"""
|
|
480
542
|
Split tokens on colons for VS clause processing, but preserve timestamps.
|
|
@@ -537,9 +599,12 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
537
599
|
question = question[1:-1]
|
|
538
600
|
answer = ask_string(question)
|
|
539
601
|
if answer is not None:
|
|
540
|
-
if answer[0] == '"' and answer[-1] == '"':
|
|
602
|
+
if len(answer) >= 2 and answer[0] == '"' and answer[-1] == '"':
|
|
541
603
|
# Remove quotes and don't uppercase and don't eval
|
|
542
604
|
answer = answer[1:-1]
|
|
605
|
+
elif answer.strip() == '':
|
|
606
|
+
# Empty answer - store as-is without tokenizing/evaluating
|
|
607
|
+
pass
|
|
543
608
|
else:
|
|
544
609
|
# Tokenize answer
|
|
545
610
|
answer_tokens = self.cstol_tokenizer(answer)
|
|
@@ -550,11 +615,13 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
550
615
|
# Uppercase
|
|
551
616
|
if isinstance(answer, str):
|
|
552
617
|
answer = answer.upper()
|
|
553
|
-
self.variables.
|
|
618
|
+
self.variables.set_local_variable(variable, answer)
|
|
554
619
|
|
|
555
620
|
def handle_check(self, tokens, line_no):
|
|
556
621
|
expressions = self.extract_expressions(tokens[1:], ",")
|
|
557
622
|
for expr in expressions:
|
|
623
|
+
if len(expr) == 0:
|
|
624
|
+
raise ValueError(f"Empty expression in CHECK command at line {line_no}")
|
|
558
625
|
format = None
|
|
559
626
|
if expr[0][0] == '%':
|
|
560
627
|
# Format string
|
|
@@ -742,7 +809,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
742
809
|
raise ValueError(f"Expected '=' after variable name in DECLARE command at line {line_no}")
|
|
743
810
|
#default_value = self.token_to_value(tokens[4])
|
|
744
811
|
default_value = self.evaluate_tokens([tokens[4]])[0]
|
|
745
|
-
self.variables.
|
|
812
|
+
self.variables.set_local_variable(variable_name, default_value)
|
|
746
813
|
|
|
747
814
|
def handle_display(self, tokens, line_no):
|
|
748
815
|
screen_name = tokens[1]
|
|
@@ -752,9 +819,29 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
752
819
|
display_screen(*screen_name.split())
|
|
753
820
|
|
|
754
821
|
def handle_else(self, tokens, lines, line_no):
|
|
755
|
-
|
|
822
|
+
goto_endif = False
|
|
823
|
+
if len(tokens) > 1:
|
|
824
|
+
# ELSE IF is tricky - We can hit these after a successful IF, or an unsuccessful IF
|
|
825
|
+
# The if_stack keeps track if an earlier if was successful and its value will determine if we
|
|
826
|
+
# execute the ELSEIF or just goto the next ENDIF
|
|
827
|
+
current_if = False
|
|
828
|
+
if len(self.variables.if_stack) > 0:
|
|
829
|
+
current_if = self.variables.if_stack[-1]
|
|
830
|
+
if tokens[0].upper() == 'ELSEIF':
|
|
831
|
+
if current_if:
|
|
832
|
+
goto_endif = True
|
|
833
|
+
else:
|
|
834
|
+
return self.handle_if(tokens, lines, line_no)
|
|
835
|
+
elif (tokens[0].upper() == 'ELSE' and tokens[1].upper() == 'IF'):
|
|
836
|
+
if current_if:
|
|
837
|
+
goto_endif = True
|
|
838
|
+
else:
|
|
839
|
+
return self.handle_if(tokens[1:], lines, line_no)
|
|
840
|
+
|
|
841
|
+
if goto_endif or (len(tokens) == 1 and tokens[0].upper() == 'ELSE'):
|
|
756
842
|
# The only way we ever hit an ELSE is if we were in a successful block beforehand
|
|
757
843
|
# Therefore goto the ENDIF
|
|
844
|
+
self.variables.if_stack[-1] = True
|
|
758
845
|
depth = 1
|
|
759
846
|
for i in range(line_no, len(lines)):
|
|
760
847
|
next_line = lines[i].strip().upper()
|
|
@@ -766,18 +853,16 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
766
853
|
return i + 1
|
|
767
854
|
raise ValueError(f"No matching ENDIF for ELSE command at line {line_no}")
|
|
768
855
|
|
|
769
|
-
|
|
770
|
-
if tokens[0] == 'ELSEIF':
|
|
771
|
-
return self.handle_if(tokens, lines, line_no)
|
|
772
|
-
elif (tokens[0] == 'ELSE' and tokens[1] == 'IF'):
|
|
773
|
-
return self.handle_if(tokens[1:], lines, line_no)
|
|
856
|
+
raise ValueError(f"handle_else called with unexpected tokens at line {line_no}")
|
|
774
857
|
|
|
775
858
|
def handle_end(self, tokens, line_no):
|
|
776
|
-
if len(tokens) == 1 and tokens[0] == 'END':
|
|
859
|
+
if len(tokens) == 1 and tokens[0].upper() == 'END':
|
|
777
860
|
raise ValueError(f"Unexpected END command at line {line_no}, expected ENDIF, ENDLOOP, ENDMACRO, or ENDPROC")
|
|
778
|
-
elif (len(tokens) == 1 and tokens[0] == 'ENDIF') or (tokens[0] == 'END' and tokens[1] == 'IF'):
|
|
861
|
+
elif (len(tokens) == 1 and tokens[0].upper() == 'ENDIF') or (tokens[0].upper() == 'END' and tokens[1].upper() == 'IF'):
|
|
862
|
+
# END IF pops the IF stack
|
|
863
|
+
self.variables.if_stack.pop()
|
|
779
864
|
pass
|
|
780
|
-
elif (len(tokens) == 1 and tokens[0] == 'ENDLOOP') or (tokens[0] == 'END' and tokens[1] == 'LOOP'):
|
|
865
|
+
elif (len(tokens) == 1 and tokens[0].upper() == 'ENDLOOP') or (tokens[0].upper() == 'END' and tokens[1].upper() == 'LOOP'):
|
|
781
866
|
if len(self.variables.loop_stack) > 0:
|
|
782
867
|
loop_info = self.variables.loop_stack[-1]
|
|
783
868
|
loop_info[2] += 1
|
|
@@ -798,10 +883,14 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
798
883
|
# Find the matching ENDLOOP
|
|
799
884
|
depth = 1
|
|
800
885
|
for i in range(line_no, len(lines)):
|
|
801
|
-
|
|
802
|
-
|
|
886
|
+
# Match on whole words so labels like "LOOPBACK:" are not mistaken
|
|
887
|
+
# for a nested LOOP keyword
|
|
888
|
+
words = lines[i].strip().upper().split()
|
|
889
|
+
if not words:
|
|
890
|
+
continue
|
|
891
|
+
if words[0] == 'LOOP':
|
|
803
892
|
depth += 1
|
|
804
|
-
elif
|
|
893
|
+
elif words[0] == 'ENDLOOP' or (words[0] == 'END' and len(words) > 1 and words[1] == 'LOOP'):
|
|
805
894
|
depth -= 1
|
|
806
895
|
if depth == 0:
|
|
807
896
|
if len(self.variables.loop_stack) > 0:
|
|
@@ -810,17 +899,17 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
810
899
|
raise ValueError(f"No matching ENDLOOP found for ESCAPE command at line {line_no}")
|
|
811
900
|
|
|
812
901
|
def handle_go(self, tokens, lines, line_no):
|
|
813
|
-
if tokens[0] == 'GOTO' or (len(tokens) > 1 and tokens[0] == 'GO' and tokens[1] == 'TO'):
|
|
814
|
-
if (tokens[0] == 'GOTO' and len(tokens) < 2) or (tokens[0] == 'GO' and
|
|
902
|
+
if tokens[0].upper() == 'GOTO' or (len(tokens) > 1 and tokens[0].upper() == 'GO' and tokens[1].upper() == 'TO'):
|
|
903
|
+
if (tokens[0].upper() == 'GOTO' and len(tokens) < 2) or (tokens[0].upper() == 'GO' and len(tokens) < 3):
|
|
815
904
|
raise ValueError(f"Invalid GOTO command format at line {line_no}")
|
|
816
905
|
label = None
|
|
817
|
-
if tokens[0] == 'GOTO':
|
|
818
|
-
label = tokens[1] + ':'
|
|
906
|
+
if tokens[0].upper() == 'GOTO':
|
|
907
|
+
label = (tokens[1] + ':').upper()
|
|
819
908
|
else:
|
|
820
|
-
label = tokens[2] + ':'
|
|
909
|
+
label = (tokens[2] + ':').upper()
|
|
821
910
|
# Find the label in the lines
|
|
822
911
|
for i in range(1, len(lines)):
|
|
823
|
-
next_line = lines[i - 1].strip()
|
|
912
|
+
next_line = lines[i - 1].strip().upper()
|
|
824
913
|
if next_line.startswith(label):
|
|
825
914
|
return i
|
|
826
915
|
raise ValueError(f"Label '{label}' not found in GOTO command at line {line_no}")
|
|
@@ -835,6 +924,8 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
835
924
|
raise ValueError(f"Error evaluating expression '{expression_tokens}' in IF command at line {line_no}: {e}")
|
|
836
925
|
|
|
837
926
|
if result:
|
|
927
|
+
# Mark if as handled
|
|
928
|
+
self.variables.if_stack[-1] = True
|
|
838
929
|
return line_no + 1 # Continue to the next line if the condition is true
|
|
839
930
|
# If the condition is false, skip to the next ENDIF or ELSE
|
|
840
931
|
else:
|
|
@@ -897,7 +988,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
897
988
|
self.variables.set_special_variable(variable_name, result)
|
|
898
989
|
else:
|
|
899
990
|
# Local variable
|
|
900
|
-
self.variables.
|
|
991
|
+
self.variables.set_local_variable(variable_name, result)
|
|
901
992
|
|
|
902
993
|
def handle_load(self, tokens, line_no):
|
|
903
994
|
# LOAD external-element-name AT location FROM file-name
|
|
@@ -923,14 +1014,11 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
923
1014
|
if len(tokens) == 1:
|
|
924
1015
|
# Infinite loop
|
|
925
1016
|
self.variables.loop_stack.append([line_no + 1, None, 1])
|
|
926
|
-
return line_no + 1 # Continue to the next line
|
|
927
|
-
elif len(tokens) == 2:
|
|
928
|
-
# Counted loop
|
|
929
|
-
count = int(tokens[1])
|
|
930
|
-
self.variables.loop_stack.append([line_no + 1, count, 1])
|
|
931
|
-
return line_no + 1 # Continue to the next line
|
|
932
1017
|
else:
|
|
933
|
-
|
|
1018
|
+
# Counted loop - the count may be a literal, variable, or expression
|
|
1019
|
+
count = int(self.evaluate_expression(tokens[1:]))
|
|
1020
|
+
self.variables.loop_stack.append([line_no + 1, count, 1])
|
|
1021
|
+
return line_no + 1 # Continue to the next line
|
|
934
1022
|
|
|
935
1023
|
def handle_proc(self, tokens, line_no):
|
|
936
1024
|
# tokens[1] is proc name
|
|
@@ -941,7 +1029,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
941
1029
|
if token == ',':
|
|
942
1030
|
continue
|
|
943
1031
|
elif token.startswith('$'):
|
|
944
|
-
self.variables.
|
|
1032
|
+
self.variables.set_local_variable(token, os.getenv(f"CSTOL_ARG_{index}"))
|
|
945
1033
|
index += 1
|
|
946
1034
|
else:
|
|
947
1035
|
raise ValueError(f"Invalid variable '{token}' in PROC command at line {line_no}")
|
|
@@ -949,7 +1037,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
949
1037
|
def handle_return(self, tokens, lines, line_no):
|
|
950
1038
|
if len(tokens) > 1:
|
|
951
1039
|
if tokens[1].upper() == 'ALL':
|
|
952
|
-
raise
|
|
1040
|
+
raise StopScriptError
|
|
953
1041
|
return (len(lines) + 1)
|
|
954
1042
|
|
|
955
1043
|
def handle_run(self, tokens, line_no):
|
|
@@ -1023,11 +1111,11 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1023
1111
|
raise ValueError(f"Invalid timestamp format at line {line_no}")
|
|
1024
1112
|
else:
|
|
1025
1113
|
# Conditional expression wait
|
|
1026
|
-
|
|
1027
|
-
python_expression = self.build_python_expression(
|
|
1028
|
-
if
|
|
1114
|
+
condition_tokens, timeout_tokens = self._split_wait_timeout(tokens[1:])
|
|
1115
|
+
python_expression = self.build_python_expression(condition_tokens)
|
|
1116
|
+
if timeout_tokens is not None:
|
|
1029
1117
|
# Timeout given with "OR FOR" or "OR UNTIL"
|
|
1030
|
-
seconds = self.evaluate_expression(
|
|
1118
|
+
seconds = self.evaluate_expression(timeout_tokens[1:]) # Drop FOR or UNTIL token
|
|
1031
1119
|
if isinstance(seconds, (int, float, complex)) and not isinstance(seconds, bool):
|
|
1032
1120
|
if seconds > self.ONE_YEAR_SECONDS:
|
|
1033
1121
|
now = datetime.datetime.now().timestamp()
|
|
@@ -1052,6 +1140,8 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1052
1140
|
expressions = self.extract_expressions(tokens[1:], ",")
|
|
1053
1141
|
results = []
|
|
1054
1142
|
for expr in expressions:
|
|
1143
|
+
if len(expr) == 0:
|
|
1144
|
+
raise ValueError(f"Empty expression in WRITE command at line {line_no}")
|
|
1055
1145
|
result = ''
|
|
1056
1146
|
if expr[0][0] == '%':
|
|
1057
1147
|
# Format string
|
|
@@ -1166,6 +1256,8 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1166
1256
|
case "GO" | "GOTO":
|
|
1167
1257
|
return self.handle_go(tokens, lines, line_no)
|
|
1168
1258
|
case "IF":
|
|
1259
|
+
# Regular IF starts an if stack
|
|
1260
|
+
self.variables.if_stack.append(False)
|
|
1169
1261
|
return self.handle_if(tokens, lines, line_no)
|
|
1170
1262
|
case "LET":
|
|
1171
1263
|
self.handle_let(tokens, line_no)
|
|
@@ -1192,4 +1284,4 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1192
1284
|
case _:
|
|
1193
1285
|
raise ValueError(f"Unknown keyword '{keyword}' at line {line_no}")
|
|
1194
1286
|
|
|
1195
|
-
return line_no + 1
|
|
1287
|
+
return line_no + 1
|
|
Binary file
|
metadata
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openc3-cosmos-script-engine-cstol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- OpenC3, Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: " This plugin provides a script engine to support CSTOL in Script Runner\n"
|
|
14
14
|
email:
|
|
15
|
-
-
|
|
15
|
+
- plugins@openc3.com
|
|
16
16
|
executables: []
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
@@ -22,12 +22,19 @@ files:
|
|
|
22
22
|
- Rakefile
|
|
23
23
|
- lib/cstol_script_engine.py
|
|
24
24
|
- plugin.txt
|
|
25
|
+
- public/store_img.png
|
|
25
26
|
- targets/CSTOL_TEST/procedures/collect.prc
|
|
26
27
|
- targets/CSTOL_TEST/procedures/test.prc
|
|
27
28
|
homepage: https://github.com/OpenC3/openc3-cosmos-script-engine-cstol
|
|
28
29
|
licenses:
|
|
29
30
|
- Nonstandard
|
|
30
|
-
metadata:
|
|
31
|
+
metadata:
|
|
32
|
+
source_code_uri: https://github.com/OpenC3/openc3-cosmos-script-engine-cstol
|
|
33
|
+
openc3_store_title: CSTOL Script Engine
|
|
34
|
+
openc3_store_keywords: script, cstol
|
|
35
|
+
openc3_store_image: public/store_img.png
|
|
36
|
+
openc3_store_access_type: public
|
|
37
|
+
openc3_cosmos_minimum_version: 7.0.0
|
|
31
38
|
post_install_message:
|
|
32
39
|
rdoc_options: []
|
|
33
40
|
require_paths:
|
|
@@ -46,5 +53,5 @@ requirements: []
|
|
|
46
53
|
rubygems_version: 3.5.22
|
|
47
54
|
signing_key:
|
|
48
55
|
specification_version: 4
|
|
49
|
-
summary: OpenC3 Script Engine
|
|
56
|
+
summary: OpenC3 CSTOL Script Engine
|
|
50
57
|
test_files: []
|