openc3-cosmos-script-engine-cstol 1.1.0 → 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 +599 -402
- 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
|
|
@@ -514,6 +590,29 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
514
590
|
expressions.append(expression)
|
|
515
591
|
return expressions
|
|
516
592
|
|
|
593
|
+
def _split_wait_timeout(self, tokens):
|
|
594
|
+
"""Split WAIT body tokens into (condition_tokens, timeout_tokens | None).
|
|
595
|
+
|
|
596
|
+
Scans from the end of the token list looking for the last ``OR``
|
|
597
|
+
that is immediately followed by ``FOR`` or ``UNTIL``. That ``OR``
|
|
598
|
+
marks the boundary between the boolean condition and the timeout
|
|
599
|
+
clause.
|
|
600
|
+
|
|
601
|
+
Returns
|
|
602
|
+
-------
|
|
603
|
+
(condition_tokens, timeout_tokens)
|
|
604
|
+
*timeout_tokens* includes the ``FOR``/``UNTIL`` keyword and the
|
|
605
|
+
value that follows. If no ``OR FOR`` / ``OR UNTIL`` sequence is
|
|
606
|
+
found the full token list is returned as the condition and
|
|
607
|
+
*timeout_tokens* is ``None``.
|
|
608
|
+
"""
|
|
609
|
+
# Walk backwards so we find the last OR FOR / OR UNTIL (before the timeout).
|
|
610
|
+
# Earlier OR tokens are boolean operators inside the condition.
|
|
611
|
+
for i in range(len(tokens) - 1, 0, -1):
|
|
612
|
+
if tokens[i - 1].upper() == "OR" and tokens[i].upper() in ("FOR", "UNTIL"):
|
|
613
|
+
return tokens[: i - 1], tokens[i:]
|
|
614
|
+
return tokens, None
|
|
615
|
+
|
|
517
616
|
def split_vs_tokens_on_colon(self, tokens):
|
|
518
617
|
"""
|
|
519
618
|
Split tokens on colons for VS clause processing, but preserve timestamps.
|
|
@@ -523,12 +622,12 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
523
622
|
"""
|
|
524
623
|
result = []
|
|
525
624
|
for token in tokens:
|
|
526
|
-
if
|
|
625
|
+
if ":" in token and not re.fullmatch(self.TIMESTAMP_PATTERN, token):
|
|
527
626
|
# Split non-timestamp tokens containing colons
|
|
528
|
-
parts = token.split(
|
|
627
|
+
parts = token.split(":")
|
|
529
628
|
for i, part in enumerate(parts):
|
|
530
629
|
if i > 0:
|
|
531
|
-
result.append(
|
|
630
|
+
result.append(":")
|
|
532
631
|
if part: # Only add non-empty parts
|
|
533
632
|
result.append(part)
|
|
534
633
|
else:
|
|
@@ -545,7 +644,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
545
644
|
result = None
|
|
546
645
|
try:
|
|
547
646
|
# Evaluate the expression and return the result
|
|
548
|
-
result = eval(python_expression, {
|
|
647
|
+
result = eval(python_expression, {"math": math, "os": os, "tlm": tlm})
|
|
549
648
|
except Exception as e:
|
|
550
649
|
raise ValueError(f"Error evaluating expression '{python_expression}': {e}")
|
|
551
650
|
return result
|
|
@@ -571,7 +670,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
571
670
|
|
|
572
671
|
variable = tokens[1]
|
|
573
672
|
question = tokens[2]
|
|
574
|
-
if (question.startswith('"') and question.endswith('"')) or (
|
|
673
|
+
if (question.startswith('"') and question.endswith('"')) or (
|
|
674
|
+
question.startswith("'") and question.endswith("'")
|
|
675
|
+
):
|
|
575
676
|
# Remove quotes
|
|
576
677
|
question = question[1:-1]
|
|
577
678
|
answer = ask_string(question)
|
|
@@ -579,7 +680,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
579
680
|
if len(answer) >= 2 and answer[0] == '"' and answer[-1] == '"':
|
|
580
681
|
# Remove quotes and don't uppercase and don't eval
|
|
581
682
|
answer = answer[1:-1]
|
|
582
|
-
elif answer.strip() ==
|
|
683
|
+
elif answer.strip() == "":
|
|
583
684
|
# Empty answer - store as-is without tokenizing/evaluating
|
|
584
685
|
pass
|
|
585
686
|
else:
|
|
@@ -594,28 +695,43 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
594
695
|
answer = answer.upper()
|
|
595
696
|
self.variables.set_local_variable(variable, answer)
|
|
596
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
|
+
|
|
597
729
|
def handle_check(self, tokens, line_no):
|
|
598
730
|
expressions = self.extract_expressions(tokens[1:], ",")
|
|
599
731
|
for expr in expressions:
|
|
600
732
|
if len(expr) == 0:
|
|
601
733
|
raise ValueError(f"Empty expression in CHECK command at line {line_no}")
|
|
602
|
-
|
|
603
|
-
if expr[0][0] == '%':
|
|
604
|
-
# Format string
|
|
605
|
-
# %X or %x Output in hexadecimal values Value is converted to an integer prior to applying the format
|
|
606
|
-
# %O or %o Output in octal values Value is converted to an integer prior to applying the format
|
|
607
|
-
# %B or %b Output in binary values Value is converted to an integer prior to applying the format
|
|
608
|
-
# %I or %i Output in decimal values Value is converted to an integer prior to applying the format
|
|
609
|
-
# %D or %d Output in decimal values Value is converted to an integer prior to applying the format
|
|
610
|
-
# %F or %f Output in floating point values Default for integer or raw value
|
|
611
|
-
# %E or %e Output in floating point values Default for float or EU value
|
|
612
|
-
format = expr[0][1:].upper()
|
|
613
|
-
if format not in ['X', 'O', 'B', 'I', 'D', 'F', 'E']:
|
|
614
|
-
raise ValueError(f"Invalid format %'{format}' in CHECK command at line {line_no}")
|
|
615
|
-
if len(expr) < 2:
|
|
616
|
-
raise ValueError(f"Missing value for format %'{format}' in CHECK command at line {line_no}")
|
|
617
|
-
# Remove format from the expression
|
|
618
|
-
expr = expr[1:]
|
|
734
|
+
format_spec, expr = self.parse_format(expr, "CHECK", line_no)
|
|
619
735
|
|
|
620
736
|
# Check for VS
|
|
621
737
|
success = True
|
|
@@ -636,7 +752,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
636
752
|
range_start = self.evaluate_expression(colon_expressions[0])
|
|
637
753
|
range_end = self.evaluate_expression(colon_expressions[1])
|
|
638
754
|
if range_start > range_end:
|
|
639
|
-
raise ValueError(
|
|
755
|
+
raise ValueError(
|
|
756
|
+
f"Invalid VS range {range_start}:{range_end} at line {line_no}"
|
|
757
|
+
)
|
|
640
758
|
if (value < range_start) or (value > range_end):
|
|
641
759
|
success = False
|
|
642
760
|
fail_message = f"not in range {range_start}:{range_end}"
|
|
@@ -647,31 +765,11 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
647
765
|
success = False
|
|
648
766
|
fail_message = f"not equal to {vs_value}"
|
|
649
767
|
else:
|
|
650
|
-
|
|
768
|
+
# No VS - Just a print check
|
|
651
769
|
source = " ".join(expr)
|
|
652
770
|
value = self.evaluate_expression(expr)
|
|
653
771
|
|
|
654
|
-
formatted_value =
|
|
655
|
-
if format is None:
|
|
656
|
-
formatted_value = str(value)
|
|
657
|
-
elif format in ['X', 'O', 'B', 'I', 'D']:
|
|
658
|
-
# Convert to integer
|
|
659
|
-
value = int(value)
|
|
660
|
-
if format == 'X':
|
|
661
|
-
formatted_value = (f"{value:X}")
|
|
662
|
-
elif format == 'O':
|
|
663
|
-
formatted_value = (f"{value:o}")
|
|
664
|
-
elif format == 'B':
|
|
665
|
-
formatted_value = (f"{value:b}")
|
|
666
|
-
elif format in ['I', 'D']:
|
|
667
|
-
formatted_value = str(value)
|
|
668
|
-
elif format in ['F', 'E']:
|
|
669
|
-
# Convert to float
|
|
670
|
-
value = float(value)
|
|
671
|
-
if format == 'F':
|
|
672
|
-
formatted_value = f"{value:.6f}"
|
|
673
|
-
elif format == 'E':
|
|
674
|
-
formatted_value = f"{value:.6e}"
|
|
772
|
+
formatted_value = self.apply_format(format_spec, value)
|
|
675
773
|
|
|
676
774
|
if success:
|
|
677
775
|
print(f"CHECK SUCCESS: {source} = {formatted_value}")
|
|
@@ -681,37 +779,73 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
681
779
|
raise CheckError(message)
|
|
682
780
|
|
|
683
781
|
def handle_clear(self, tokens, line_no):
|
|
684
|
-
"""
|
|
685
|
-
if tokens[1].upper() ==
|
|
782
|
+
"""Only handles displays"""
|
|
783
|
+
if tokens[1].upper() == "ALL":
|
|
686
784
|
clear_all_screens()
|
|
687
785
|
else:
|
|
688
786
|
screen_name = tokens[1]
|
|
689
|
-
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
|
+
):
|
|
690
790
|
# Remove quotes
|
|
691
791
|
screen_name = screen_name[1:-1]
|
|
692
792
|
clear_screen(*screen_name.split())
|
|
693
793
|
|
|
694
794
|
def handle_cmd(self, tokens, line_no):
|
|
695
|
-
if tokens[0].upper() ==
|
|
795
|
+
if tokens[0].upper() == "NOW":
|
|
696
796
|
# Drop NOW
|
|
697
797
|
tokens = tokens[1:]
|
|
698
798
|
|
|
699
799
|
verb = tokens[0].upper()
|
|
700
800
|
tokens = tokens[1:]
|
|
701
801
|
match verb:
|
|
702
|
-
case
|
|
703
|
-
"
|
|
704
|
-
|
|
705
|
-
|
|
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
|
+
):
|
|
706
836
|
# Handle all the weird verbs
|
|
707
837
|
if verb == "TURN" or verb == "FORCE":
|
|
708
838
|
# Expect ON or OFF to follow
|
|
709
|
-
if tokens[0].upper() ==
|
|
839
|
+
if tokens[0].upper() == "ON" or tokens[0].upper() == "OFF":
|
|
710
840
|
verb = verb + tokens[0].upper()
|
|
711
841
|
tokens = tokens[1:]
|
|
712
842
|
else:
|
|
713
|
-
raise ValueError(
|
|
843
|
+
raise ValueError(
|
|
844
|
+
f"TURN and FORCE must be followed by ON or OFF at line {line_no}"
|
|
845
|
+
)
|
|
714
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
|
|
715
849
|
pass
|
|
716
850
|
|
|
717
851
|
# Now we need to discover any TO, BY, FROM, WITH clauses
|
|
@@ -773,24 +907,29 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
773
907
|
cmd(target_name, cmd_name, args)
|
|
774
908
|
|
|
775
909
|
def handle_declare(self, tokens, line_no):
|
|
776
|
-
"""
|
|
910
|
+
"""We ignore ranges and allowed value lists"""
|
|
777
911
|
# DECLARE mode variable-name = default-value [range value-list]
|
|
778
912
|
mode = tokens[1].upper()
|
|
779
|
-
if mode not in [
|
|
913
|
+
if mode not in ["INPUT", "VARIABLE", "CONSTANT"]:
|
|
780
914
|
raise ValueError(f"Invalid mode '{mode}' in DECLARE command at line {line_no}")
|
|
781
915
|
variable_name = tokens[2]
|
|
782
|
-
if not variable_name.startswith(
|
|
783
|
-
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
|
+
)
|
|
784
920
|
equals = tokens[3]
|
|
785
|
-
if equals !=
|
|
786
|
-
raise ValueError(
|
|
787
|
-
|
|
921
|
+
if equals != "=":
|
|
922
|
+
raise ValueError(
|
|
923
|
+
f"Expected '=' after variable name in DECLARE command at line {line_no}"
|
|
924
|
+
)
|
|
788
925
|
default_value = self.evaluate_tokens([tokens[4]])[0]
|
|
789
926
|
self.variables.set_local_variable(variable_name, default_value)
|
|
790
927
|
|
|
791
928
|
def handle_display(self, tokens, line_no):
|
|
792
929
|
screen_name = tokens[1]
|
|
793
|
-
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
|
+
):
|
|
794
933
|
# Remove quotes
|
|
795
934
|
screen_name = screen_name[1:-1]
|
|
796
935
|
display_screen(*screen_name.split())
|
|
@@ -799,32 +938,32 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
799
938
|
goto_endif = False
|
|
800
939
|
if len(tokens) > 1:
|
|
801
940
|
# ELSE IF is tricky - We can hit these after a successful IF, or an unsuccessful IF
|
|
802
|
-
# The if_stack keeps track if an earlier if was successful and its
|
|
803
|
-
# 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
|
|
804
943
|
current_if = False
|
|
805
944
|
if len(self.variables.if_stack) > 0:
|
|
806
945
|
current_if = self.variables.if_stack[-1]
|
|
807
|
-
if tokens[0].upper() ==
|
|
946
|
+
if tokens[0].upper() == "ELSEIF":
|
|
808
947
|
if current_if:
|
|
809
948
|
goto_endif = True
|
|
810
949
|
else:
|
|
811
950
|
return self.handle_if(tokens, lines, line_no)
|
|
812
|
-
elif
|
|
951
|
+
elif tokens[0].upper() == "ELSE" and tokens[1].upper() == "IF":
|
|
813
952
|
if current_if:
|
|
814
953
|
goto_endif = True
|
|
815
954
|
else:
|
|
816
955
|
return self.handle_if(tokens[1:], lines, line_no)
|
|
817
956
|
|
|
818
|
-
if goto_endif or (len(tokens) == 1 and tokens[0].upper() ==
|
|
957
|
+
if goto_endif or (len(tokens) == 1 and tokens[0].upper() == "ELSE"):
|
|
819
958
|
# The only way we ever hit an ELSE is if we were in a successful block beforehand
|
|
820
959
|
# Therefore goto the ENDIF
|
|
821
960
|
self.variables.if_stack[-1] = True
|
|
822
961
|
depth = 1
|
|
823
962
|
for i in range(line_no, len(lines)):
|
|
824
963
|
next_line = lines[i].strip().upper()
|
|
825
|
-
if next_line.startswith(
|
|
964
|
+
if next_line.startswith("IF "):
|
|
826
965
|
depth += 1
|
|
827
|
-
elif next_line.startswith(
|
|
966
|
+
elif next_line.startswith(("ENDIF", "END IF")):
|
|
828
967
|
depth -= 1
|
|
829
968
|
if depth == 0:
|
|
830
969
|
return i + 1
|
|
@@ -833,29 +972,43 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
833
972
|
raise ValueError(f"handle_else called with unexpected tokens at line {line_no}")
|
|
834
973
|
|
|
835
974
|
def handle_end(self, tokens, line_no):
|
|
836
|
-
if len(tokens) == 1 and tokens[0].upper() ==
|
|
837
|
-
raise ValueError(
|
|
838
|
-
|
|
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
|
+
):
|
|
839
983
|
# END IF pops the IF stack
|
|
840
984
|
self.variables.if_stack.pop()
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
loop_info[2] += 1
|
|
846
|
-
if loop_info[1] is not None:
|
|
847
|
-
# Counted loop, decrement the count
|
|
848
|
-
loop_info[1] -= 1
|
|
849
|
-
if loop_info[1] > 0:
|
|
850
|
-
return loop_info[0]
|
|
851
|
-
else:
|
|
852
|
-
self.variables.loop_stack.pop()
|
|
853
|
-
return line_no + 1 # Continue to the next line
|
|
854
|
-
else:
|
|
855
|
-
# Infinite loop, just continue to the start of the loop
|
|
856
|
-
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)
|
|
857
989
|
return line_no + 1
|
|
858
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
|
+
|
|
859
1012
|
def handle_escape(self, _tokens, lines, line_no):
|
|
860
1013
|
# Find the matching ENDLOOP
|
|
861
1014
|
depth = 1
|
|
@@ -865,9 +1018,11 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
865
1018
|
words = lines[i].strip().upper().split()
|
|
866
1019
|
if not words:
|
|
867
1020
|
continue
|
|
868
|
-
if words[0] ==
|
|
1021
|
+
if words[0] == "LOOP":
|
|
869
1022
|
depth += 1
|
|
870
|
-
elif words[0] ==
|
|
1023
|
+
elif words[0] == "ENDLOOP" or (
|
|
1024
|
+
words[0] == "END" and len(words) > 1 and words[1] == "LOOP"
|
|
1025
|
+
):
|
|
871
1026
|
depth -= 1
|
|
872
1027
|
if depth == 0:
|
|
873
1028
|
if len(self.variables.loop_stack) > 0:
|
|
@@ -876,14 +1031,18 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
876
1031
|
raise ValueError(f"No matching ENDLOOP found for ESCAPE command at line {line_no}")
|
|
877
1032
|
|
|
878
1033
|
def handle_go(self, tokens, lines, line_no):
|
|
879
|
-
if tokens[0].upper() ==
|
|
880
|
-
|
|
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
|
+
):
|
|
881
1040
|
raise ValueError(f"Invalid GOTO command format at line {line_no}")
|
|
882
1041
|
label = None
|
|
883
|
-
if tokens[0].upper() ==
|
|
884
|
-
label = (tokens[1] +
|
|
1042
|
+
if tokens[0].upper() == "GOTO":
|
|
1043
|
+
label = (tokens[1] + ":").upper()
|
|
885
1044
|
else:
|
|
886
|
-
label = (tokens[2] +
|
|
1045
|
+
label = (tokens[2] + ":").upper()
|
|
887
1046
|
# Find the label in the lines
|
|
888
1047
|
for i in range(1, len(lines)):
|
|
889
1048
|
next_line = lines[i - 1].strip().upper()
|
|
@@ -898,7 +1057,10 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
898
1057
|
# Evaluate the expression and store the result in the variable
|
|
899
1058
|
result = self.evaluate_expression(expression_tokens)
|
|
900
1059
|
except Exception as e:
|
|
901
|
-
raise ValueError(
|
|
1060
|
+
raise ValueError(
|
|
1061
|
+
f"Error evaluating expression '{expression_tokens}' "
|
|
1062
|
+
f"in IF command at line {line_no}: {e}"
|
|
1063
|
+
)
|
|
902
1064
|
|
|
903
1065
|
if result:
|
|
904
1066
|
# Mark if as handled
|
|
@@ -910,21 +1072,18 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
910
1072
|
depth = 1
|
|
911
1073
|
for i in range(line_no, len(lines)):
|
|
912
1074
|
next_line = lines[i].strip().upper()
|
|
913
|
-
if next_line.startswith(
|
|
1075
|
+
if next_line.startswith("IF "):
|
|
914
1076
|
depth += 1
|
|
915
|
-
elif next_line.startswith(
|
|
1077
|
+
elif next_line.startswith(("ENDIF", "END IF")):
|
|
916
1078
|
depth -= 1
|
|
917
1079
|
if depth == 0:
|
|
918
1080
|
return i + 1
|
|
919
|
-
elif next_line.startswith(
|
|
920
|
-
if next_line.startswith(
|
|
1081
|
+
elif next_line.startswith("ELSE") and depth == 1:
|
|
1082
|
+
if next_line.startswith(("ELSEIF", "ELSE IF")):
|
|
921
1083
|
# Continue to the ELSE IF
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
# Regular ELSE - Continue to line after
|
|
926
|
-
if depth == 1:
|
|
927
|
-
return i + 2
|
|
1084
|
+
return i + 1
|
|
1085
|
+
# Regular ELSE - Continue to line after
|
|
1086
|
+
return i + 2
|
|
928
1087
|
|
|
929
1088
|
raise ValueError(f"No matching ENDIF or ELSE found for IF command at line {line_no}")
|
|
930
1089
|
|
|
@@ -935,14 +1094,16 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
935
1094
|
variable_name = tokens[1]
|
|
936
1095
|
item_name = None
|
|
937
1096
|
expression_tokens = None
|
|
938
|
-
if not variable_name.startswith(
|
|
939
|
-
if tokens[2] !=
|
|
1097
|
+
if not variable_name.startswith("$"):
|
|
1098
|
+
if tokens[2] != "=":
|
|
940
1099
|
# Global variable for set_tlm
|
|
941
1100
|
item_name = tokens[2]
|
|
942
1101
|
expression_tokens = tokens[4:]
|
|
943
1102
|
else:
|
|
944
|
-
raise ValueError(
|
|
945
|
-
|
|
1103
|
+
raise ValueError(
|
|
1104
|
+
f"Non-global variable name must start with '$' in LET command at line {line_no}"
|
|
1105
|
+
)
|
|
1106
|
+
elif tokens[2] == "=":
|
|
946
1107
|
# Local or Special variable
|
|
947
1108
|
expression_tokens = tokens[3:]
|
|
948
1109
|
else:
|
|
@@ -953,14 +1114,17 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
953
1114
|
# Evaluate the expression and store the result in the variable
|
|
954
1115
|
result = self.evaluate_expression(expression_tokens)
|
|
955
1116
|
except Exception as e:
|
|
956
|
-
raise ValueError(
|
|
1117
|
+
raise ValueError(
|
|
1118
|
+
f"Error evaluating expression '{expression_tokens}' "
|
|
1119
|
+
f"in LET command at line {line_no}: {e}"
|
|
1120
|
+
)
|
|
957
1121
|
|
|
958
1122
|
if item_name:
|
|
959
1123
|
if isinstance(result, str):
|
|
960
1124
|
set_tlm(f"{variable_name} LATEST {item_name} = '{result}'")
|
|
961
1125
|
else:
|
|
962
1126
|
set_tlm(f"{variable_name} LATEST {item_name} = {result}")
|
|
963
|
-
elif variable_name.startswith(
|
|
1127
|
+
elif variable_name.startswith("$$"):
|
|
964
1128
|
# Special variable
|
|
965
1129
|
self.variables.set_special_variable(variable_name, result)
|
|
966
1130
|
else:
|
|
@@ -978,9 +1142,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
978
1142
|
raise ValueError(f"Invalid LOAD command format at line {line_no}")
|
|
979
1143
|
location = expressions[0]
|
|
980
1144
|
filename = expressions[1]
|
|
1145
|
+
# The evaluated location is unused, COSMOS sends the whole file to the interface
|
|
981
1146
|
results = self.evaluate_expressions([interface_name, location, filename])
|
|
982
1147
|
interface_name = results[0]
|
|
983
|
-
location = results[1]
|
|
984
1148
|
filename = results[2]
|
|
985
1149
|
file = get_target_file(filename)
|
|
986
1150
|
data = file.read()
|
|
@@ -1003,19 +1167,20 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1003
1167
|
# Variables
|
|
1004
1168
|
index = 0
|
|
1005
1169
|
for token in tokens[2:]:
|
|
1006
|
-
if token ==
|
|
1170
|
+
if token == ",":
|
|
1007
1171
|
continue
|
|
1008
|
-
elif token.startswith(
|
|
1172
|
+
elif token.startswith("$"):
|
|
1009
1173
|
self.variables.set_local_variable(token, os.getenv(f"CSTOL_ARG_{index}"))
|
|
1010
1174
|
index += 1
|
|
1011
1175
|
else:
|
|
1012
|
-
raise ValueError(
|
|
1176
|
+
raise ValueError(
|
|
1177
|
+
f"Invalid variable '{token}' in PROC command at line {line_no}"
|
|
1178
|
+
)
|
|
1013
1179
|
|
|
1014
1180
|
def handle_return(self, tokens, lines, line_no):
|
|
1015
|
-
if len(tokens) > 1:
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
return (len(lines) + 1)
|
|
1181
|
+
if len(tokens) > 1 and tokens[1].upper() == "ALL":
|
|
1182
|
+
raise StopScriptError
|
|
1183
|
+
return len(lines) + 1
|
|
1019
1184
|
|
|
1020
1185
|
def handle_run(self, tokens, line_no):
|
|
1021
1186
|
if len(tokens) < 2:
|
|
@@ -1026,7 +1191,7 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1026
1191
|
results = self.evaluate_expressions(expressions)
|
|
1027
1192
|
string_results = [('"' + str(result) + '"') for result in results]
|
|
1028
1193
|
string_results.insert(0, script)
|
|
1029
|
-
script =
|
|
1194
|
+
script = " ".join(string_results)
|
|
1030
1195
|
print(f"Running system call: {script}")
|
|
1031
1196
|
os.system(script)
|
|
1032
1197
|
|
|
@@ -1045,7 +1210,9 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1045
1210
|
# START proc-name [argument-list]
|
|
1046
1211
|
proc_name = tokens[1]
|
|
1047
1212
|
|
|
1048
|
-
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
|
+
):
|
|
1049
1216
|
# Remove quotes
|
|
1050
1217
|
proc_name = proc_name[1:-1]
|
|
1051
1218
|
|
|
@@ -1062,12 +1229,12 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1062
1229
|
|
|
1063
1230
|
def handle_switch(self, tokens, line_no):
|
|
1064
1231
|
action = tokens[1].upper()
|
|
1065
|
-
if action not in [
|
|
1232
|
+
if action not in ["ON", "OFF"]:
|
|
1066
1233
|
raise ValueError(f"Invalid SWITCH action '{action}' at line {line_no}")
|
|
1067
1234
|
interface_name = tokens[2]
|
|
1068
|
-
if action ==
|
|
1235
|
+
if action == "ON":
|
|
1069
1236
|
connect_interface(interface_name)
|
|
1070
|
-
elif action ==
|
|
1237
|
+
elif action == "OFF":
|
|
1071
1238
|
disconnect_interface(interface_name)
|
|
1072
1239
|
|
|
1073
1240
|
def handle_wait(self, tokens, line_no):
|
|
@@ -1088,30 +1255,40 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1088
1255
|
raise ValueError(f"Invalid timestamp format at line {line_no}")
|
|
1089
1256
|
else:
|
|
1090
1257
|
# Conditional expression wait
|
|
1091
|
-
|
|
1092
|
-
python_expression = self.build_python_expression(
|
|
1093
|
-
if
|
|
1258
|
+
condition_tokens, timeout_tokens = self._split_wait_timeout(tokens[1:])
|
|
1259
|
+
python_expression = self.build_python_expression(condition_tokens)
|
|
1260
|
+
if timeout_tokens is not None:
|
|
1094
1261
|
# Timeout given with "OR FOR" or "OR UNTIL"
|
|
1095
|
-
seconds = self.evaluate_expression(
|
|
1262
|
+
seconds = self.evaluate_expression(timeout_tokens[1:]) # Drop FOR or UNTIL token
|
|
1096
1263
|
if isinstance(seconds, (int, float, complex)) and not isinstance(seconds, bool):
|
|
1097
1264
|
if seconds > self.ONE_YEAR_SECONDS:
|
|
1098
1265
|
now = datetime.datetime.now().timestamp()
|
|
1099
1266
|
seconds = seconds - now
|
|
1100
1267
|
if seconds < 0:
|
|
1101
1268
|
seconds = 0.0
|
|
1102
|
-
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
|
+
)
|
|
1103
1275
|
if result:
|
|
1104
|
-
self.variables.set_special_variable(
|
|
1276
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "NO_ERROR")
|
|
1105
1277
|
else:
|
|
1106
|
-
self.variables.set_special_variable(
|
|
1278
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "TIME_OUT")
|
|
1107
1279
|
else:
|
|
1108
1280
|
raise ValueError(f"Invalid timestamp format at line {line_no}")
|
|
1109
1281
|
else:
|
|
1110
|
-
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
|
|
1111
1288
|
if result:
|
|
1112
|
-
self.variables.set_special_variable(
|
|
1289
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "NO_ERROR")
|
|
1113
1290
|
else:
|
|
1114
|
-
self.variables.set_special_variable(
|
|
1291
|
+
self.variables.set_special_variable(SpecialVars.ERROR, "TIME_OUT")
|
|
1115
1292
|
|
|
1116
1293
|
def handle_write(self, tokens, line_no):
|
|
1117
1294
|
expressions = self.extract_expressions(tokens[1:], ",")
|
|
@@ -1119,48 +1296,13 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1119
1296
|
for expr in expressions:
|
|
1120
1297
|
if len(expr) == 0:
|
|
1121
1298
|
raise ValueError(f"Empty expression in WRITE command at line {line_no}")
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
# Format string
|
|
1125
|
-
# %X or %x Output in hexadecimal values Value is converted to an integer prior to applying the format
|
|
1126
|
-
# %O or %o Output in octal values Value is converted to an integer prior to applying the format
|
|
1127
|
-
# %B or %b Output in binary values Value is converted to an integer prior to applying the format
|
|
1128
|
-
# %I or %i Output in decimal values Value is converted to an integer prior to applying the format
|
|
1129
|
-
# %D or %d Output in decimal values Value is converted to an integer prior to applying the format
|
|
1130
|
-
# %F or %f Output in floating point values Default for integer or raw value
|
|
1131
|
-
# %E or %e Output in floating point values Default for float or EU value
|
|
1132
|
-
format = expr[0][1:].upper()
|
|
1133
|
-
if format not in ['X', 'O', 'B', 'I', 'D', 'F', 'E']:
|
|
1134
|
-
raise ValueError(f"Invalid format %'{format}' in WRITE command at line {line_no}")
|
|
1135
|
-
if len(expr) < 2:
|
|
1136
|
-
raise ValueError(f"Missing value for format %'{format}' in WRITE command at line {line_no}")
|
|
1137
|
-
value = self.evaluate_expression(expr[1:])
|
|
1138
|
-
if format in ['X', 'O', 'B', 'I', 'D']:
|
|
1139
|
-
# Convert to integer
|
|
1140
|
-
value = int(value)
|
|
1141
|
-
if format == 'X':
|
|
1142
|
-
result = (f"{value:X}")
|
|
1143
|
-
elif format == 'O':
|
|
1144
|
-
result = (f"{value:o}")
|
|
1145
|
-
elif format == 'B':
|
|
1146
|
-
result = (f"{value:b}")
|
|
1147
|
-
elif format in ['I', 'D']:
|
|
1148
|
-
result = str(value)
|
|
1149
|
-
elif format in ['F', 'E']:
|
|
1150
|
-
# Convert to float
|
|
1151
|
-
value = float(value)
|
|
1152
|
-
if format == 'F':
|
|
1153
|
-
result = f"{value:.6f}"
|
|
1154
|
-
elif format == 'E':
|
|
1155
|
-
result = f"{value:.6e}"
|
|
1156
|
-
else:
|
|
1157
|
-
result = str(self.evaluate_expression(expr))
|
|
1158
|
-
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)))
|
|
1159
1301
|
# Join the results with spaces
|
|
1160
|
-
output =
|
|
1302
|
+
output = " ".join(results)
|
|
1161
1303
|
print(output)
|
|
1162
1304
|
|
|
1163
|
-
def run_text(self, text, filename
|
|
1305
|
+
def run_text(self, text, filename=None, line_no=1, end_line_no=None, bind_variables=False):
|
|
1164
1306
|
saved_variables = self.variables
|
|
1165
1307
|
try:
|
|
1166
1308
|
if not bind_variables:
|
|
@@ -1181,20 +1323,20 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1181
1323
|
if self.saved_tokens:
|
|
1182
1324
|
tokens = self.saved_tokens + tokens
|
|
1183
1325
|
self.saved_tokens = None
|
|
1184
|
-
if tokens[-1] ==
|
|
1185
|
-
self.saved_tokens = tokens[:-1]
|
|
1326
|
+
if tokens[-1] == "&":
|
|
1327
|
+
self.saved_tokens = tokens[:-1] # Everything before the '&' is saved for the next line
|
|
1186
1328
|
return line_no + 1 # Skip to the next line
|
|
1187
1329
|
|
|
1188
1330
|
# Remove any trailing comments
|
|
1189
|
-
if
|
|
1190
|
-
comment_index = tokens.index(
|
|
1331
|
+
if ";" in tokens:
|
|
1332
|
+
comment_index = tokens.index(";")
|
|
1191
1333
|
tokens = tokens[:comment_index]
|
|
1192
1334
|
|
|
1193
1335
|
if len(tokens) != 0:
|
|
1194
1336
|
keyword = tokens[0].upper()
|
|
1195
1337
|
|
|
1196
1338
|
# Handle labels
|
|
1197
|
-
if keyword.endswith(
|
|
1339
|
+
if keyword.endswith(":"):
|
|
1198
1340
|
if len(tokens) > 1:
|
|
1199
1341
|
tokens = tokens[1:]
|
|
1200
1342
|
keyword = tokens[0].upper()
|
|
@@ -1205,20 +1347,75 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1205
1347
|
case "ASK":
|
|
1206
1348
|
self.handle_ask(tokens, line_no)
|
|
1207
1349
|
case "BEGIN":
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
"
|
|
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
|
+
):
|
|
1212
1378
|
# These keywords are noops for this CSTOL script engine
|
|
1213
1379
|
print(f"Ignoring Unsupported Keyword: {keyword}")
|
|
1214
1380
|
case "CHECK":
|
|
1215
1381
|
self.handle_check(tokens, line_no)
|
|
1216
1382
|
case "CLEAR":
|
|
1217
1383
|
self.handle_clear(tokens, line_no)
|
|
1218
|
-
case
|
|
1219
|
-
"
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
+
):
|
|
1222
1419
|
self.handle_cmd(tokens, line_no)
|
|
1223
1420
|
case "DECLARE":
|
|
1224
1421
|
self.handle_declare(tokens, line_no)
|
|
@@ -1261,4 +1458,4 @@ class CstolScriptEngine(ScriptEngine):
|
|
|
1261
1458
|
case _:
|
|
1262
1459
|
raise ValueError(f"Unknown keyword '{keyword}' at line {line_no}")
|
|
1263
1460
|
|
|
1264
|
-
return line_no + 1
|
|
1461
|
+
return line_no + 1
|