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