openc3-cosmos-script-engine-cstol 1.1.1 → 1.3.0
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/README.md +100 -8
- data/lib/cstol_script_engine.py +623 -405
- data/pyproject.toml +83 -0
- data/uv.lock +1396 -0
- metadata +4 -2
data/lib/cstol_script_engine.py
CHANGED
|
@@ -20,15 +20,72 @@
|
|
|
20
20
|
#
|
|
21
21
|
# CSTOL language originally developed by the University of Colorado / LASP.
|
|
22
22
|
|
|
23
|
-
import re
|
|
24
|
-
import shlex
|
|
25
23
|
import datetime
|
|
26
24
|
import math
|
|
27
25
|
import os
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
from openc3.script import (
|
|
29
|
+
ask_string,
|
|
30
|
+
clear_all_screens,
|
|
31
|
+
clear_screen,
|
|
32
|
+
cmd,
|
|
33
|
+
connect_interface,
|
|
34
|
+
disconnect_interface,
|
|
35
|
+
display_screen,
|
|
36
|
+
get_target_file,
|
|
37
|
+
run_mode,
|
|
38
|
+
send_raw,
|
|
39
|
+
set_line_delay,
|
|
40
|
+
set_tlm,
|
|
41
|
+
start,
|
|
42
|
+
step_mode,
|
|
43
|
+
tlm,
|
|
44
|
+
wait,
|
|
45
|
+
wait_expression,
|
|
46
|
+
)
|
|
28
47
|
from openc3.script.exceptions import CheckError, StopScriptError
|
|
29
48
|
from openc3.script_engines.script_engine import ScriptEngine
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
|
|
50
|
+
# Pieces of a yyyy/doy-HH:MM:SS timestamp. The tokenizer splits timestamps on '/'
|
|
51
|
+
# and '-', so cstol_tokenizer uses these to stitch the pieces back together.
|
|
52
|
+
YEAR_PATTERN = re.compile(r"^\d{4}$")
|
|
53
|
+
DAY_OF_YEAR_PATTERN = re.compile(r"^\d{1,3}$")
|
|
54
|
+
CLOCK_TIME_PATTERN = re.compile(r"^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$")
|
|
55
|
+
|
|
56
|
+
SPECIAL_VARIABLE_PREFIX = "$$"
|
|
57
|
+
|
|
58
|
+
# CSTOL %<format> specifiers, mapped to the Python format spec that implements them.
|
|
59
|
+
# %X or %x Output in hexadecimal values
|
|
60
|
+
# %O or %o Output in octal values
|
|
61
|
+
# %B or %b Output in binary values
|
|
62
|
+
# %I or %i Output in decimal values
|
|
63
|
+
# %D or %d Output in decimal values
|
|
64
|
+
# The five above convert the value to an integer prior to applying the format.
|
|
65
|
+
# %F or %f Output in floating point values Default for integer or raw value
|
|
66
|
+
# %E or %e Output in floating point values Default for float or EU value
|
|
67
|
+
INTEGER_FORMATS = {"X": "X", "O": "o", "B": "b", "I": "d", "D": "d"}
|
|
68
|
+
FLOAT_FORMATS = {"F": ".6f", "E": ".6e"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SpecialVars:
|
|
72
|
+
"""
|
|
73
|
+
Names of the special ($$) variables the engine reads or writes itself. Scripts
|
|
74
|
+
can reference any special variable by name, these are only the ones referenced
|
|
75
|
+
from Python code.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
OWLT = "$$OWLT"
|
|
79
|
+
ERROR = "$$ERROR"
|
|
80
|
+
CHECK_INTERVAL = "$$CHECK_INTERVAL"
|
|
81
|
+
STEP_INTERVAL = "$$STEP_INTERVAL"
|
|
82
|
+
CLP_STP_INTERVAL = "$$CLP_STP_INTERVAL"
|
|
83
|
+
CLP_STEP_MODE = "$$CLP_STEP_MODE"
|
|
84
|
+
STEP_MODE = "$$STEP_MODE"
|
|
85
|
+
CURRENT_TIME = "$$CURRENT_TIME"
|
|
86
|
+
SC_TIME = "$$SC_TIME"
|
|
87
|
+
LOOP_COUNT = "$$LOOP_COUNT"
|
|
88
|
+
|
|
32
89
|
|
|
33
90
|
class CstolVariables:
|
|
34
91
|
# NOTE: special_variables is INTENTIONALLY a class-level (shared) dictionary.
|
|
@@ -38,13 +95,13 @@ class CstolVariables:
|
|
|
38
95
|
# be shared by all instances. Do NOT move this into __init__ or copy it per
|
|
39
96
|
# instance - that would break the intended global behavior.
|
|
40
97
|
special_variables = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
SpecialVars.OWLT: 0.0,
|
|
99
|
+
SpecialVars.ERROR: "NO_ERROR",
|
|
100
|
+
SpecialVars.CHECK_INTERVAL: 1.0,
|
|
101
|
+
SpecialVars.STEP_INTERVAL: 0.1,
|
|
102
|
+
SpecialVars.CLP_STP_INTERVAL: 0.1,
|
|
103
|
+
SpecialVars.CLP_STEP_MODE: "PAUSE",
|
|
104
|
+
SpecialVars.STEP_MODE: "PAUSE",
|
|
48
105
|
}
|
|
49
106
|
|
|
50
107
|
def __init__(self):
|
|
@@ -66,51 +123,66 @@ class CstolVariables:
|
|
|
66
123
|
"""
|
|
67
124
|
return self.local_variables.get(name.upper(), None)
|
|
68
125
|
|
|
126
|
+
def has_local_variable(self, name):
|
|
127
|
+
"""
|
|
128
|
+
Returns True if the local variable has been set (case insensitive).
|
|
129
|
+
Note this is different from get_local_variable returning None because
|
|
130
|
+
a variable can be legitimately set to None.
|
|
131
|
+
"""
|
|
132
|
+
return name.upper() in self.local_variables
|
|
133
|
+
|
|
69
134
|
def set_special_variable(self, name, value):
|
|
70
135
|
"""
|
|
71
136
|
Sets a special variable, which is a variable that starts with '$$'.
|
|
72
137
|
Special variables are not stored in the local variables dictionary.
|
|
73
138
|
"""
|
|
74
|
-
if not name.startswith(
|
|
75
|
-
raise ValueError(
|
|
139
|
+
if not name.startswith(SPECIAL_VARIABLE_PREFIX):
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Special variable names must start with '{SPECIAL_VARIABLE_PREFIX}': {name}"
|
|
142
|
+
)
|
|
76
143
|
name = name.upper()
|
|
77
144
|
self.special_variables[name] = value
|
|
78
145
|
match name:
|
|
79
|
-
case
|
|
146
|
+
case SpecialVars.CLP_STP_INTERVAL:
|
|
80
147
|
set_line_delay(value)
|
|
81
|
-
self.special_variables[
|
|
82
|
-
case
|
|
83
|
-
self.set_special_variable(
|
|
84
|
-
case
|
|
148
|
+
self.special_variables[SpecialVars.STEP_INTERVAL] = value
|
|
149
|
+
case SpecialVars.STEP_INTERVAL:
|
|
150
|
+
self.set_special_variable(SpecialVars.CLP_STP_INTERVAL, value)
|
|
151
|
+
case SpecialVars.CLP_STEP_MODE:
|
|
85
152
|
mode = str(value).upper()
|
|
86
153
|
if mode == "GO":
|
|
87
154
|
run_mode()
|
|
88
155
|
set_line_delay(0)
|
|
89
156
|
elif mode == "PAUSE":
|
|
90
157
|
run_mode()
|
|
91
|
-
set_line_delay(self.special_variables[
|
|
158
|
+
set_line_delay(self.special_variables[SpecialVars.STEP_INTERVAL])
|
|
92
159
|
elif mode == "WAIT":
|
|
93
160
|
step_mode()
|
|
94
161
|
else:
|
|
95
162
|
raise ValueError(f"Invalid step mode: {mode}")
|
|
96
|
-
self.special_variables[
|
|
97
|
-
case
|
|
98
|
-
self.set_special_variable(
|
|
163
|
+
self.special_variables[SpecialVars.STEP_MODE] = value
|
|
164
|
+
case SpecialVars.STEP_MODE:
|
|
165
|
+
self.set_special_variable(SpecialVars.CLP_STEP_MODE, value)
|
|
99
166
|
|
|
100
167
|
def get_special_variable(self, name):
|
|
101
168
|
"""
|
|
102
169
|
Gets a special variable by name.
|
|
103
170
|
Returns None if the variable does not exist.
|
|
104
171
|
"""
|
|
105
|
-
if not name.startswith(
|
|
106
|
-
raise ValueError(
|
|
172
|
+
if not name.startswith(SPECIAL_VARIABLE_PREFIX):
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"Special variable names must start with '{SPECIAL_VARIABLE_PREFIX}': {name}"
|
|
175
|
+
)
|
|
107
176
|
name = name.upper()
|
|
108
177
|
match name:
|
|
109
|
-
case
|
|
110
|
-
return datetime.datetime.now(datetime.
|
|
111
|
-
case
|
|
112
|
-
return
|
|
113
|
-
|
|
178
|
+
case SpecialVars.CURRENT_TIME:
|
|
179
|
+
return datetime.datetime.now(datetime.UTC).timestamp()
|
|
180
|
+
case SpecialVars.SC_TIME:
|
|
181
|
+
return (
|
|
182
|
+
datetime.datetime.now(datetime.UTC).timestamp()
|
|
183
|
+
+ self.special_variables[SpecialVars.OWLT]
|
|
184
|
+
)
|
|
185
|
+
case SpecialVars.LOOP_COUNT:
|
|
114
186
|
if len(self.loop_stack) > 0:
|
|
115
187
|
return self.loop_stack[-1][2]
|
|
116
188
|
else:
|
|
@@ -118,91 +190,91 @@ class CstolVariables:
|
|
|
118
190
|
|
|
119
191
|
return self.special_variables.get(name, None)
|
|
120
192
|
|
|
121
|
-
class CstolScriptEngine(ScriptEngine):
|
|
122
193
|
|
|
194
|
+
class CstolScriptEngine(ScriptEngine):
|
|
123
195
|
ONE_YEAR_SECONDS = 31536000
|
|
124
196
|
|
|
125
197
|
# Dictionary of known CSTOL tokens - Unknown could be numbers or parts of telemetry names
|
|
126
198
|
KNOWN_TOKENS = {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
199
|
+
"AND": "and",
|
|
200
|
+
"OR": "or",
|
|
201
|
+
"XOR": "xor",
|
|
202
|
+
"NOT": "not",
|
|
203
|
+
"FOR": "",
|
|
204
|
+
"UNTIL": "",
|
|
205
|
+
"MOD": "%",
|
|
206
|
+
"**": "**",
|
|
207
|
+
"*": "*",
|
|
208
|
+
"(": "(",
|
|
209
|
+
")": ")",
|
|
210
|
+
"<": "<",
|
|
211
|
+
">": ">",
|
|
212
|
+
"<=": "<=",
|
|
213
|
+
">=": ">=",
|
|
214
|
+
"/=": "!=",
|
|
215
|
+
"=": "==",
|
|
216
|
+
"+": "+",
|
|
217
|
+
"-": "-",
|
|
218
|
+
"/": "/",
|
|
219
|
+
"TRUE": "True",
|
|
220
|
+
"FALSE": "False",
|
|
149
221
|
}
|
|
150
222
|
|
|
151
223
|
FUNCTION_TOKENS = {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
224
|
+
"SIN": "math.sin",
|
|
225
|
+
"COS": "math.cos",
|
|
226
|
+
"TAN": "math.tan",
|
|
227
|
+
"ASIN": "math.asin",
|
|
228
|
+
"ACOS": "math.acos",
|
|
229
|
+
"ATAN": "math.atan",
|
|
230
|
+
"SINH": "math.sinh",
|
|
231
|
+
"COSH": "math.cosh",
|
|
232
|
+
"TANH": "math.tanh",
|
|
233
|
+
"EXP": "math.exp",
|
|
234
|
+
"LOG": "math.log",
|
|
235
|
+
"LOG2": "math.log2",
|
|
236
|
+
"LOG10": "math.log10",
|
|
237
|
+
"SQRT": "math.sqrt",
|
|
238
|
+
"EVAL": "eval",
|
|
239
|
+
"GETENV": "os.getenv",
|
|
168
240
|
}
|
|
169
241
|
|
|
170
242
|
KNOWN_UNITS = [
|
|
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
|
-
|
|
243
|
+
"DN",
|
|
244
|
+
"A",
|
|
245
|
+
"C",
|
|
246
|
+
"CM",
|
|
247
|
+
"F",
|
|
248
|
+
"FT",
|
|
249
|
+
"G",
|
|
250
|
+
"GHZ",
|
|
251
|
+
"H",
|
|
252
|
+
"HZ",
|
|
253
|
+
"IN",
|
|
254
|
+
"J",
|
|
255
|
+
"K",
|
|
256
|
+
"KG",
|
|
257
|
+
"KM",
|
|
258
|
+
"KOHM",
|
|
259
|
+
"KV",
|
|
260
|
+
"KW",
|
|
261
|
+
"M",
|
|
262
|
+
"MA",
|
|
263
|
+
"MG",
|
|
264
|
+
"MHZ",
|
|
265
|
+
"MIN",
|
|
266
|
+
"MM",
|
|
267
|
+
"MOHM",
|
|
268
|
+
"MV",
|
|
269
|
+
"MW",
|
|
270
|
+
"OHM",
|
|
271
|
+
"PA",
|
|
272
|
+
"PSI",
|
|
273
|
+
"S",
|
|
274
|
+
"UA",
|
|
275
|
+
"UV",
|
|
276
|
+
"V",
|
|
277
|
+
"W",
|
|
206
278
|
]
|
|
207
279
|
|
|
208
280
|
def __init__(self, running_script):
|
|
@@ -210,7 +282,52 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
210
282
|
self.variables = CstolVariables()
|
|
211
283
|
self.saved_tokens = None
|
|
212
284
|
|
|
213
|
-
def
|
|
285
|
+
def timestamp_token_count(self, tokens, i):
|
|
286
|
+
"""
|
|
287
|
+
Returns how many tokens starting at index i form a single timestamp, or None if
|
|
288
|
+
they do not. The tokenizer splits timestamps on '/' and '-', so
|
|
289
|
+
yyyy/doy-HH:MM:SS arrives as ["yyyy", "/", "doy", "-", "HH:MM:SS"].
|
|
290
|
+
|
|
291
|
+
Each form requires at least one token to follow the timestamp, matching the
|
|
292
|
+
original bounds checks.
|
|
293
|
+
"""
|
|
294
|
+
remaining = len(tokens) - i
|
|
295
|
+
|
|
296
|
+
# yyyy/doy-HH:MM:SS
|
|
297
|
+
if (
|
|
298
|
+
remaining >= 5
|
|
299
|
+
and YEAR_PATTERN.match(tokens[i])
|
|
300
|
+
and tokens[i + 1] == "/"
|
|
301
|
+
and DAY_OF_YEAR_PATTERN.match(tokens[i + 2])
|
|
302
|
+
and tokens[i + 3] == "-"
|
|
303
|
+
and CLOCK_TIME_PATTERN.match(tokens[i + 4])
|
|
304
|
+
):
|
|
305
|
+
return 5
|
|
306
|
+
|
|
307
|
+
# /doy-HH:MM:SS or yyyy/-HH:MM:SS
|
|
308
|
+
if (
|
|
309
|
+
remaining >= 4
|
|
310
|
+
and CLOCK_TIME_PATTERN.match(tokens[i + 3])
|
|
311
|
+
and (
|
|
312
|
+
(tokens[i] == "/" and DAY_OF_YEAR_PATTERN.match(tokens[i + 1]))
|
|
313
|
+
or (YEAR_PATTERN.match(tokens[i]) and tokens[i + 1] == "/")
|
|
314
|
+
)
|
|
315
|
+
and tokens[i + 2] == "-"
|
|
316
|
+
):
|
|
317
|
+
return 4
|
|
318
|
+
|
|
319
|
+
# /-HH:MM:SS
|
|
320
|
+
if (
|
|
321
|
+
remaining >= 3
|
|
322
|
+
and tokens[i] == "/"
|
|
323
|
+
and tokens[i + 1] == "-"
|
|
324
|
+
and CLOCK_TIME_PATTERN.match(tokens[i + 2])
|
|
325
|
+
):
|
|
326
|
+
return 3
|
|
327
|
+
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def cstol_tokenizer(self, s, special_chars="()><+-*/=;,"):
|
|
214
331
|
tokens = self.tokenizer(s, special_chars)
|
|
215
332
|
|
|
216
333
|
# Reconstruct full timestamps of the format yyyy/doy-HH:MM:SS into single tokens
|
|
@@ -218,65 +335,28 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
218
335
|
reconstructed_tokens = []
|
|
219
336
|
i = 0
|
|
220
337
|
while i < len(tokens):
|
|
221
|
-
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
tokens[i + 3] == '-' and
|
|
227
|
-
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 4])):
|
|
228
|
-
# Reconstruct the full timestamp
|
|
229
|
-
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2] + tokens[i + 3] + tokens[i + 4]
|
|
230
|
-
reconstructed_tokens.append(timestamp)
|
|
231
|
-
i += 5 # Skip the next 4 tokens since we combined them
|
|
232
|
-
|
|
233
|
-
# Check if we have a potential timestamp pattern: /doy-HH:MM:SS
|
|
234
|
-
elif (i + 3 < len(tokens) and
|
|
235
|
-
tokens[i] == '/' and
|
|
236
|
-
re.match(r'^\d{1,3}$', tokens[i + 1]) and
|
|
237
|
-
tokens[i + 2] == '-' and
|
|
238
|
-
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 3])):
|
|
239
|
-
# Reconstruct the full timestamp
|
|
240
|
-
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2] + tokens[i + 3]
|
|
241
|
-
reconstructed_tokens.append(timestamp)
|
|
242
|
-
i += 4 # Skip the next 3 tokens since we combined them
|
|
243
|
-
|
|
244
|
-
# Check if we have a potential timestamp pattern: yyyy/-HH:MM:SS
|
|
245
|
-
elif (i + 3 < len(tokens) and
|
|
246
|
-
re.match(r'^\d{4}$', tokens[i]) and
|
|
247
|
-
tokens[i + 1] == '/' and
|
|
248
|
-
tokens[i + 2] == '-' and
|
|
249
|
-
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 3])):
|
|
250
|
-
# Reconstruct the full timestamp
|
|
251
|
-
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2] + tokens[i + 3]
|
|
252
|
-
reconstructed_tokens.append(timestamp)
|
|
253
|
-
i += 4 # Skip the next 3 tokens since we combined them
|
|
254
|
-
|
|
255
|
-
# Check if we have a potential timestamp pattern: yyyy/-HH:MM:SS
|
|
256
|
-
elif (i + 2 < len(tokens) and
|
|
257
|
-
tokens[i] == '/' and
|
|
258
|
-
tokens[i + 1] == '-' and
|
|
259
|
-
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 2])):
|
|
260
|
-
# Reconstruct the full timestamp
|
|
261
|
-
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2]
|
|
262
|
-
reconstructed_tokens.append(timestamp)
|
|
263
|
-
i += 3 # Skip the next 2 tokens since we combined them
|
|
338
|
+
timestamp_length = self.timestamp_token_count(tokens, i)
|
|
339
|
+
if timestamp_length:
|
|
340
|
+
reconstructed_tokens.append("".join(tokens[i : i + timestamp_length]))
|
|
341
|
+
i += timestamp_length
|
|
342
|
+
continue
|
|
264
343
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
344
|
+
# Recombine multi-part operator tokens
|
|
345
|
+
token = tokens[i]
|
|
346
|
+
if token in self.KNOWN_TOKENS and i + 1 < len(tokens):
|
|
347
|
+
next_token = tokens[i + 1]
|
|
348
|
+
if (
|
|
349
|
+
(token == "*" and next_token == "*")
|
|
350
|
+
or (token == "<" and next_token == "=")
|
|
351
|
+
or (token == ">" and next_token == "=")
|
|
352
|
+
or (token == "/" and next_token == "=")
|
|
353
|
+
):
|
|
354
|
+
reconstructed_tokens.append(token + next_token)
|
|
355
|
+
i += 2 # Skip the next token
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
reconstructed_tokens.append(token)
|
|
359
|
+
i += 1
|
|
280
360
|
|
|
281
361
|
return reconstructed_tokens
|
|
282
362
|
|
|
@@ -284,7 +364,8 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
284
364
|
"""
|
|
285
365
|
Builds a Python expression from the provided tokens.
|
|
286
366
|
|
|
287
|
-
Needs to handle all the CSTOL operators and syntax, converting them into
|
|
367
|
+
Needs to handle all the CSTOL operators and syntax, converting them into
|
|
368
|
+
valid Python syntax.
|
|
288
369
|
|
|
289
370
|
Args:
|
|
290
371
|
expression_tokens: List of tokens that form a valid Python expression
|
|
@@ -303,7 +384,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
303
384
|
previous_number = False
|
|
304
385
|
|
|
305
386
|
# Handle special variables
|
|
306
|
-
if token.startswith(
|
|
387
|
+
if token.startswith("$$"):
|
|
307
388
|
# Get the actual value of special variable
|
|
308
389
|
value = self.variables.get_special_variable(token)
|
|
309
390
|
if isinstance(value, str):
|
|
@@ -313,7 +394,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
313
394
|
continue
|
|
314
395
|
|
|
315
396
|
# Handle local variables
|
|
316
|
-
if token.startswith(
|
|
397
|
+
if token.startswith("$"):
|
|
317
398
|
# Get the actual value of local variable
|
|
318
399
|
value = self.variables.get_local_variable(token)
|
|
319
400
|
if value is None:
|
|
@@ -333,7 +414,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
333
414
|
# Handle CSTOL functions
|
|
334
415
|
if token.upper() in self.FUNCTION_TOKENS:
|
|
335
416
|
# Function tokens must be followed by ( or they will be considered just strings
|
|
336
|
-
if len(expression_tokens) > index + 1 and expression_tokens[index + 1] ==
|
|
417
|
+
if len(expression_tokens) > index + 1 and expression_tokens[index + 1] == "(":
|
|
337
418
|
token = self.FUNCTION_TOKENS[token.upper()]
|
|
338
419
|
final_tokens.append(token)
|
|
339
420
|
else:
|
|
@@ -349,16 +430,18 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
349
430
|
# Handle radix notation integers
|
|
350
431
|
# Allow hex digits (A-F) for all bases so X#FF and H#DEADBEEF parse;
|
|
351
432
|
# int() with the proper base validates the digits for each radix.
|
|
352
|
-
matches = re.match(r
|
|
433
|
+
matches = re.match(r"^([BODXHbodxh])#([0-9A-Fa-f]+)$", token)
|
|
353
434
|
if matches is not None:
|
|
354
435
|
radix_char = matches.group(1).upper()
|
|
355
436
|
digits = matches.group(2)
|
|
356
437
|
match radix_char:
|
|
357
|
-
case
|
|
438
|
+
case "H":
|
|
358
439
|
# Hex byte buffer (raw bytes rather than an integer)
|
|
359
|
-
final_tokens.append(
|
|
440
|
+
final_tokens.append(
|
|
441
|
+
str(int(digits, 16).to_bytes((len(digits) + 1) // 2, "big"))
|
|
442
|
+
)
|
|
360
443
|
case _:
|
|
361
|
-
base = {
|
|
444
|
+
base = {"B": 2, "O": 8, "D": 10, "X": 16}[radix_char]
|
|
362
445
|
final_tokens.append(str(int(digits, base)))
|
|
363
446
|
previous_number = True
|
|
364
447
|
continue
|
|
@@ -370,7 +453,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
370
453
|
# - Digits, optional decimal point and more digits
|
|
371
454
|
# - Optional scientific notation (e/E followed by optional sign and digits)
|
|
372
455
|
# - Ignores any alphabetic characters that follow
|
|
373
|
-
matches = re.match(r
|
|
456
|
+
matches = re.match(r"^([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)", token)
|
|
374
457
|
if matches is not None:
|
|
375
458
|
final_tokens.append(str(matches.group(1)))
|
|
376
459
|
previous_number = True
|
|
@@ -394,22 +477,30 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
394
477
|
token = final_tokens[i]
|
|
395
478
|
|
|
396
479
|
# Check for "RAW" pattern first: "RAW", "TARGET_NAME", "ITEM_NAME"
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
480
|
+
if (
|
|
481
|
+
token.upper() == '"RAW"'
|
|
482
|
+
and i + 2 < len(final_tokens)
|
|
483
|
+
and final_tokens[i + 1].startswith('"')
|
|
484
|
+
and final_tokens[i + 1].endswith('"')
|
|
485
|
+
and final_tokens[i + 2].startswith('"')
|
|
486
|
+
and final_tokens[i + 2].endswith('"')
|
|
487
|
+
):
|
|
401
488
|
target_name = final_tokens[i + 1][1:-1] # Remove quotes
|
|
402
|
-
item_name = final_tokens[i + 2][1:-1]
|
|
489
|
+
item_name = final_tokens[i + 2][1:-1] # Remove quotes
|
|
403
490
|
tlm_call = f'tlm("{target_name}", "LATEST", "{item_name}", type="RAW")'
|
|
404
491
|
processed_tokens.append(tlm_call)
|
|
405
492
|
i += 3 # Skip the next 2 tokens
|
|
406
493
|
|
|
407
494
|
# Check for regular pattern: "TARGET_NAME", "ITEM_NAME"
|
|
408
|
-
elif (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
495
|
+
elif (
|
|
496
|
+
token.startswith('"')
|
|
497
|
+
and token.endswith('"')
|
|
498
|
+
and i + 1 < len(final_tokens)
|
|
499
|
+
and final_tokens[i + 1].startswith('"')
|
|
500
|
+
and final_tokens[i + 1].endswith('"')
|
|
501
|
+
):
|
|
502
|
+
target_name = token[1:-1] # Remove quotes
|
|
503
|
+
item_name = final_tokens[i + 1][1:-1] # Remove quotes
|
|
413
504
|
tlm_call = f'tlm("{target_name}", "LATEST", "{item_name}")'
|
|
414
505
|
processed_tokens.append(tlm_call)
|
|
415
506
|
i += 2 # Skip the next token
|
|
@@ -420,15 +511,15 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
420
511
|
i += 1
|
|
421
512
|
|
|
422
513
|
# Join the processed tokens into a single string
|
|
423
|
-
return
|
|
514
|
+
return " ".join(processed_tokens)
|
|
424
515
|
|
|
425
516
|
# Combined regex pattern to match both formats
|
|
426
|
-
TIMESTAMP_PATTERN = r
|
|
517
|
+
TIMESTAMP_PATTERN = r"(?:(\d{4})?/(\d{1,3})?-)?(\d{0,2}):(\d{0,2}):(\d{1,2}\.?\d*)"
|
|
427
518
|
|
|
428
519
|
# Will convert a CSTOL Clock Time or Delta Time into floating point seconds
|
|
429
|
-
def parse_timestamp(self, timestamp, now
|
|
520
|
+
def parse_timestamp(self, timestamp, now=None):
|
|
430
521
|
if now is None:
|
|
431
|
-
now = datetime.datetime.now(datetime.
|
|
522
|
+
now = datetime.datetime.now(datetime.UTC)
|
|
432
523
|
|
|
433
524
|
matches = re.match(self.TIMESTAMP_PATTERN, timestamp)
|
|
434
525
|
if matches:
|
|
@@ -437,61 +528,54 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
437
528
|
hour = 0
|
|
438
529
|
if len(minute) == 0:
|
|
439
530
|
minute = 0
|
|
440
|
-
result = {
|
|
441
|
-
'hour': int(hour),
|
|
442
|
-
'minute': int(minute),
|
|
443
|
-
'second': float(second)
|
|
444
|
-
}
|
|
531
|
+
result = {"hour": int(hour), "minute": int(minute), "second": float(second)}
|
|
445
532
|
|
|
446
533
|
# Determine if / was present (meaning a clock time rather than delta time)
|
|
447
|
-
if
|
|
448
|
-
result[
|
|
534
|
+
if "/" in timestamp:
|
|
535
|
+
result["clock_time"] = True
|
|
449
536
|
else:
|
|
450
|
-
result[
|
|
537
|
+
result["clock_time"] = False
|
|
451
538
|
|
|
452
539
|
# Add year and day_of_year if they exist
|
|
453
540
|
if year is not None:
|
|
454
|
-
result[
|
|
455
|
-
elif result[
|
|
456
|
-
result[
|
|
541
|
+
result["year"] = int(year)
|
|
542
|
+
elif result["clock_time"]:
|
|
543
|
+
result["year"] = now.year
|
|
457
544
|
|
|
458
545
|
if day_of_year is not None:
|
|
459
|
-
result[
|
|
460
|
-
elif result[
|
|
461
|
-
result[
|
|
546
|
+
result["day_of_year"] = int(day_of_year)
|
|
547
|
+
elif result["clock_time"]:
|
|
548
|
+
result["day_of_year"] = now.timetuple().tm_yday
|
|
462
549
|
|
|
463
550
|
# Extract seconds and microseconds from fractional seconds
|
|
464
|
-
total_seconds = result[
|
|
551
|
+
total_seconds = result["second"]
|
|
465
552
|
seconds = int(total_seconds)
|
|
466
553
|
# Use round to handle floating point precision issues
|
|
467
554
|
microseconds = round((total_seconds - seconds) * 1000000)
|
|
468
555
|
|
|
469
|
-
if
|
|
556
|
+
if "year" in result and "day_of_year" in result:
|
|
470
557
|
# Create datetime from year and day of year
|
|
471
558
|
base_date = datetime.datetime(
|
|
472
|
-
year=result[
|
|
473
|
-
|
|
474
|
-
day=1,
|
|
475
|
-
tzinfo=datetime.timezone.utc
|
|
476
|
-
) + datetime.timedelta(days=result['day_of_year'] - 1)
|
|
559
|
+
year=result["year"], month=1, day=1, tzinfo=datetime.UTC
|
|
560
|
+
) + datetime.timedelta(days=result["day_of_year"] - 1)
|
|
477
561
|
|
|
478
562
|
return datetime.datetime(
|
|
479
563
|
year=base_date.year,
|
|
480
564
|
month=base_date.month,
|
|
481
565
|
day=base_date.day,
|
|
482
|
-
hour=result[
|
|
483
|
-
minute=result[
|
|
566
|
+
hour=result["hour"],
|
|
567
|
+
minute=result["minute"],
|
|
484
568
|
second=seconds,
|
|
485
569
|
microsecond=microseconds,
|
|
486
|
-
tzinfo=datetime.
|
|
570
|
+
tzinfo=datetime.UTC,
|
|
487
571
|
).timestamp()
|
|
488
572
|
else:
|
|
489
573
|
# Delta Time
|
|
490
574
|
return datetime.timedelta(
|
|
491
|
-
hours=result[
|
|
492
|
-
minutes=result[
|
|
575
|
+
hours=result["hour"],
|
|
576
|
+
minutes=result["minute"],
|
|
493
577
|
seconds=seconds,
|
|
494
|
-
microseconds=microseconds
|
|
578
|
+
microseconds=microseconds,
|
|
495
579
|
).total_seconds()
|
|
496
580
|
|
|
497
581
|
return None
|
|
@@ -534,7 +618,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
534
618
|
# Earlier OR tokens are boolean operators inside the condition.
|
|
535
619
|
for i in range(len(tokens) - 1, 0, -1):
|
|
536
620
|
if tokens[i - 1].upper() == "OR" and tokens[i].upper() in ("FOR", "UNTIL"):
|
|
537
|
-
return tokens[:i - 1], tokens[i:]
|
|
621
|
+
return tokens[: i - 1], tokens[i:]
|
|
538
622
|
return tokens, None
|
|
539
623
|
|
|
540
624
|
def split_vs_tokens_on_colon(self, tokens):
|
|
@@ -546,12 +630,12 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
546
630
|
"""
|
|
547
631
|
result = []
|
|
548
632
|
for token in tokens:
|
|
549
|
-
if
|
|
633
|
+
if ":" in token and not re.fullmatch(self.TIMESTAMP_PATTERN, token):
|
|
550
634
|
# Split non-timestamp tokens containing colons
|
|
551
|
-
parts = token.split(
|
|
635
|
+
parts = token.split(":")
|
|
552
636
|
for i, part in enumerate(parts):
|
|
553
637
|
if i > 0:
|
|
554
|
-
result.append(
|
|
638
|
+
result.append(":")
|
|
555
639
|
if part: # Only add non-empty parts
|
|
556
640
|
result.append(part)
|
|
557
641
|
else:
|
|
@@ -568,7 +652,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
568
652
|
result = None
|
|
569
653
|
try:
|
|
570
654
|
# Evaluate the expression and return the result
|
|
571
|
-
result = eval(python_expression, {
|
|
655
|
+
result = eval(python_expression, {"math": math, "os": os, "tlm": tlm})
|
|
572
656
|
except Exception as e:
|
|
573
657
|
raise ValueError(f"Error evaluating expression '{python_expression}': {e}")
|
|
574
658
|
return result
|
|
@@ -594,7 +678,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
594
678
|
|
|
595
679
|
variable = tokens[1]
|
|
596
680
|
question = tokens[2]
|
|
597
|
-
if (question.startswith('"') and question.endswith('"')) or (
|
|
681
|
+
if (question.startswith('"') and question.endswith('"')) or (
|
|
682
|
+
question.startswith("'") and question.endswith("'")
|
|
683
|
+
):
|
|
598
684
|
# Remove quotes
|
|
599
685
|
question = question[1:-1]
|
|
600
686
|
answer = ask_string(question)
|
|
@@ -602,7 +688,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
602
688
|
if len(answer) >= 2 and answer[0] == '"' and answer[-1] == '"':
|
|
603
689
|
# Remove quotes and don't uppercase and don't eval
|
|
604
690
|
answer = answer[1:-1]
|
|
605
|
-
elif answer.strip() ==
|
|
691
|
+
elif answer.strip() == "":
|
|
606
692
|
# Empty answer - store as-is without tokenizing/evaluating
|
|
607
693
|
pass
|
|
608
694
|
else:
|
|
@@ -617,28 +703,43 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
617
703
|
answer = answer.upper()
|
|
618
704
|
self.variables.set_local_variable(variable, answer)
|
|
619
705
|
|
|
706
|
+
def parse_format(self, expr, keyword, line_no):
|
|
707
|
+
"""
|
|
708
|
+
Splits a leading %<format> token off of expr. Returns the format specifier and
|
|
709
|
+
the remaining tokens, with a specifier of None when expr has no format token.
|
|
710
|
+
"""
|
|
711
|
+
if expr[0][0] != "%":
|
|
712
|
+
return None, expr
|
|
713
|
+
|
|
714
|
+
format_spec = expr[0][1:].upper()
|
|
715
|
+
if format_spec not in INTEGER_FORMATS and format_spec not in FLOAT_FORMATS:
|
|
716
|
+
raise ValueError(
|
|
717
|
+
f"Invalid format %'{format_spec}' in {keyword} command at line {line_no}"
|
|
718
|
+
)
|
|
719
|
+
if len(expr) < 2:
|
|
720
|
+
raise ValueError(
|
|
721
|
+
f"Missing value for format %'{format_spec}' in {keyword} command at line {line_no}"
|
|
722
|
+
)
|
|
723
|
+
return format_spec, expr[1:]
|
|
724
|
+
|
|
725
|
+
def apply_format(self, format_spec, value):
|
|
726
|
+
"""
|
|
727
|
+
Renders value using a CSTOL %<format> specifier, or str(value) when there is no
|
|
728
|
+
specifier. Integer formats convert the value to an integer first, float formats
|
|
729
|
+
convert it to a float.
|
|
730
|
+
"""
|
|
731
|
+
if format_spec is None:
|
|
732
|
+
return str(value)
|
|
733
|
+
if format_spec in INTEGER_FORMATS:
|
|
734
|
+
return format(int(value), INTEGER_FORMATS[format_spec])
|
|
735
|
+
return format(float(value), FLOAT_FORMATS[format_spec])
|
|
736
|
+
|
|
620
737
|
def handle_check(self, tokens, line_no):
|
|
621
738
|
expressions = self.extract_expressions(tokens[1:], ",")
|
|
622
739
|
for expr in expressions:
|
|
623
740
|
if len(expr) == 0:
|
|
624
741
|
raise ValueError(f"Empty expression in CHECK command at line {line_no}")
|
|
625
|
-
|
|
626
|
-
if expr[0][0] == '%':
|
|
627
|
-
# Format string
|
|
628
|
-
# %X or %x Output in hexadecimal values Value is converted to an integer prior to applying the format
|
|
629
|
-
# %O or %o Output in octal values Value is converted to an integer prior to applying the format
|
|
630
|
-
# %B or %b Output in binary values Value is converted to an integer prior to applying the format
|
|
631
|
-
# %I or %i Output in decimal values Value is converted to an integer prior to applying the format
|
|
632
|
-
# %D or %d Output in decimal values Value is converted to an integer prior to applying the format
|
|
633
|
-
# %F or %f Output in floating point values Default for integer or raw value
|
|
634
|
-
# %E or %e Output in floating point values Default for float or EU value
|
|
635
|
-
format = expr[0][1:].upper()
|
|
636
|
-
if format not in ['X', 'O', 'B', 'I', 'D', 'F', 'E']:
|
|
637
|
-
raise ValueError(f"Invalid format %'{format}' in CHECK command at line {line_no}")
|
|
638
|
-
if len(expr) < 2:
|
|
639
|
-
raise ValueError(f"Missing value for format %'{format}' in CHECK command at line {line_no}")
|
|
640
|
-
# Remove format from the expression
|
|
641
|
-
expr = expr[1:]
|
|
742
|
+
format_spec, expr = self.parse_format(expr, "CHECK", line_no)
|
|
642
743
|
|
|
643
744
|
# Check for VS
|
|
644
745
|
success = True
|
|
@@ -659,7 +760,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
659
760
|
range_start = self.evaluate_expression(colon_expressions[0])
|
|
660
761
|
range_end = self.evaluate_expression(colon_expressions[1])
|
|
661
762
|
if range_start > range_end:
|
|
662
|
-
raise ValueError(
|
|
763
|
+
raise ValueError(
|
|
764
|
+
f"Invalid VS range {range_start}:{range_end} at line {line_no}"
|
|
765
|
+
)
|
|
663
766
|
if (value < range_start) or (value > range_end):
|
|
664
767
|
success = False
|
|
665
768
|
fail_message = f"not in range {range_start}:{range_end}"
|
|
@@ -670,31 +773,15 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
670
773
|
success = False
|
|
671
774
|
fail_message = f"not equal to {vs_value}"
|
|
672
775
|
else:
|
|
673
|
-
|
|
776
|
+
# No VS but could be a relational or logical expression
|
|
674
777
|
source = " ".join(expr)
|
|
675
778
|
value = self.evaluate_expression(expr)
|
|
779
|
+
# Relational/logical expressions evaluate to a bool and must be True
|
|
780
|
+
if value is False:
|
|
781
|
+
success = False
|
|
782
|
+
fail_message = "expression is false"
|
|
676
783
|
|
|
677
|
-
formatted_value =
|
|
678
|
-
if format is None:
|
|
679
|
-
formatted_value = str(value)
|
|
680
|
-
elif format in ['X', 'O', 'B', 'I', 'D']:
|
|
681
|
-
# Convert to integer
|
|
682
|
-
value = int(value)
|
|
683
|
-
if format == 'X':
|
|
684
|
-
formatted_value = (f"{value:X}")
|
|
685
|
-
elif format == 'O':
|
|
686
|
-
formatted_value = (f"{value:o}")
|
|
687
|
-
elif format == 'B':
|
|
688
|
-
formatted_value = (f"{value:b}")
|
|
689
|
-
elif format in ['I', 'D']:
|
|
690
|
-
formatted_value = str(value)
|
|
691
|
-
elif format in ['F', 'E']:
|
|
692
|
-
# Convert to float
|
|
693
|
-
value = float(value)
|
|
694
|
-
if format == 'F':
|
|
695
|
-
formatted_value = f"{value:.6f}"
|
|
696
|
-
elif format == 'E':
|
|
697
|
-
formatted_value = f"{value:.6e}"
|
|
784
|
+
formatted_value = self.apply_format(format_spec, value)
|
|
698
785
|
|
|
699
786
|
if success:
|
|
700
787
|
print(f"CHECK SUCCESS: {source} = {formatted_value}")
|
|
@@ -704,37 +791,73 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
704
791
|
raise CheckError(message)
|
|
705
792
|
|
|
706
793
|
def handle_clear(self, tokens, line_no):
|
|
707
|
-
"""
|
|
708
|
-
if tokens[1].upper() ==
|
|
794
|
+
"""Only handles displays"""
|
|
795
|
+
if tokens[1].upper() == "ALL":
|
|
709
796
|
clear_all_screens()
|
|
710
797
|
else:
|
|
711
798
|
screen_name = tokens[1]
|
|
712
|
-
if (screen_name.startswith('"') and screen_name.endswith('"')) or (
|
|
799
|
+
if (screen_name.startswith('"') and screen_name.endswith('"')) or (
|
|
800
|
+
screen_name.startswith("'") and screen_name.endswith("'")
|
|
801
|
+
):
|
|
713
802
|
# Remove quotes
|
|
714
803
|
screen_name = screen_name[1:-1]
|
|
715
804
|
clear_screen(*screen_name.split())
|
|
716
805
|
|
|
717
806
|
def handle_cmd(self, tokens, line_no):
|
|
718
|
-
if tokens[0].upper() ==
|
|
807
|
+
if tokens[0].upper() == "NOW":
|
|
719
808
|
# Drop NOW
|
|
720
809
|
tokens = tokens[1:]
|
|
721
810
|
|
|
722
811
|
verb = tokens[0].upper()
|
|
723
812
|
tokens = tokens[1:]
|
|
724
813
|
match verb:
|
|
725
|
-
case
|
|
726
|
-
"
|
|
727
|
-
|
|
728
|
-
|
|
814
|
+
case (
|
|
815
|
+
"ACTIVATE"
|
|
816
|
+
| "ARM"
|
|
817
|
+
| "BOOT"
|
|
818
|
+
| "CHANGE"
|
|
819
|
+
| "CLOSE"
|
|
820
|
+
| "DISABLE"
|
|
821
|
+
| "DISARM"
|
|
822
|
+
| "DRIVE"
|
|
823
|
+
| "DUMP"
|
|
824
|
+
| "ENABLE"
|
|
825
|
+
| "FIRE"
|
|
826
|
+
| "FLYBACK"
|
|
827
|
+
| "FORCE"
|
|
828
|
+
| "GET"
|
|
829
|
+
| "HALT"
|
|
830
|
+
| "HOLD"
|
|
831
|
+
| "IGNORE"
|
|
832
|
+
| "INITIATE"
|
|
833
|
+
| "MOVE"
|
|
834
|
+
| "NOW"
|
|
835
|
+
| "OPEN"
|
|
836
|
+
| "PASS"
|
|
837
|
+
| "PERFORM"
|
|
838
|
+
| "RESET"
|
|
839
|
+
| "SELECT"
|
|
840
|
+
| "SET"
|
|
841
|
+
| "SLEW"
|
|
842
|
+
| "STEP"
|
|
843
|
+
| "TEST"
|
|
844
|
+
| "TOGGLE"
|
|
845
|
+
| "TURN"
|
|
846
|
+
| "USE"
|
|
847
|
+
):
|
|
729
848
|
# Handle all the weird verbs
|
|
730
849
|
if verb == "TURN" or verb == "FORCE":
|
|
731
850
|
# Expect ON or OFF to follow
|
|
732
|
-
if tokens[0].upper() ==
|
|
851
|
+
if tokens[0].upper() == "ON" or tokens[0].upper() == "OFF":
|
|
733
852
|
verb = verb + tokens[0].upper()
|
|
734
853
|
tokens = tokens[1:]
|
|
735
854
|
else:
|
|
736
|
-
raise ValueError(
|
|
855
|
+
raise ValueError(
|
|
856
|
+
f"TURN and FORCE must be followed by ON or OFF at line {line_no}"
|
|
857
|
+
)
|
|
737
858
|
case "CMD":
|
|
859
|
+
# CMD is the explicit form of a command, so there is no implied
|
|
860
|
+
# verb to fold into the parameters below
|
|
738
861
|
pass
|
|
739
862
|
|
|
740
863
|
# Now we need to discover any TO, BY, FROM, WITH clauses
|
|
@@ -796,24 +919,33 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
796
919
|
cmd(target_name, cmd_name, args)
|
|
797
920
|
|
|
798
921
|
def handle_declare(self, tokens, line_no):
|
|
799
|
-
"""
|
|
922
|
+
"""We ignore ranges and allowed value lists"""
|
|
800
923
|
# DECLARE mode variable-name = default-value [range value-list]
|
|
801
924
|
mode = tokens[1].upper()
|
|
802
|
-
if mode not in [
|
|
925
|
+
if mode not in ["INPUT", "VARIABLE", "CONSTANT"]:
|
|
803
926
|
raise ValueError(f"Invalid mode '{mode}' in DECLARE command at line {line_no}")
|
|
804
927
|
variable_name = tokens[2]
|
|
805
|
-
if not variable_name.startswith(
|
|
806
|
-
raise ValueError(
|
|
928
|
+
if not variable_name.startswith("$"):
|
|
929
|
+
raise ValueError(
|
|
930
|
+
f"Variable name must start with '$' in DECLARE command at line {line_no}"
|
|
931
|
+
)
|
|
807
932
|
equals = tokens[3]
|
|
808
|
-
if equals !=
|
|
809
|
-
raise ValueError(
|
|
810
|
-
|
|
933
|
+
if equals != "=":
|
|
934
|
+
raise ValueError(
|
|
935
|
+
f"Expected '=' after variable name in DECLARE command at line {line_no}"
|
|
936
|
+
)
|
|
811
937
|
default_value = self.evaluate_tokens([tokens[4]])[0]
|
|
938
|
+
if mode == "INPUT" and self.variables.has_local_variable(variable_name):
|
|
939
|
+
# INPUT variables are passed in by the PROC argument list, so the
|
|
940
|
+
# default value is only applied when an argument wasn't given
|
|
941
|
+
return
|
|
812
942
|
self.variables.set_local_variable(variable_name, default_value)
|
|
813
943
|
|
|
814
944
|
def handle_display(self, tokens, line_no):
|
|
815
945
|
screen_name = tokens[1]
|
|
816
|
-
if (screen_name.startswith('"') and screen_name.endswith('"')) or (
|
|
946
|
+
if (screen_name.startswith('"') and screen_name.endswith('"')) or (
|
|
947
|
+
screen_name.startswith("'") and screen_name.endswith("'")
|
|
948
|
+
):
|
|
817
949
|
# Remove quotes
|
|
818
950
|
screen_name = screen_name[1:-1]
|
|
819
951
|
display_screen(*screen_name.split())
|
|
@@ -822,32 +954,32 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
822
954
|
goto_endif = False
|
|
823
955
|
if len(tokens) > 1:
|
|
824
956
|
# 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
|
|
826
|
-
# execute the ELSEIF or just goto the next ENDIF
|
|
957
|
+
# The if_stack keeps track if an earlier if was successful and its
|
|
958
|
+
# value will determine if we execute the ELSEIF or just goto the next ENDIF
|
|
827
959
|
current_if = False
|
|
828
960
|
if len(self.variables.if_stack) > 0:
|
|
829
961
|
current_if = self.variables.if_stack[-1]
|
|
830
|
-
if tokens[0].upper() ==
|
|
962
|
+
if tokens[0].upper() == "ELSEIF":
|
|
831
963
|
if current_if:
|
|
832
964
|
goto_endif = True
|
|
833
965
|
else:
|
|
834
966
|
return self.handle_if(tokens, lines, line_no)
|
|
835
|
-
elif
|
|
967
|
+
elif tokens[0].upper() == "ELSE" and tokens[1].upper() == "IF":
|
|
836
968
|
if current_if:
|
|
837
969
|
goto_endif = True
|
|
838
970
|
else:
|
|
839
971
|
return self.handle_if(tokens[1:], lines, line_no)
|
|
840
972
|
|
|
841
|
-
if goto_endif or (len(tokens) == 1 and tokens[0].upper() ==
|
|
973
|
+
if goto_endif or (len(tokens) == 1 and tokens[0].upper() == "ELSE"):
|
|
842
974
|
# The only way we ever hit an ELSE is if we were in a successful block beforehand
|
|
843
975
|
# Therefore goto the ENDIF
|
|
844
976
|
self.variables.if_stack[-1] = True
|
|
845
977
|
depth = 1
|
|
846
978
|
for i in range(line_no, len(lines)):
|
|
847
979
|
next_line = lines[i].strip().upper()
|
|
848
|
-
if next_line.startswith(
|
|
980
|
+
if next_line.startswith("IF "):
|
|
849
981
|
depth += 1
|
|
850
|
-
elif next_line.startswith(
|
|
982
|
+
elif next_line.startswith(("ENDIF", "END IF")):
|
|
851
983
|
depth -= 1
|
|
852
984
|
if depth == 0:
|
|
853
985
|
return i + 1
|
|
@@ -856,29 +988,43 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
856
988
|
raise ValueError(f"handle_else called with unexpected tokens at line {line_no}")
|
|
857
989
|
|
|
858
990
|
def handle_end(self, tokens, line_no):
|
|
859
|
-
if len(tokens) == 1 and tokens[0].upper() ==
|
|
860
|
-
raise ValueError(
|
|
861
|
-
|
|
991
|
+
if len(tokens) == 1 and tokens[0].upper() == "END":
|
|
992
|
+
raise ValueError(
|
|
993
|
+
f"Unexpected END command at line {line_no}, "
|
|
994
|
+
"expected ENDIF, ENDLOOP, ENDMACRO, or ENDPROC"
|
|
995
|
+
)
|
|
996
|
+
elif (len(tokens) == 1 and tokens[0].upper() == "ENDIF") or (
|
|
997
|
+
tokens[0].upper() == "END" and tokens[1].upper() == "IF"
|
|
998
|
+
):
|
|
862
999
|
# END IF pops the IF stack
|
|
863
1000
|
self.variables.if_stack.pop()
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
loop_info[2] += 1
|
|
869
|
-
if loop_info[1] is not None:
|
|
870
|
-
# Counted loop, decrement the count
|
|
871
|
-
loop_info[1] -= 1
|
|
872
|
-
if loop_info[1] > 0:
|
|
873
|
-
return loop_info[0]
|
|
874
|
-
else:
|
|
875
|
-
self.variables.loop_stack.pop()
|
|
876
|
-
return line_no + 1 # Continue to the next line
|
|
877
|
-
else:
|
|
878
|
-
# Infinite loop, just continue to the start of the loop
|
|
879
|
-
return loop_info[0]
|
|
1001
|
+
elif (len(tokens) == 1 and tokens[0].upper() == "ENDLOOP") or (
|
|
1002
|
+
tokens[0].upper() == "END" and tokens[1].upper() == "LOOP"
|
|
1003
|
+
):
|
|
1004
|
+
return self.handle_end_loop(line_no)
|
|
880
1005
|
return line_no + 1
|
|
881
1006
|
|
|
1007
|
+
def handle_end_loop(self, line_no):
|
|
1008
|
+
"""
|
|
1009
|
+
Advances the innermost loop when its ENDLOOP is reached. Returns the top of the
|
|
1010
|
+
loop while iterations remain, otherwise the line after the ENDLOOP.
|
|
1011
|
+
"""
|
|
1012
|
+
if len(self.variables.loop_stack) == 0:
|
|
1013
|
+
return line_no + 1
|
|
1014
|
+
|
|
1015
|
+
loop_info = self.variables.loop_stack[-1]
|
|
1016
|
+
loop_info[2] += 1
|
|
1017
|
+
if loop_info[1] is None:
|
|
1018
|
+
# Infinite loop, just continue to the start of the loop
|
|
1019
|
+
return loop_info[0]
|
|
1020
|
+
|
|
1021
|
+
# Counted loop, decrement the count
|
|
1022
|
+
loop_info[1] -= 1
|
|
1023
|
+
if loop_info[1] > 0:
|
|
1024
|
+
return loop_info[0]
|
|
1025
|
+
self.variables.loop_stack.pop()
|
|
1026
|
+
return line_no + 1 # Continue to the next line
|
|
1027
|
+
|
|
882
1028
|
def handle_escape(self, _tokens, lines, line_no):
|
|
883
1029
|
# Find the matching ENDLOOP
|
|
884
1030
|
depth = 1
|
|
@@ -888,9 +1034,11 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
888
1034
|
words = lines[i].strip().upper().split()
|
|
889
1035
|
if not words:
|
|
890
1036
|
continue
|
|
891
|
-
if words[0] ==
|
|
1037
|
+
if words[0] == "LOOP":
|
|
892
1038
|
depth += 1
|
|
893
|
-
elif words[0] ==
|
|
1039
|
+
elif words[0] == "ENDLOOP" or (
|
|
1040
|
+
words[0] == "END" and len(words) > 1 and words[1] == "LOOP"
|
|
1041
|
+
):
|
|
894
1042
|
depth -= 1
|
|
895
1043
|
if depth == 0:
|
|
896
1044
|
if len(self.variables.loop_stack) > 0:
|
|
@@ -899,14 +1047,18 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
899
1047
|
raise ValueError(f"No matching ENDLOOP found for ESCAPE command at line {line_no}")
|
|
900
1048
|
|
|
901
1049
|
def handle_go(self, tokens, lines, line_no):
|
|
902
|
-
if tokens[0].upper() ==
|
|
903
|
-
|
|
1050
|
+
if tokens[0].upper() == "GOTO" or (
|
|
1051
|
+
len(tokens) > 1 and tokens[0].upper() == "GO" and tokens[1].upper() == "TO"
|
|
1052
|
+
):
|
|
1053
|
+
if (tokens[0].upper() == "GOTO" and len(tokens) < 2) or (
|
|
1054
|
+
tokens[0].upper() == "GO" and len(tokens) < 3
|
|
1055
|
+
):
|
|
904
1056
|
raise ValueError(f"Invalid GOTO command format at line {line_no}")
|
|
905
1057
|
label = None
|
|
906
|
-
if tokens[0].upper() ==
|
|
907
|
-
label = (tokens[1] +
|
|
1058
|
+
if tokens[0].upper() == "GOTO":
|
|
1059
|
+
label = (tokens[1] + ":").upper()
|
|
908
1060
|
else:
|
|
909
|
-
label = (tokens[2] +
|
|
1061
|
+
label = (tokens[2] + ":").upper()
|
|
910
1062
|
# Find the label in the lines
|
|
911
1063
|
for i in range(1, len(lines)):
|
|
912
1064
|
next_line = lines[i - 1].strip().upper()
|
|
@@ -921,7 +1073,10 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
921
1073
|
# Evaluate the expression and store the result in the variable
|
|
922
1074
|
result = self.evaluate_expression(expression_tokens)
|
|
923
1075
|
except Exception as e:
|
|
924
|
-
raise ValueError(
|
|
1076
|
+
raise ValueError(
|
|
1077
|
+
f"Error evaluating expression '{expression_tokens}' "
|
|
1078
|
+
f"in IF command at line {line_no}: {e}"
|
|
1079
|
+
)
|
|
925
1080
|
|
|
926
1081
|
if result:
|
|
927
1082
|
# Mark if as handled
|
|
@@ -933,21 +1088,18 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
933
1088
|
depth = 1
|
|
934
1089
|
for i in range(line_no, len(lines)):
|
|
935
1090
|
next_line = lines[i].strip().upper()
|
|
936
|
-
if next_line.startswith(
|
|
1091
|
+
if next_line.startswith("IF "):
|
|
937
1092
|
depth += 1
|
|
938
|
-
elif next_line.startswith(
|
|
1093
|
+
elif next_line.startswith(("ENDIF", "END IF")):
|
|
939
1094
|
depth -= 1
|
|
940
1095
|
if depth == 0:
|
|
941
1096
|
return i + 1
|
|
942
|
-
elif next_line.startswith(
|
|
943
|
-
if next_line.startswith(
|
|
1097
|
+
elif next_line.startswith("ELSE") and depth == 1:
|
|
1098
|
+
if next_line.startswith(("ELSEIF", "ELSE IF")):
|
|
944
1099
|
# Continue to the ELSE IF
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
# Regular ELSE - Continue to line after
|
|
949
|
-
if depth == 1:
|
|
950
|
-
return i + 2
|
|
1100
|
+
return i + 1
|
|
1101
|
+
# Regular ELSE - Continue to line after
|
|
1102
|
+
return i + 2
|
|
951
1103
|
|
|
952
1104
|
raise ValueError(f"No matching ENDIF or ELSE found for IF command at line {line_no}")
|
|
953
1105
|
|
|
@@ -958,14 +1110,16 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
958
1110
|
variable_name = tokens[1]
|
|
959
1111
|
item_name = None
|
|
960
1112
|
expression_tokens = None
|
|
961
|
-
if not variable_name.startswith(
|
|
962
|
-
if tokens[2] !=
|
|
1113
|
+
if not variable_name.startswith("$"):
|
|
1114
|
+
if tokens[2] != "=":
|
|
963
1115
|
# Global variable for set_tlm
|
|
964
1116
|
item_name = tokens[2]
|
|
965
1117
|
expression_tokens = tokens[4:]
|
|
966
1118
|
else:
|
|
967
|
-
raise ValueError(
|
|
968
|
-
|
|
1119
|
+
raise ValueError(
|
|
1120
|
+
f"Non-global variable name must start with '$' in LET command at line {line_no}"
|
|
1121
|
+
)
|
|
1122
|
+
elif tokens[2] == "=":
|
|
969
1123
|
# Local or Special variable
|
|
970
1124
|
expression_tokens = tokens[3:]
|
|
971
1125
|
else:
|
|
@@ -976,14 +1130,17 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
976
1130
|
# Evaluate the expression and store the result in the variable
|
|
977
1131
|
result = self.evaluate_expression(expression_tokens)
|
|
978
1132
|
except Exception as e:
|
|
979
|
-
raise ValueError(
|
|
1133
|
+
raise ValueError(
|
|
1134
|
+
f"Error evaluating expression '{expression_tokens}' "
|
|
1135
|
+
f"in LET command at line {line_no}: {e}"
|
|
1136
|
+
)
|
|
980
1137
|
|
|
981
1138
|
if item_name:
|
|
982
1139
|
if isinstance(result, str):
|
|
983
1140
|
set_tlm(f"{variable_name} LATEST {item_name} = '{result}'")
|
|
984
1141
|
else:
|
|
985
1142
|
set_tlm(f"{variable_name} LATEST {item_name} = {result}")
|
|
986
|
-
elif variable_name.startswith(
|
|
1143
|
+
elif variable_name.startswith("$$"):
|
|
987
1144
|
# Special variable
|
|
988
1145
|
self.variables.set_special_variable(variable_name, result)
|
|
989
1146
|
else:
|
|
@@ -1001,9 +1158,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1001
1158
|
raise ValueError(f"Invalid LOAD command format at line {line_no}")
|
|
1002
1159
|
location = expressions[0]
|
|
1003
1160
|
filename = expressions[1]
|
|
1161
|
+
# The evaluated location is unused, COSMOS sends the whole file to the interface
|
|
1004
1162
|
results = self.evaluate_expressions([interface_name, location, filename])
|
|
1005
1163
|
interface_name = results[0]
|
|
1006
|
-
location = results[1]
|
|
1007
1164
|
filename = results[2]
|
|
1008
1165
|
file = get_target_file(filename)
|
|
1009
1166
|
data = file.read()
|
|
@@ -1026,19 +1183,49 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1026
1183
|
# Variables
|
|
1027
1184
|
index = 0
|
|
1028
1185
|
for token in tokens[2:]:
|
|
1029
|
-
if token ==
|
|
1186
|
+
if token == ",":
|
|
1030
1187
|
continue
|
|
1031
|
-
elif token.startswith(
|
|
1032
|
-
|
|
1188
|
+
elif token.startswith("$"):
|
|
1189
|
+
value = os.getenv(f"CSTOL_ARG_{index}")
|
|
1190
|
+
if value is not None:
|
|
1191
|
+
# START stores the type alongside the value so the local
|
|
1192
|
+
# variable gets the type the caller passed rather than a string
|
|
1193
|
+
value = self.convert_arg(value, os.getenv(f"CSTOL_ARG_{index}_TYPE"))
|
|
1194
|
+
self.variables.set_local_variable(token, value)
|
|
1033
1195
|
index += 1
|
|
1034
1196
|
else:
|
|
1035
|
-
raise ValueError(
|
|
1197
|
+
raise ValueError(
|
|
1198
|
+
f"Invalid variable '{token}' in PROC command at line {line_no}"
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
@staticmethod
|
|
1202
|
+
def convert_arg(value, type_name):
|
|
1203
|
+
"""
|
|
1204
|
+
Converts a CSTOL_ARG environment variable string back into the type it
|
|
1205
|
+
had when START was called. Returns the string unchanged if the type is
|
|
1206
|
+
unknown or the value can't be converted.
|
|
1207
|
+
"""
|
|
1208
|
+
try:
|
|
1209
|
+
match type_name:
|
|
1210
|
+
case "int":
|
|
1211
|
+
return int(value)
|
|
1212
|
+
case "float":
|
|
1213
|
+
return float(value)
|
|
1214
|
+
case "complex":
|
|
1215
|
+
return complex(value)
|
|
1216
|
+
case "bool":
|
|
1217
|
+
return value == "True"
|
|
1218
|
+
case "NoneType":
|
|
1219
|
+
return None
|
|
1220
|
+
case _:
|
|
1221
|
+
return value
|
|
1222
|
+
except ValueError:
|
|
1223
|
+
return value
|
|
1036
1224
|
|
|
1037
1225
|
def handle_return(self, tokens, lines, line_no):
|
|
1038
|
-
if len(tokens) > 1:
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
return (len(lines) + 1)
|
|
1226
|
+
if len(tokens) > 1 and tokens[1].upper() == "ALL":
|
|
1227
|
+
raise StopScriptError
|
|
1228
|
+
return len(lines) + 1
|
|
1042
1229
|
|
|
1043
1230
|
def handle_run(self, tokens, line_no):
|
|
1044
1231
|
if len(tokens) < 2:
|
|
@@ -1049,7 +1236,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1049
1236
|
results = self.evaluate_expressions(expressions)
|
|
1050
1237
|
string_results = [('"' + str(result) + '"') for result in results]
|
|
1051
1238
|
string_results.insert(0, script)
|
|
1052
|
-
script =
|
|
1239
|
+
script = " ".join(string_results)
|
|
1053
1240
|
print(f"Running system call: {script}")
|
|
1054
1241
|
os.system(script)
|
|
1055
1242
|
|
|
@@ -1068,29 +1255,30 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1068
1255
|
# START proc-name [argument-list]
|
|
1069
1256
|
proc_name = tokens[1]
|
|
1070
1257
|
|
|
1071
|
-
if (proc_name.startswith('"') and proc_name.endswith('"')) or (
|
|
1258
|
+
if (proc_name.startswith('"') and proc_name.endswith('"')) or (
|
|
1259
|
+
proc_name.startswith("'") and proc_name.endswith("'")
|
|
1260
|
+
):
|
|
1072
1261
|
# Remove quotes
|
|
1073
1262
|
proc_name = proc_name[1:-1]
|
|
1074
1263
|
|
|
1075
|
-
args = []
|
|
1076
1264
|
if len(tokens) > 2:
|
|
1077
1265
|
# Handle argument list
|
|
1078
1266
|
expressions = self.extract_expressions(tokens[2:], ",")
|
|
1079
1267
|
results = self.evaluate_expressions(expressions)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
os.environ[f"CSTOL_ARG_{index}"] =
|
|
1268
|
+
for index, result in enumerate(results):
|
|
1269
|
+
# Set the environment variables for the argument and its type
|
|
1270
|
+
os.environ[f"CSTOL_ARG_{index}"] = str(result)
|
|
1271
|
+
os.environ[f"CSTOL_ARG_{index}_TYPE"] = type(result).__name__
|
|
1084
1272
|
start(proc_name, bind_variables=False)
|
|
1085
1273
|
|
|
1086
1274
|
def handle_switch(self, tokens, line_no):
|
|
1087
1275
|
action = tokens[1].upper()
|
|
1088
|
-
if action not in [
|
|
1276
|
+
if action not in ["ON", "OFF"]:
|
|
1089
1277
|
raise ValueError(f"Invalid SWITCH action '{action}' at line {line_no}")
|
|
1090
1278
|
interface_name = tokens[2]
|
|
1091
|
-
if action ==
|
|
1279
|
+
if action == "ON":
|
|
1092
1280
|
connect_interface(interface_name)
|
|
1093
|
-
elif action ==
|
|
1281
|
+
elif action == "OFF":
|
|
1094
1282
|
disconnect_interface(interface_name)
|
|
1095
1283
|
|
|
1096
1284
|
def handle_wait(self, tokens, line_no):
|
|
@@ -1115,26 +1303,36 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1115
1303
|
python_expression = self.build_python_expression(condition_tokens)
|
|
1116
1304
|
if timeout_tokens is not None:
|
|
1117
1305
|
# Timeout given with "OR FOR" or "OR UNTIL"
|
|
1118
|
-
seconds = self.evaluate_expression(timeout_tokens[1:])
|
|
1306
|
+
seconds = self.evaluate_expression(timeout_tokens[1:]) # Drop FOR or UNTIL token
|
|
1119
1307
|
if isinstance(seconds, (int, float, complex)) and not isinstance(seconds, bool):
|
|
1120
1308
|
if seconds > self.ONE_YEAR_SECONDS:
|
|
1121
1309
|
now = datetime.datetime.now().timestamp()
|
|
1122
1310
|
seconds = seconds - now
|
|
1123
1311
|
if seconds < 0:
|
|
1124
1312
|
seconds = 0.0
|
|
1125
|
-
result = wait_expression(
|
|
1313
|
+
result = wait_expression(
|
|
1314
|
+
python_expression,
|
|
1315
|
+
seconds,
|
|
1316
|
+
self.variables.get_special_variable(SpecialVars.CHECK_INTERVAL),
|
|
1317
|
+
globals={"math": math, "os": os, "tlm": tlm},
|
|
1318
|
+
)
|
|
1126
1319
|
if result:
|
|
1127
|
-
self.variables.set_special_variable(
|
|
1320
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "NO_ERROR")
|
|
1128
1321
|
else:
|
|
1129
|
-
self.variables.set_special_variable(
|
|
1322
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "TIME_OUT")
|
|
1130
1323
|
else:
|
|
1131
1324
|
raise ValueError(f"Invalid timestamp format at line {line_no}")
|
|
1132
1325
|
else:
|
|
1133
|
-
result = wait_expression(
|
|
1326
|
+
result = wait_expression(
|
|
1327
|
+
python_expression,
|
|
1328
|
+
1000000000,
|
|
1329
|
+
self.variables.get_special_variable(SpecialVars.CHECK_INTERVAL),
|
|
1330
|
+
globals={"math": math, "os": os, "tlm": tlm},
|
|
1331
|
+
) # Effective infinite wait
|
|
1134
1332
|
if result:
|
|
1135
|
-
self.variables.set_special_variable(
|
|
1333
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "NO_ERROR")
|
|
1136
1334
|
else:
|
|
1137
|
-
self.variables.set_special_variable(
|
|
1335
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "TIME_OUT")
|
|
1138
1336
|
|
|
1139
1337
|
def handle_write(self, tokens, line_no):
|
|
1140
1338
|
expressions = self.extract_expressions(tokens[1:], ",")
|
|
@@ -1142,48 +1340,13 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1142
1340
|
for expr in expressions:
|
|
1143
1341
|
if len(expr) == 0:
|
|
1144
1342
|
raise ValueError(f"Empty expression in WRITE command at line {line_no}")
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
# Format string
|
|
1148
|
-
# %X or %x Output in hexadecimal values Value is converted to an integer prior to applying the format
|
|
1149
|
-
# %O or %o Output in octal values Value is converted to an integer prior to applying the format
|
|
1150
|
-
# %B or %b Output in binary values Value is converted to an integer prior to applying the format
|
|
1151
|
-
# %I or %i Output in decimal values Value is converted to an integer prior to applying the format
|
|
1152
|
-
# %D or %d Output in decimal values Value is converted to an integer prior to applying the format
|
|
1153
|
-
# %F or %f Output in floating point values Default for integer or raw value
|
|
1154
|
-
# %E or %e Output in floating point values Default for float or EU value
|
|
1155
|
-
format = expr[0][1:].upper()
|
|
1156
|
-
if format not in ['X', 'O', 'B', 'I', 'D', 'F', 'E']:
|
|
1157
|
-
raise ValueError(f"Invalid format %'{format}' in WRITE command at line {line_no}")
|
|
1158
|
-
if len(expr) < 2:
|
|
1159
|
-
raise ValueError(f"Missing value for format %'{format}' in WRITE command at line {line_no}")
|
|
1160
|
-
value = self.evaluate_expression(expr[1:])
|
|
1161
|
-
if format in ['X', 'O', 'B', 'I', 'D']:
|
|
1162
|
-
# Convert to integer
|
|
1163
|
-
value = int(value)
|
|
1164
|
-
if format == 'X':
|
|
1165
|
-
result = (f"{value:X}")
|
|
1166
|
-
elif format == 'O':
|
|
1167
|
-
result = (f"{value:o}")
|
|
1168
|
-
elif format == 'B':
|
|
1169
|
-
result = (f"{value:b}")
|
|
1170
|
-
elif format in ['I', 'D']:
|
|
1171
|
-
result = str(value)
|
|
1172
|
-
elif format in ['F', 'E']:
|
|
1173
|
-
# Convert to float
|
|
1174
|
-
value = float(value)
|
|
1175
|
-
if format == 'F':
|
|
1176
|
-
result = f"{value:.6f}"
|
|
1177
|
-
elif format == 'E':
|
|
1178
|
-
result = f"{value:.6e}"
|
|
1179
|
-
else:
|
|
1180
|
-
result = str(self.evaluate_expression(expr))
|
|
1181
|
-
results.append(result)
|
|
1343
|
+
format_spec, expr = self.parse_format(expr, "WRITE", line_no)
|
|
1344
|
+
results.append(self.apply_format(format_spec, self.evaluate_expression(expr)))
|
|
1182
1345
|
# Join the results with spaces
|
|
1183
|
-
output =
|
|
1346
|
+
output = " ".join(results)
|
|
1184
1347
|
print(output)
|
|
1185
1348
|
|
|
1186
|
-
def run_text(self, text, filename
|
|
1349
|
+
def run_text(self, text, filename=None, line_no=1, end_line_no=None, bind_variables=False):
|
|
1187
1350
|
saved_variables = self.variables
|
|
1188
1351
|
try:
|
|
1189
1352
|
if not bind_variables:
|
|
@@ -1204,20 +1367,20 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1204
1367
|
if self.saved_tokens:
|
|
1205
1368
|
tokens = self.saved_tokens + tokens
|
|
1206
1369
|
self.saved_tokens = None
|
|
1207
|
-
if tokens[-1] ==
|
|
1208
|
-
self.saved_tokens = tokens[:-1]
|
|
1370
|
+
if tokens[-1] == "&":
|
|
1371
|
+
self.saved_tokens = tokens[:-1] # Everything before the '&' is saved for the next line
|
|
1209
1372
|
return line_no + 1 # Skip to the next line
|
|
1210
1373
|
|
|
1211
1374
|
# Remove any trailing comments
|
|
1212
|
-
if
|
|
1213
|
-
comment_index = tokens.index(
|
|
1375
|
+
if ";" in tokens:
|
|
1376
|
+
comment_index = tokens.index(";")
|
|
1214
1377
|
tokens = tokens[:comment_index]
|
|
1215
1378
|
|
|
1216
1379
|
if len(tokens) != 0:
|
|
1217
1380
|
keyword = tokens[0].upper()
|
|
1218
1381
|
|
|
1219
1382
|
# Handle labels
|
|
1220
|
-
if keyword.endswith(
|
|
1383
|
+
if keyword.endswith(":"):
|
|
1221
1384
|
if len(tokens) > 1:
|
|
1222
1385
|
tokens = tokens[1:]
|
|
1223
1386
|
keyword = tokens[0].upper()
|
|
@@ -1228,20 +1391,75 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1228
1391
|
case "ASK":
|
|
1229
1392
|
self.handle_ask(tokens, line_no)
|
|
1230
1393
|
case "BEGIN":
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
"
|
|
1394
|
+
# BEGIN only marks the start of the procedure body
|
|
1395
|
+
pass
|
|
1396
|
+
case (
|
|
1397
|
+
"CANCEL"
|
|
1398
|
+
| "CHECKPOINT"
|
|
1399
|
+
| "COMMIT"
|
|
1400
|
+
| "COMPILE"
|
|
1401
|
+
| "CSTOL"
|
|
1402
|
+
| "DECOMPILE"
|
|
1403
|
+
| "DEFINE"
|
|
1404
|
+
| "DELETE"
|
|
1405
|
+
| "FLUSH"
|
|
1406
|
+
| "INSERT"
|
|
1407
|
+
| "LOCK"
|
|
1408
|
+
| "MACRO"
|
|
1409
|
+
| "RECORD"
|
|
1410
|
+
| "REPORT"
|
|
1411
|
+
| "RESTORE"
|
|
1412
|
+
| "RETREIVE"
|
|
1413
|
+
| "RETRY"
|
|
1414
|
+
| "SHOW"
|
|
1415
|
+
| "SNAP"
|
|
1416
|
+
| "STOP"
|
|
1417
|
+
| "UNDEFINE"
|
|
1418
|
+
| "UNLOCK"
|
|
1419
|
+
| "UPDATE"
|
|
1420
|
+
| "ROUTE"
|
|
1421
|
+
):
|
|
1235
1422
|
# These keywords are noops for this CSTOL script engine
|
|
1236
1423
|
print(f"Ignoring Unsupported Keyword: {keyword}")
|
|
1237
1424
|
case "CHECK":
|
|
1238
1425
|
self.handle_check(tokens, line_no)
|
|
1239
1426
|
case "CLEAR":
|
|
1240
1427
|
self.handle_clear(tokens, line_no)
|
|
1241
|
-
case
|
|
1242
|
-
"
|
|
1243
|
-
|
|
1244
|
-
|
|
1428
|
+
case (
|
|
1429
|
+
"ACTIVATE"
|
|
1430
|
+
| "ARM"
|
|
1431
|
+
| "BOOT"
|
|
1432
|
+
| "CHANGE"
|
|
1433
|
+
| "CLOSE"
|
|
1434
|
+
| "CMD"
|
|
1435
|
+
| "DISABLE"
|
|
1436
|
+
| "DISARM"
|
|
1437
|
+
| "DRIVE"
|
|
1438
|
+
| "DUMP"
|
|
1439
|
+
| "ENABLE"
|
|
1440
|
+
| "FIRE"
|
|
1441
|
+
| "FLYBACK"
|
|
1442
|
+
| "FORCE"
|
|
1443
|
+
| "GET"
|
|
1444
|
+
| "HALT"
|
|
1445
|
+
| "HOLD"
|
|
1446
|
+
| "IGNORE"
|
|
1447
|
+
| "INITIATE"
|
|
1448
|
+
| "MOVE"
|
|
1449
|
+
| "NOW"
|
|
1450
|
+
| "OPEN"
|
|
1451
|
+
| "PASS"
|
|
1452
|
+
| "PERFORM"
|
|
1453
|
+
| "RESET"
|
|
1454
|
+
| "SELECT"
|
|
1455
|
+
| "SET"
|
|
1456
|
+
| "SLEW"
|
|
1457
|
+
| "STEP"
|
|
1458
|
+
| "TEST"
|
|
1459
|
+
| "TOGGLE"
|
|
1460
|
+
| "TURN"
|
|
1461
|
+
| "USE"
|
|
1462
|
+
):
|
|
1245
1463
|
self.handle_cmd(tokens, line_no)
|
|
1246
1464
|
case "DECLARE":
|
|
1247
1465
|
self.handle_declare(tokens, line_no)
|