openc3-cosmos-script-engine-cstol 1.0.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 +7 -0
- data/LICENSE.txt +7 -0
- data/README.md +200 -0
- data/Rakefile +23 -0
- data/lib/cstol_script_engine.py +1195 -0
- data/plugin.txt +10 -0
- data/targets/CSTOL_TEST/procedures/collect.prc +65 -0
- data/targets/CSTOL_TEST/procedures/test.prc +7 -0
- metadata +50 -0
@@ -0,0 +1,1195 @@
|
|
1
|
+
# Copyright 2025 OpenC3, Inc.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to
|
5
|
+
# deal in the Software without restriction, including without limitation the
|
6
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
# sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
18
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
19
|
+
# DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
# CSTOL language originally developed by the University of Colorado / LASP.
|
22
|
+
|
23
|
+
import re
|
24
|
+
import shlex
|
25
|
+
import datetime
|
26
|
+
import math
|
27
|
+
import os
|
28
|
+
from openc3.script.exceptions import CheckError, StopScript
|
29
|
+
from openc3.script_engines.script_engine import ScriptEngine
|
30
|
+
from openc3.script import wait, wait_expression, ask_string, clear_screen, clear_all_screens, set_tlm, display_screen, \
|
31
|
+
connect_interface, disconnect_interface, send_raw, start, cmd, get_target_file, tlm, ask_string, set_line_delay, step_mode, run_mode
|
32
|
+
|
33
|
+
class CstolVariables:
|
34
|
+
special_variables = {
|
35
|
+
"$$OWLT": 0.0,
|
36
|
+
"$$ERROR": "NO_ERROR",
|
37
|
+
"$$CHECK_INTERVAL": 1.0,
|
38
|
+
"$$STEP_INTERVAL": 0.1,
|
39
|
+
"$$CLP_STP_INTERVAL": 0.1,
|
40
|
+
"$$CLP_STEP_MODE": "PAUSE",
|
41
|
+
"$$STEP_MODE": "PAUSE"
|
42
|
+
}
|
43
|
+
|
44
|
+
def __init__(self):
|
45
|
+
self.local_variables = {}
|
46
|
+
self.loop_stack = []
|
47
|
+
|
48
|
+
def set_special_variable(self, name, value):
|
49
|
+
"""
|
50
|
+
Sets a special variable, which is a variable that starts with '$$'.
|
51
|
+
Special variables are not stored in the local variables dictionary.
|
52
|
+
"""
|
53
|
+
if not name.startswith('$$'):
|
54
|
+
raise ValueError(f"Special variable names must start with '$$': {name}")
|
55
|
+
self.special_variables[name] = value
|
56
|
+
match name.upper():
|
57
|
+
case "$$CLP_STP_INTERVAL":
|
58
|
+
set_line_delay(value)
|
59
|
+
self.special_variables["$$STEP_INTERVAL"] = value
|
60
|
+
case "$$STEP_INTERVAL":
|
61
|
+
self.set_special_variable("$$CLP_STP_INTERVAL", value)
|
62
|
+
case "$$CLP_STEP_MODE":
|
63
|
+
mode = str(value).upper()
|
64
|
+
if mode == "GO":
|
65
|
+
run_mode()
|
66
|
+
set_line_delay(0)
|
67
|
+
elif mode == "PAUSE":
|
68
|
+
run_mode()
|
69
|
+
set_line_delay(self.special_variables["$$STEP_INTERVAL"])
|
70
|
+
elif mode == "WAIT":
|
71
|
+
step_mode()
|
72
|
+
else:
|
73
|
+
raise ValueError(f"Invalid step mode: {mode}")
|
74
|
+
self.special_variables["$$STEP_MODE"] = value
|
75
|
+
case "$$STEP_MODE":
|
76
|
+
self.set_special_variable("$$CLP_STEP_MODE", value)
|
77
|
+
|
78
|
+
def get_special_variable(self, name):
|
79
|
+
"""
|
80
|
+
Gets a special variable by name.
|
81
|
+
Returns None if the variable does not exist.
|
82
|
+
"""
|
83
|
+
if not name.startswith('$$'):
|
84
|
+
raise ValueError(f"Special variable names must start with '$$': {name}")
|
85
|
+
match name.upper():
|
86
|
+
case "$$CURRENT_TIME":
|
87
|
+
return datetime.datetime.now(datetime.timezone.utc).timestamp()
|
88
|
+
case "$$SC_TIME":
|
89
|
+
return datetime.datetime.now(datetime.timezone.utc).timestamp() + self.special_variables["$$OWLT"]
|
90
|
+
case "$$LOOP_COUNT":
|
91
|
+
if len(self.loop_stack) > 0:
|
92
|
+
return self.loop_stack[-1][2]
|
93
|
+
else:
|
94
|
+
return 0
|
95
|
+
|
96
|
+
return self.special_variables.get(name, None)
|
97
|
+
|
98
|
+
class CstolScriptEngine(ScriptEngine):
|
99
|
+
|
100
|
+
ONE_YEAR_SECONDS = 31536000
|
101
|
+
|
102
|
+
# Dictionary of known CSTOL tokens - Unknown could be numbers or parts of telemetry names
|
103
|
+
KNOWN_TOKENS = {
|
104
|
+
'AND': 'and',
|
105
|
+
'OR': 'or',
|
106
|
+
'XOR': 'xor',
|
107
|
+
'NOT': 'not',
|
108
|
+
'FOR': '',
|
109
|
+
'UNTIL': '',
|
110
|
+
'MOD': '%',
|
111
|
+
'**': '**',
|
112
|
+
'*': '*',
|
113
|
+
'(': '(',
|
114
|
+
')': ')',
|
115
|
+
'<': '<',
|
116
|
+
'>': '>',
|
117
|
+
'<=': '<=',
|
118
|
+
'>=': '>=',
|
119
|
+
'/=': '!=',
|
120
|
+
'=': '==',
|
121
|
+
'+': '+',
|
122
|
+
'-': '-',
|
123
|
+
'/': '/',
|
124
|
+
'TRUE': 'True',
|
125
|
+
'FALSE': 'False',
|
126
|
+
}
|
127
|
+
|
128
|
+
FUNCTION_TOKENS = {
|
129
|
+
'SIN': 'math.sin',
|
130
|
+
'COS': 'math.cos',
|
131
|
+
'TAN': 'math.tan',
|
132
|
+
'ASIN': 'math.asin',
|
133
|
+
'ACOS': 'math.acos',
|
134
|
+
'ATAN': 'math.atan',
|
135
|
+
'SINH': 'math.sinh',
|
136
|
+
'COSH': 'math.cosh',
|
137
|
+
'TANH': 'math.tanh',
|
138
|
+
'EXP': 'math.exp',
|
139
|
+
'LOG': 'math.log',
|
140
|
+
'LOG2': 'math.log2',
|
141
|
+
'LOG10': 'math.log10',
|
142
|
+
'SQRT': 'math.sqrt',
|
143
|
+
'EVAL': 'eval',
|
144
|
+
'GETENV': 'os.getenv',
|
145
|
+
}
|
146
|
+
|
147
|
+
KNOWN_UNITS = [
|
148
|
+
'DN',
|
149
|
+
'A',
|
150
|
+
'C',
|
151
|
+
'CM',
|
152
|
+
'F',
|
153
|
+
'FT',
|
154
|
+
'G',
|
155
|
+
'GHZ',
|
156
|
+
'H',
|
157
|
+
'HZ',
|
158
|
+
'IN',
|
159
|
+
'J',
|
160
|
+
'K',
|
161
|
+
'KG',
|
162
|
+
'KM',
|
163
|
+
'KOHM',
|
164
|
+
'KV',
|
165
|
+
'KW',
|
166
|
+
'M',
|
167
|
+
'MA',
|
168
|
+
'MG',
|
169
|
+
'MHZ',
|
170
|
+
'MIN',
|
171
|
+
'MM',
|
172
|
+
'MOHM',
|
173
|
+
'MV',
|
174
|
+
'MW',
|
175
|
+
'OHM',
|
176
|
+
'PA',
|
177
|
+
'PSI',
|
178
|
+
'S',
|
179
|
+
'UA',
|
180
|
+
'UV',
|
181
|
+
'V',
|
182
|
+
'W',
|
183
|
+
]
|
184
|
+
|
185
|
+
def __init__(self, running_script):
|
186
|
+
super().__init__(running_script)
|
187
|
+
self.variables = CstolVariables()
|
188
|
+
self.saved_tokens = None
|
189
|
+
|
190
|
+
def cstol_tokenizer(self, s, special_chars='()><+-*/=;,'):
|
191
|
+
tokens = self.tokenizer(s, special_chars)
|
192
|
+
|
193
|
+
# Reconstruct full timestamps of the format yyyy/doy-HH:MM:SS into single tokens
|
194
|
+
# These get broken up like ["yyyy", "/", "doy", "-", "HH:MM:SS"] by the tokenizer
|
195
|
+
reconstructed_tokens = []
|
196
|
+
i = 0
|
197
|
+
while i < len(tokens):
|
198
|
+
# Check if we have a potential timestamp pattern: yyyy/doy-HH:MM:SS
|
199
|
+
if (i + 4 < len(tokens) and
|
200
|
+
re.match(r'^\d{4}$', tokens[i]) and
|
201
|
+
tokens[i + 1] == '/' and
|
202
|
+
re.match(r'^\d{1,3}$', tokens[i + 2]) and
|
203
|
+
tokens[i + 3] == '-' and
|
204
|
+
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 4])):
|
205
|
+
# Reconstruct the full timestamp
|
206
|
+
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2] + tokens[i + 3] + tokens[i + 4]
|
207
|
+
reconstructed_tokens.append(timestamp)
|
208
|
+
i += 5 # Skip the next 4 tokens since we combined them
|
209
|
+
|
210
|
+
# Check if we have a potential timestamp pattern: /doy-HH:MM:SS
|
211
|
+
elif (i + 3 < len(tokens) and
|
212
|
+
tokens[i] == '/' and
|
213
|
+
re.match(r'^\d{1,3}$', tokens[i + 1]) and
|
214
|
+
tokens[i + 2] == '-' and
|
215
|
+
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 3])):
|
216
|
+
# Reconstruct the full timestamp
|
217
|
+
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2] + tokens[i + 3]
|
218
|
+
reconstructed_tokens.append(timestamp)
|
219
|
+
i += 4 # Skip the next 3 tokens since we combined them
|
220
|
+
|
221
|
+
# Check if we have a potential timestamp pattern: yyyy/-HH:MM:SS
|
222
|
+
elif (i + 3 < len(tokens) and
|
223
|
+
re.match(r'^\d{4}$', tokens[i]) and
|
224
|
+
tokens[i + 1] == '/' and
|
225
|
+
tokens[i + 2] == '-' and
|
226
|
+
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 3])):
|
227
|
+
# Reconstruct the full timestamp
|
228
|
+
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2] + tokens[i + 3]
|
229
|
+
reconstructed_tokens.append(timestamp)
|
230
|
+
i += 4 # Skip the next 3 tokens since we combined them
|
231
|
+
|
232
|
+
# Check if we have a potential timestamp pattern: yyyy/-HH:MM:SS
|
233
|
+
elif (i + 2 < len(tokens) and
|
234
|
+
tokens[i] == '/' and
|
235
|
+
tokens[i + 1] == '-' and
|
236
|
+
re.match(r'^\d{1,2}:\d{1,2}:\d{1,2}\.?\d*$', tokens[i + 2])):
|
237
|
+
# Reconstruct the full timestamp
|
238
|
+
timestamp = tokens[i] + tokens[i + 1] + tokens[i + 2]
|
239
|
+
reconstructed_tokens.append(timestamp)
|
240
|
+
i += 3 # Skip the next 2 tokens since we combined them
|
241
|
+
|
242
|
+
else:
|
243
|
+
reconstructed_tokens.append(tokens[i])
|
244
|
+
i += 1
|
245
|
+
|
246
|
+
return reconstructed_tokens
|
247
|
+
|
248
|
+
def build_python_expression(self, expression_tokens):
|
249
|
+
"""
|
250
|
+
Builds a Python expression from the provided tokens.
|
251
|
+
|
252
|
+
Needs to handle all the CSTOL operators and syntax, converting them into valid Python syntax.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
expression_tokens: List of tokens that form a valid Python expression
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
A string representing the Python expression
|
259
|
+
"""
|
260
|
+
final_tokens = []
|
261
|
+
previous_number = False
|
262
|
+
for index, token in enumerate(expression_tokens):
|
263
|
+
# Handle Units
|
264
|
+
if previous_number and token.upper() in self.KNOWN_UNITS:
|
265
|
+
# Just drop the units
|
266
|
+
previous_number = False
|
267
|
+
continue
|
268
|
+
previous_number = False
|
269
|
+
|
270
|
+
# Handle special variables
|
271
|
+
if token.startswith('$$'):
|
272
|
+
# Get the actual value of special variable
|
273
|
+
value = self.variables.get_special_variable(token)
|
274
|
+
if isinstance(value, str):
|
275
|
+
final_tokens.append(f'"{value}"')
|
276
|
+
else:
|
277
|
+
final_tokens.append(str(value))
|
278
|
+
continue
|
279
|
+
|
280
|
+
# Handle local variables
|
281
|
+
if token.startswith('$'):
|
282
|
+
# Get the actual value of local variable
|
283
|
+
value = self.variables.local_variables.get(token, None)
|
284
|
+
if value is None:
|
285
|
+
raise ValueError(f"Unknown variable: {token}")
|
286
|
+
if isinstance(value, str):
|
287
|
+
final_tokens.append(f'"{value}"')
|
288
|
+
else:
|
289
|
+
final_tokens.append(str(value))
|
290
|
+
continue
|
291
|
+
|
292
|
+
# Handle CSTOL operators and convert to Python equivalents
|
293
|
+
if token.upper() in self.KNOWN_TOKENS:
|
294
|
+
token = self.KNOWN_TOKENS[token.upper()]
|
295
|
+
final_tokens.append(token)
|
296
|
+
continue
|
297
|
+
|
298
|
+
# Handle CSTOL functions
|
299
|
+
if token.upper() in self.FUNCTION_TOKENS:
|
300
|
+
# Function tokens must be followed by ( or they will be considered just strings
|
301
|
+
if len(expression_tokens) > index + 1 and expression_tokens[index + 1] == '(':
|
302
|
+
token = self.FUNCTION_TOKENS[token.upper()]
|
303
|
+
final_tokens.append(token)
|
304
|
+
else:
|
305
|
+
final_tokens.append(f'"{token}"')
|
306
|
+
continue
|
307
|
+
|
308
|
+
# Handle timestamps
|
309
|
+
timestamp = self.parse_timestamp(token)
|
310
|
+
if timestamp is not None:
|
311
|
+
final_tokens.append(str(timestamp))
|
312
|
+
continue
|
313
|
+
|
314
|
+
# Handle radix notation integers
|
315
|
+
matches = re.match(r'^[BODXHbodxh]#\d+$', token)
|
316
|
+
if matches is not None:
|
317
|
+
match (token[0].upper()):
|
318
|
+
case 'B':
|
319
|
+
final_tokens.append(f'0b{token[2:]}')
|
320
|
+
case 'O':
|
321
|
+
final_tokens.append(f'0o{token[2:]}')
|
322
|
+
case 'D':
|
323
|
+
final_tokens.append(f'{token[2:]}')
|
324
|
+
case 'H':
|
325
|
+
final_tokens.append(str(int(token[2:], 16).to_bytes((len(token[2:]) + 1) // 2, 'big')))
|
326
|
+
case 'X':
|
327
|
+
final_tokens.append(f'0x{token[2:]}')
|
328
|
+
previous_number = True
|
329
|
+
continue
|
330
|
+
|
331
|
+
# Handle numbers - Remove DN and EUs
|
332
|
+
# Use regex to extract the numeric part (including scientific notation)
|
333
|
+
# This pattern matches:
|
334
|
+
# - Optional sign (+/-)
|
335
|
+
# - Digits, optional decimal point and more digits
|
336
|
+
# - Optional scientific notation (e/E followed by optional sign and digits)
|
337
|
+
# - Ignores any alphabetic characters that follow
|
338
|
+
matches = re.match(r'^([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)', token)
|
339
|
+
if matches is not None:
|
340
|
+
final_tokens.append(str(matches.group(1)))
|
341
|
+
previous_number = True
|
342
|
+
continue
|
343
|
+
|
344
|
+
# Handle quoted strings and non-numbers
|
345
|
+
if token.startswith('"') and token.endswith('"'):
|
346
|
+
# Already quoted with double quotes
|
347
|
+
final_tokens.append(token)
|
348
|
+
elif token.startswith("'") and token.endswith("'"):
|
349
|
+
# Already quoted with single quotes
|
350
|
+
final_tokens.append(token)
|
351
|
+
else:
|
352
|
+
# Non numbers should become quoted strings
|
353
|
+
final_tokens.append(f'"{token}"')
|
354
|
+
|
355
|
+
# Second pass: look for sets of double quoted strings and convert to tlm() calls
|
356
|
+
processed_tokens = []
|
357
|
+
i = 0
|
358
|
+
while i < len(final_tokens):
|
359
|
+
token = final_tokens[i]
|
360
|
+
|
361
|
+
# Check for "RAW" pattern first: "RAW", "TARGET_NAME", "ITEM_NAME"
|
362
|
+
if (token == '"RAW"' and i + 2 < len(final_tokens) and
|
363
|
+
final_tokens[i + 1].startswith('"') and final_tokens[i + 1].endswith('"') and
|
364
|
+
final_tokens[i + 2].startswith('"') and final_tokens[i + 2].endswith('"')):
|
365
|
+
|
366
|
+
target_name = final_tokens[i + 1][1:-1] # Remove quotes
|
367
|
+
item_name = final_tokens[i + 2][1:-1] # Remove quotes
|
368
|
+
tlm_call = f'tlm("{target_name}", "LATEST", "{item_name}", type="RAW")'
|
369
|
+
processed_tokens.append(tlm_call)
|
370
|
+
i += 3 # Skip the next 2 tokens
|
371
|
+
|
372
|
+
# Check for regular pattern: "TARGET_NAME", "ITEM_NAME"
|
373
|
+
elif (token.startswith('"') and token.endswith('"') and i + 1 < len(final_tokens) and
|
374
|
+
final_tokens[i + 1].startswith('"') and final_tokens[i + 1].endswith('"')):
|
375
|
+
|
376
|
+
target_name = token[1:-1] # Remove quotes
|
377
|
+
item_name = final_tokens[i + 1][1:-1] # Remove quotes
|
378
|
+
tlm_call = f'tlm("{target_name}", "LATEST", "{item_name}")'
|
379
|
+
processed_tokens.append(tlm_call)
|
380
|
+
i += 2 # Skip the next token
|
381
|
+
|
382
|
+
else:
|
383
|
+
# Keep the original token
|
384
|
+
processed_tokens.append(token)
|
385
|
+
i += 1
|
386
|
+
|
387
|
+
# Join the processed tokens into a single string
|
388
|
+
return ' '.join(processed_tokens)
|
389
|
+
|
390
|
+
# Combined regex pattern to match both formats
|
391
|
+
TIMESTAMP_PATTERN = r'(?:(\d{4})?/(\d{1,3})?-)?(\d{1,2}):(\d{1,2}):(\d{1,2}\.?\d*)'
|
392
|
+
|
393
|
+
# Will convert a CSTOL Clock Time or Delta Time into floating point seconds
|
394
|
+
def parse_timestamp(self, timestamp, now = None):
|
395
|
+
if now is None:
|
396
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
397
|
+
|
398
|
+
matches = re.match(self.TIMESTAMP_PATTERN, timestamp)
|
399
|
+
if matches:
|
400
|
+
year, day_of_year, hour, minute, second = matches.groups()
|
401
|
+
result = {
|
402
|
+
'hour': int(hour),
|
403
|
+
'minute': int(minute),
|
404
|
+
'second': float(second)
|
405
|
+
}
|
406
|
+
|
407
|
+
# Determine if / was present (meaning a clock time rather than delta time)
|
408
|
+
if '/' in timestamp:
|
409
|
+
result['clock_time'] = True
|
410
|
+
else:
|
411
|
+
result['clock_time'] = False
|
412
|
+
|
413
|
+
# Add year and day_of_year if they exist
|
414
|
+
if year is not None:
|
415
|
+
result['year'] = int(year)
|
416
|
+
elif result['clock_time']:
|
417
|
+
result['year'] = now.year
|
418
|
+
|
419
|
+
if day_of_year is not None:
|
420
|
+
result['day_of_year'] = int(day_of_year)
|
421
|
+
elif result['clock_time']:
|
422
|
+
result['day_of_year'] = now.timetuple().tm_yday
|
423
|
+
|
424
|
+
# Extract seconds and microseconds from fractional seconds
|
425
|
+
total_seconds = result['second']
|
426
|
+
seconds = int(total_seconds)
|
427
|
+
# Use round to handle floating point precision issues
|
428
|
+
microseconds = round((total_seconds - seconds) * 1000000)
|
429
|
+
|
430
|
+
if 'year' in result and 'day_of_year' in result:
|
431
|
+
# Create datetime from year and day of year
|
432
|
+
base_date = datetime.datetime(
|
433
|
+
year=result['year'],
|
434
|
+
month=1,
|
435
|
+
day=1,
|
436
|
+
tzinfo=datetime.timezone.utc
|
437
|
+
) + datetime.timedelta(days=result['day_of_year'] - 1)
|
438
|
+
|
439
|
+
return datetime.datetime(
|
440
|
+
year=base_date.year,
|
441
|
+
month=base_date.month,
|
442
|
+
day=base_date.day,
|
443
|
+
hour=result['hour'],
|
444
|
+
minute=result['minute'],
|
445
|
+
second=seconds,
|
446
|
+
microsecond=microseconds,
|
447
|
+
tzinfo=datetime.timezone.utc
|
448
|
+
).timestamp()
|
449
|
+
else:
|
450
|
+
# Delta Time
|
451
|
+
return datetime.timedelta(
|
452
|
+
hours=result['hour'],
|
453
|
+
minutes=result['minute'],
|
454
|
+
seconds=seconds,
|
455
|
+
microseconds=microseconds
|
456
|
+
).total_seconds()
|
457
|
+
|
458
|
+
return None
|
459
|
+
|
460
|
+
def flatten(self, xss):
|
461
|
+
return [x for xs in xss for x in xs]
|
462
|
+
|
463
|
+
def extract_expressions(self, tokens, seperator=","):
|
464
|
+
# Split the tokens into expressions based on seperator
|
465
|
+
# This allows for multiple expressions in a single line, separated by commas
|
466
|
+
expressions = []
|
467
|
+
expression = []
|
468
|
+
for token in tokens:
|
469
|
+
if token.upper() == seperator:
|
470
|
+
expressions.append(expression)
|
471
|
+
expression = []
|
472
|
+
else:
|
473
|
+
expression.append(token)
|
474
|
+
if len(expression) > 0:
|
475
|
+
expressions.append(expression)
|
476
|
+
return expressions
|
477
|
+
|
478
|
+
def split_vs_tokens_on_colon(self, tokens):
|
479
|
+
"""
|
480
|
+
Split tokens on colons for VS clause processing, but preserve timestamps.
|
481
|
+
Timestamps (yyyy/doy-HH:MM:SS or HH:MM:SS) should not be split.
|
482
|
+
Range expressions like "1V:2V" should be split into ["1V", ":", "2V"].
|
483
|
+
Handles both pre-split tokens and tokens that need splitting.
|
484
|
+
"""
|
485
|
+
result = []
|
486
|
+
for token in tokens:
|
487
|
+
if ':' in token and not re.fullmatch(self.TIMESTAMP_PATTERN, token):
|
488
|
+
# Split non-timestamp tokens containing colons
|
489
|
+
parts = token.split(':')
|
490
|
+
for i, part in enumerate(parts):
|
491
|
+
if i > 0:
|
492
|
+
result.append(':')
|
493
|
+
if part: # Only add non-empty parts
|
494
|
+
result.append(part)
|
495
|
+
else:
|
496
|
+
result.append(token)
|
497
|
+
return result
|
498
|
+
|
499
|
+
def evaluate_expression(self, expression):
|
500
|
+
"""
|
501
|
+
Evaluates a single CSTOL expression and returns the result.
|
502
|
+
The expression can be a variable, a number, or a complex expression.
|
503
|
+
"""
|
504
|
+
# Convert the expression to a Python expression
|
505
|
+
python_expression = self.build_python_expression(expression)
|
506
|
+
result = None
|
507
|
+
try:
|
508
|
+
# Evaluate the expression and return the result
|
509
|
+
result = eval(python_expression, {'math': math, 'os': os, 'tlm': tlm})
|
510
|
+
except Exception as e:
|
511
|
+
raise ValueError(f"Error evaluating expression '{python_expression}': {e}")
|
512
|
+
return result
|
513
|
+
|
514
|
+
def evaluate_expressions(self, expressions):
|
515
|
+
# Convert each expression to a Python expression
|
516
|
+
results = []
|
517
|
+
for expr in expressions:
|
518
|
+
results.append(self.evaluate_expression(expr))
|
519
|
+
|
520
|
+
return results
|
521
|
+
|
522
|
+
def evaluate_tokens(self, tokens):
|
523
|
+
# Evaluates each token to clear any possible variables
|
524
|
+
results = []
|
525
|
+
for token in tokens:
|
526
|
+
results.append(self.evaluate_expression([token]))
|
527
|
+
return results
|
528
|
+
|
529
|
+
def handle_ask(self, tokens, line_no):
|
530
|
+
if len(tokens) != 3:
|
531
|
+
raise ValueError(f"Invalid ASK command format at line {line_no}")
|
532
|
+
|
533
|
+
variable = tokens[1]
|
534
|
+
question = tokens[2]
|
535
|
+
if (question.startswith('"') and question.endswith('"')) or (question.startswith("'") and question.endswith("'")):
|
536
|
+
# Remove quotes
|
537
|
+
question = question[1:-1]
|
538
|
+
answer = ask_string(question)
|
539
|
+
if answer is not None:
|
540
|
+
if answer[0] == '"' and answer[-1] == '"':
|
541
|
+
# Remove quotes and don't uppercase and don't eval
|
542
|
+
answer = answer[1:-1]
|
543
|
+
else:
|
544
|
+
# Tokenize answer
|
545
|
+
answer_tokens = self.cstol_tokenizer(answer)
|
546
|
+
|
547
|
+
# Evaluate answer
|
548
|
+
answer = self.evaluate_expression(answer_tokens)
|
549
|
+
|
550
|
+
# Uppercase
|
551
|
+
if isinstance(answer, str):
|
552
|
+
answer = answer.upper()
|
553
|
+
self.variables.local_variables[variable] = answer
|
554
|
+
|
555
|
+
def handle_check(self, tokens, line_no):
|
556
|
+
expressions = self.extract_expressions(tokens[1:], ",")
|
557
|
+
for expr in expressions:
|
558
|
+
format = None
|
559
|
+
if expr[0][0] == '%':
|
560
|
+
# Format string
|
561
|
+
# %X or %x Output in hexadecimal values Value is converted to an integer prior to applying the format
|
562
|
+
# %O or %o Output in octal values Value is converted to an integer prior to applying the format
|
563
|
+
# %B or %b Output in binary values Value is converted to an integer prior to applying the format
|
564
|
+
# %I or %i Output in decimal values Value is converted to an integer prior to applying the format
|
565
|
+
# %D or %d Output in decimal values Value is converted to an integer prior to applying the format
|
566
|
+
# %F or %f Output in floating point values Default for integer or raw value
|
567
|
+
# %E or %e Output in floating point values Default for float or EU value
|
568
|
+
format = expr[0][1:].upper()
|
569
|
+
if format not in ['X', 'O', 'B', 'I', 'D', 'F', 'E']:
|
570
|
+
raise ValueError(f"Invalid format %'{format}' in CHECK command at line {line_no}")
|
571
|
+
if len(expr) < 2:
|
572
|
+
raise ValueError(f"Missing value for format %'{format}' in CHECK command at line {line_no}")
|
573
|
+
# Remove format from the expression
|
574
|
+
expr = expr[1:]
|
575
|
+
|
576
|
+
# Check for VS
|
577
|
+
success = True
|
578
|
+
fail_message = None
|
579
|
+
source = None
|
580
|
+
value = None
|
581
|
+
vs_expressions = self.extract_expressions(expr, "VS")
|
582
|
+
if len(vs_expressions) > 1:
|
583
|
+
source = " ".join(vs_expressions[0])
|
584
|
+
value = self.evaluate_expression(vs_expressions[0])
|
585
|
+
# First split any non-timestamp tokens containing colons
|
586
|
+
vs_tokens = self.split_vs_tokens_on_colon(self.flatten(vs_expressions[1:]))
|
587
|
+
colon_expressions = self.extract_expressions(vs_tokens, ":")
|
588
|
+
if len(colon_expressions) > 1:
|
589
|
+
# Range check
|
590
|
+
if len(colon_expressions) != 2:
|
591
|
+
raise ValueError(f"Invalid range format at line {line_no}")
|
592
|
+
range_start = self.evaluate_expression(colon_expressions[0])
|
593
|
+
range_end = self.evaluate_expression(colon_expressions[1])
|
594
|
+
if range_start > range_end:
|
595
|
+
raise ValueError(f"Invalid VS range {range_start}:{range_end} at line {line_no}")
|
596
|
+
if (value < range_start) or (value > range_end):
|
597
|
+
success = False
|
598
|
+
fail_message = f"not in range {range_start}:{range_end}"
|
599
|
+
else:
|
600
|
+
# Single value check
|
601
|
+
vs_value = self.evaluate_expression(colon_expressions[0])
|
602
|
+
if value != vs_value:
|
603
|
+
success = False
|
604
|
+
fail_message = f"not equal to {vs_value}"
|
605
|
+
else:
|
606
|
+
# No VS - Just a print check
|
607
|
+
source = " ".join(expr)
|
608
|
+
value = self.evaluate_expression(expr)
|
609
|
+
|
610
|
+
formatted_value = None
|
611
|
+
if format is None:
|
612
|
+
formatted_value = str(value)
|
613
|
+
elif format in ['X', 'O', 'B', 'I', 'D']:
|
614
|
+
# Convert to integer
|
615
|
+
value = int(value)
|
616
|
+
if format == 'X':
|
617
|
+
formatted_value = (f"{value:X}")
|
618
|
+
elif format == 'O':
|
619
|
+
formatted_value = (f"{value:o}")
|
620
|
+
elif format == 'B':
|
621
|
+
formatted_value = (f"{value:b}")
|
622
|
+
elif format in ['I', 'D']:
|
623
|
+
formatted_value = str(value)
|
624
|
+
elif format in ['F', 'E']:
|
625
|
+
# Convert to float
|
626
|
+
value = float(value)
|
627
|
+
if format == 'F':
|
628
|
+
formatted_value = f"{value:.6f}"
|
629
|
+
elif format == 'E':
|
630
|
+
formatted_value = f"{value:.6e}"
|
631
|
+
|
632
|
+
if success:
|
633
|
+
print(f"CHECK SUCCESS: {source} = {formatted_value}")
|
634
|
+
else:
|
635
|
+
message = f"CHECK FAILED: {source} = {formatted_value} ({fail_message})"
|
636
|
+
print(message)
|
637
|
+
raise CheckError(message)
|
638
|
+
|
639
|
+
def handle_clear(self, tokens, line_no):
|
640
|
+
""" Only handles displays """
|
641
|
+
if tokens[1].upper() == 'ALL':
|
642
|
+
clear_all_screens()
|
643
|
+
else:
|
644
|
+
screen_name = tokens[1]
|
645
|
+
if (screen_name.startswith('"') and screen_name.endswith('"')) or (screen_name.startswith("'") and screen_name.endswith("'")):
|
646
|
+
# Remove quotes
|
647
|
+
screen_name = screen_name[1:-1]
|
648
|
+
clear_screen(*screen_name.split())
|
649
|
+
|
650
|
+
def handle_cmd(self, tokens, line_no):
|
651
|
+
if tokens[0].upper() == 'NOW':
|
652
|
+
# Drop NOW
|
653
|
+
tokens = tokens[1:]
|
654
|
+
|
655
|
+
verb = tokens[0].upper()
|
656
|
+
tokens = tokens[1:]
|
657
|
+
match verb:
|
658
|
+
case "ACTIVATE" | "ARM" | "BOOT" | "CHANGE" | "CLOSE" | "DISABLE" | "DISARM" | "DRIVE" | \
|
659
|
+
"DUMP" | "ENABLE" | "FIRE" | "FLYBACK" | "FORCE" | "GET" | "HALT" | "HOLD" | "IGNORE" | "INITIATE" | \
|
660
|
+
"MOVE" | "NOW" | "OPEN" | "PASS" | "PERFORM" | "RESET" | "SELECT" | "SET" | "SLEW" | "STEP" | "TEST" | \
|
661
|
+
"TOGGLE" | "TURN" | "USE":
|
662
|
+
# Handle all the weird verbs
|
663
|
+
if verb == "TURN" or verb == "FORCE":
|
664
|
+
# Expect ON or OFF to follow
|
665
|
+
if tokens[0].upper() == 'ON' or tokens[0].upper() == 'OFF':
|
666
|
+
verb = verb + tokens[0].upper()
|
667
|
+
tokens = tokens[1:]
|
668
|
+
else:
|
669
|
+
raise ValueError(f"TURN and FORCE must be followed by ON or OFF at line {line_no}")
|
670
|
+
case "CMD":
|
671
|
+
pass
|
672
|
+
|
673
|
+
# Now we need to discover any TO, BY, FROM, WITH clauses
|
674
|
+
target_name = None
|
675
|
+
cmd_name = None
|
676
|
+
args = {}
|
677
|
+
single_value_expressions = None
|
678
|
+
to_expressions = self.extract_expressions(tokens, "TO")
|
679
|
+
by_expressions = self.extract_expressions(tokens, "BY")
|
680
|
+
from_expressions = self.extract_expressions(tokens, "FROM")
|
681
|
+
with_expressions = self.extract_expressions(tokens, "WITH")
|
682
|
+
if len(to_expressions) > 1:
|
683
|
+
single_value_expressions = to_expressions
|
684
|
+
elif len(by_expressions) > 1:
|
685
|
+
single_value_expressions = by_expressions
|
686
|
+
elif len(from_expressions) > 1:
|
687
|
+
single_value_expressions = from_expressions
|
688
|
+
|
689
|
+
if single_value_expressions is not None:
|
690
|
+
# TO, BY, or FROM
|
691
|
+
value = self.evaluate_expression(single_value_expressions[1])
|
692
|
+
pretokens = self.evaluate_tokens(single_value_expressions[0])
|
693
|
+
if len(pretokens) == 1:
|
694
|
+
target_name = pretokens[0]
|
695
|
+
cmd_name = verb
|
696
|
+
args["VALUE"] = value
|
697
|
+
elif len(pretokens) == 2:
|
698
|
+
target_name = pretokens[0]
|
699
|
+
if verb == "CMD":
|
700
|
+
cmd_name = pretokens[1]
|
701
|
+
args["VALUE"] = value
|
702
|
+
else:
|
703
|
+
cmd_name = verb
|
704
|
+
args[pretokens[1]] = value
|
705
|
+
else:
|
706
|
+
raise ValueError(f"Malformed TO cmd at line {line_no}")
|
707
|
+
elif len(with_expressions) > 1:
|
708
|
+
# WITH
|
709
|
+
pretokens = self.evaluate_tokens(with_expressions[0])
|
710
|
+
target_name = pretokens[0]
|
711
|
+
if verb == "CMD":
|
712
|
+
cmd_name = pretokens[1]
|
713
|
+
else:
|
714
|
+
cmd_name = verb
|
715
|
+
expressions = self.extract_expressions(with_expressions[1])
|
716
|
+
for expr in expressions:
|
717
|
+
arg_name = self.evaluate_tokens([expr[0]])[0]
|
718
|
+
value = self.evaluate_expression(expr[1:])
|
719
|
+
args[arg_name] = value
|
720
|
+
else:
|
721
|
+
# No clauses
|
722
|
+
tokens = self.evaluate_tokens(tokens)
|
723
|
+
target_name = tokens[0]
|
724
|
+
if verb == "CMD":
|
725
|
+
cmd_name = tokens[1]
|
726
|
+
else:
|
727
|
+
cmd_name = verb
|
728
|
+
|
729
|
+
cmd(target_name, cmd_name, args)
|
730
|
+
|
731
|
+
def handle_declare(self, tokens, line_no):
|
732
|
+
""" We ignore ranges and allowed value lists """
|
733
|
+
# DECLARE mode variable-name = default-value [range value-list]
|
734
|
+
mode = tokens[1].upper()
|
735
|
+
if mode not in ['INPUT', 'VARIABLE', 'CONSTANT']:
|
736
|
+
raise ValueError(f"Invalid mode '{mode}' in DECLARE command at line {line_no}")
|
737
|
+
variable_name = tokens[2]
|
738
|
+
if not variable_name.startswith('$'):
|
739
|
+
raise ValueError(f"Variable name must start with '$' in DECLARE command at line {line_no}")
|
740
|
+
equals = tokens[3]
|
741
|
+
if equals != '=':
|
742
|
+
raise ValueError(f"Expected '=' after variable name in DECLARE command at line {line_no}")
|
743
|
+
#default_value = self.token_to_value(tokens[4])
|
744
|
+
default_value = self.evaluate_tokens([tokens[4]])[0]
|
745
|
+
self.variables.local_variables[variable_name] = default_value
|
746
|
+
|
747
|
+
def handle_display(self, tokens, line_no):
|
748
|
+
screen_name = tokens[1]
|
749
|
+
if (screen_name.startswith('"') and screen_name.endswith('"')) or (screen_name.startswith("'") and screen_name.endswith("'")):
|
750
|
+
# Remove quotes
|
751
|
+
screen_name = screen_name[1:-1]
|
752
|
+
display_screen(*screen_name.split())
|
753
|
+
|
754
|
+
def handle_else(self, tokens, lines, line_no):
|
755
|
+
if len(tokens) == 1 and tokens[0] == 'ELSE':
|
756
|
+
# The only way we ever hit an ELSE is if we were in a successful block beforehand
|
757
|
+
# Therefore goto the ENDIF
|
758
|
+
depth = 1
|
759
|
+
for i in range(line_no, len(lines)):
|
760
|
+
next_line = lines[i].strip().upper()
|
761
|
+
if next_line.startswith('IF '):
|
762
|
+
depth += 1
|
763
|
+
elif next_line.startswith('ENDIF') or next_line.startswith('END IF'):
|
764
|
+
depth -= 1
|
765
|
+
if depth == 0:
|
766
|
+
return i + 1
|
767
|
+
raise ValueError(f"No matching ENDIF for ELSE command at line {line_no}")
|
768
|
+
|
769
|
+
elif len(tokens) > 1:
|
770
|
+
if tokens[0] == 'ELSEIF':
|
771
|
+
return self.handle_if(tokens, lines, line_no)
|
772
|
+
elif (tokens[0] == 'ELSE' and tokens[1] == 'IF'):
|
773
|
+
return self.handle_if(tokens[1:], lines, line_no)
|
774
|
+
|
775
|
+
def handle_end(self, tokens, line_no):
|
776
|
+
if len(tokens) == 1 and tokens[0] == 'END':
|
777
|
+
raise ValueError(f"Unexpected END command at line {line_no}, expected ENDIF, ENDLOOP, ENDMACRO, or ENDPROC")
|
778
|
+
elif (len(tokens) == 1 and tokens[0] == 'ENDIF') or (tokens[0] == 'END' and tokens[1] == 'IF'):
|
779
|
+
pass
|
780
|
+
elif (len(tokens) == 1 and tokens[0] == 'ENDLOOP') or (tokens[0] == 'END' and tokens[1] == 'LOOP'):
|
781
|
+
if len(self.variables.loop_stack) > 0:
|
782
|
+
loop_info = self.variables.loop_stack[-1]
|
783
|
+
loop_info[2] += 1
|
784
|
+
if loop_info[1] is not None:
|
785
|
+
# Counted loop, decrement the count
|
786
|
+
loop_info[1] -= 1
|
787
|
+
if loop_info[1] > 0:
|
788
|
+
return loop_info[0]
|
789
|
+
else:
|
790
|
+
self.variables.loop_stack.pop()
|
791
|
+
return line_no + 1 # Continue to the next line
|
792
|
+
else:
|
793
|
+
# Infinite loop, just continue to the start of the loop
|
794
|
+
return loop_info[0]
|
795
|
+
return line_no + 1
|
796
|
+
|
797
|
+
def handle_escape(self, _tokens, lines, line_no):
|
798
|
+
# Find the matching ENDLOOP
|
799
|
+
depth = 1
|
800
|
+
for i in range(line_no, len(lines)):
|
801
|
+
next_line = lines[i].strip()
|
802
|
+
if next_line.startswith('LOOP'):
|
803
|
+
depth += 1
|
804
|
+
elif next_line.startswith('END LOOP') or next_line.startswith('ENDLOOP'):
|
805
|
+
depth -= 1
|
806
|
+
if depth == 0:
|
807
|
+
if len(self.variables.loop_stack) > 0:
|
808
|
+
self.variables.loop_stack.pop()
|
809
|
+
return i + 1
|
810
|
+
raise ValueError(f"No matching ENDLOOP found for ESCAPE command at line {line_no}")
|
811
|
+
|
812
|
+
def handle_go(self, tokens, lines, line_no):
|
813
|
+
if tokens[0] == 'GOTO' or (len(tokens) > 1 and tokens[0] == 'GO' and tokens[1] == 'TO'):
|
814
|
+
if (tokens[0] == 'GOTO' and len(tokens) < 2) or (tokens[0] == 'GO' and tokens[1] != 'TO' and len(tokens) < 3):
|
815
|
+
raise ValueError(f"Invalid GOTO command format at line {line_no}")
|
816
|
+
label = None
|
817
|
+
if tokens[0] == 'GOTO':
|
818
|
+
label = tokens[1] + ':'
|
819
|
+
else:
|
820
|
+
label = tokens[2] + ':'
|
821
|
+
# Find the label in the lines
|
822
|
+
for i in range(1, len(lines)):
|
823
|
+
next_line = lines[i - 1].strip()
|
824
|
+
if next_line.startswith(label):
|
825
|
+
return i
|
826
|
+
raise ValueError(f"Label '{label}' not found in GOTO command at line {line_no}")
|
827
|
+
|
828
|
+
def handle_if(self, tokens, lines, line_no):
|
829
|
+
expression_tokens = tokens[1:]
|
830
|
+
result = None
|
831
|
+
try:
|
832
|
+
# Evaluate the expression and store the result in the variable
|
833
|
+
result = self.evaluate_expression(expression_tokens)
|
834
|
+
except Exception as e:
|
835
|
+
raise ValueError(f"Error evaluating expression '{expression_tokens}' in IF command at line {line_no}: {e}")
|
836
|
+
|
837
|
+
if result:
|
838
|
+
return line_no + 1 # Continue to the next line if the condition is true
|
839
|
+
# If the condition is false, skip to the next ENDIF or ELSE
|
840
|
+
else:
|
841
|
+
# Find the matching ENDIF or ELSE
|
842
|
+
depth = 1
|
843
|
+
for i in range(line_no, len(lines)):
|
844
|
+
next_line = lines[i].strip().upper()
|
845
|
+
if next_line.startswith('IF '):
|
846
|
+
depth += 1
|
847
|
+
elif next_line.startswith('ENDIF') or next_line.startswith('END IF'):
|
848
|
+
depth -= 1
|
849
|
+
if depth == 0:
|
850
|
+
return i + 1
|
851
|
+
elif next_line.startswith('ELSE'):
|
852
|
+
if next_line.startswith('ELSEIF') or next_line.startswith('ELSE IF'):
|
853
|
+
# Continue to the ELSE IF
|
854
|
+
if depth == 1:
|
855
|
+
return i + 1
|
856
|
+
else:
|
857
|
+
# Regular ELSE - Continue to line after
|
858
|
+
if depth == 1:
|
859
|
+
return i + 2
|
860
|
+
|
861
|
+
raise ValueError(f"No matching ENDIF or ELSE found for IF command at line {line_no}")
|
862
|
+
|
863
|
+
def handle_let(self, tokens, line_no):
|
864
|
+
# LET variable = expression
|
865
|
+
if len(tokens) < 4:
|
866
|
+
raise ValueError(f"Invalid LET command format at line {line_no}")
|
867
|
+
variable_name = tokens[1]
|
868
|
+
item_name = None
|
869
|
+
expression_tokens = None
|
870
|
+
if not variable_name.startswith('$'):
|
871
|
+
if tokens[2] != '=':
|
872
|
+
# Global variable for set_tlm
|
873
|
+
item_name = tokens[2]
|
874
|
+
expression_tokens = tokens[4:]
|
875
|
+
else:
|
876
|
+
raise ValueError(f"Non-global variable name must start with '$' in LET command at line {line_no}")
|
877
|
+
elif tokens[2] == '=':
|
878
|
+
# Local or Special variable
|
879
|
+
expression_tokens = tokens[3:]
|
880
|
+
else:
|
881
|
+
raise ValueError(f"Expected '=' after variable name in LET command at line {line_no}")
|
882
|
+
|
883
|
+
result = None
|
884
|
+
try:
|
885
|
+
# Evaluate the expression and store the result in the variable
|
886
|
+
result = self.evaluate_expression(expression_tokens)
|
887
|
+
except Exception as e:
|
888
|
+
raise ValueError(f"Error evaluating expression '{expression_tokens}' in LET command at line {line_no}: {e}")
|
889
|
+
|
890
|
+
if item_name:
|
891
|
+
if isinstance(result, str):
|
892
|
+
set_tlm(f"{variable_name} LATEST {item_name} = '{result}'")
|
893
|
+
else:
|
894
|
+
set_tlm(f"{variable_name} LATEST {item_name} = {result}")
|
895
|
+
elif variable_name.startswith('$$'):
|
896
|
+
# Special variable
|
897
|
+
self.variables.set_special_variable(variable_name, result)
|
898
|
+
else:
|
899
|
+
# Local variable
|
900
|
+
self.variables.local_variables[variable_name] = result
|
901
|
+
|
902
|
+
def handle_load(self, tokens, line_no):
|
903
|
+
# LOAD external-element-name AT location FROM file-name
|
904
|
+
expressions = self.extract_expressions(tokens[1:], "AT")
|
905
|
+
if len(expressions) != 2:
|
906
|
+
raise ValueError(f"Invalid LOAD command format at line {line_no}")
|
907
|
+
interface_name = expressions[0]
|
908
|
+
expressions = self.extract_expressions(self.flatten(expressions[1:]), "FROM")
|
909
|
+
if len(expressions) != 2:
|
910
|
+
raise ValueError(f"Invalid LOAD command format at line {line_no}")
|
911
|
+
location = expressions[0]
|
912
|
+
filename = expressions[1]
|
913
|
+
results = self.evaluate_expressions([interface_name, location, filename])
|
914
|
+
interface_name = results[0]
|
915
|
+
location = results[1]
|
916
|
+
filename = results[2]
|
917
|
+
file = get_target_file(filename)
|
918
|
+
data = file.read()
|
919
|
+
file.close()
|
920
|
+
send_raw(interface_name, data)
|
921
|
+
|
922
|
+
def handle_loop(self, tokens, line_no):
|
923
|
+
if len(tokens) == 1:
|
924
|
+
# Infinite loop
|
925
|
+
self.variables.loop_stack.append([line_no + 1, None, 1])
|
926
|
+
return line_no + 1 # Continue to the next line
|
927
|
+
elif len(tokens) == 2:
|
928
|
+
# Counted loop
|
929
|
+
count = int(tokens[1])
|
930
|
+
self.variables.loop_stack.append([line_no + 1, count, 1])
|
931
|
+
return line_no + 1 # Continue to the next line
|
932
|
+
else:
|
933
|
+
raise ValueError(f"Invalid LOOP command format at line {line_no}")
|
934
|
+
|
935
|
+
def handle_proc(self, tokens, line_no):
|
936
|
+
# tokens[1] is proc name
|
937
|
+
if len(tokens) > 2:
|
938
|
+
# Variables
|
939
|
+
index = 0
|
940
|
+
for token in tokens[2:]:
|
941
|
+
if token == ',':
|
942
|
+
continue
|
943
|
+
elif token.startswith('$'):
|
944
|
+
self.variables.local_variables[token] = os.getenv(f"CSTOL_ARG_{index}")
|
945
|
+
index += 1
|
946
|
+
else:
|
947
|
+
raise ValueError(f"Invalid variable '{token}' in PROC command at line {line_no}")
|
948
|
+
|
949
|
+
def handle_return(self, tokens, lines, line_no):
|
950
|
+
if len(tokens) > 1:
|
951
|
+
if tokens[1].upper() == 'ALL':
|
952
|
+
raise StopScript
|
953
|
+
return (len(lines) + 1)
|
954
|
+
|
955
|
+
def handle_run(self, tokens, line_no):
|
956
|
+
if len(tokens) < 2:
|
957
|
+
raise ValueError(f"Invalid RUN command format at line {line_no}")
|
958
|
+
script = tokens[1]
|
959
|
+
if len(tokens) > 2:
|
960
|
+
expressions = self.extract_expressions(tokens[2:], ",")
|
961
|
+
results = self.evaluate_expressions(expressions)
|
962
|
+
string_results = [('"' + str(result) + '"') for result in results]
|
963
|
+
string_results.insert(0, script)
|
964
|
+
script = ' '.join(string_results)
|
965
|
+
print(f"Running system call: {script}")
|
966
|
+
os.system(script)
|
967
|
+
|
968
|
+
def handle_send(self, tokens, line_no):
|
969
|
+
# SEND bit-pattern-command TO external-element-name
|
970
|
+
# COSMOS implementation uses interface name instead of external-element-name
|
971
|
+
expressions = self.extract_expressions(tokens[1:], "TO")
|
972
|
+
if len(expressions) != 2:
|
973
|
+
raise ValueError(f"Invalid SEND command format at line {line_no}")
|
974
|
+
results = self.evaluate_expressions(expressions)
|
975
|
+
data = results[0]
|
976
|
+
interface_name = results[1]
|
977
|
+
send_raw(interface_name, data)
|
978
|
+
|
979
|
+
def handle_start(self, tokens, line_no):
|
980
|
+
# START proc-name [argument-list]
|
981
|
+
proc_name = tokens[1]
|
982
|
+
|
983
|
+
if (proc_name.startswith('"') and proc_name.endswith('"')) or (proc_name.startswith("'") and proc_name.endswith("'")):
|
984
|
+
# Remove quotes
|
985
|
+
proc_name = proc_name[1:-1]
|
986
|
+
|
987
|
+
args = []
|
988
|
+
if len(tokens) > 2:
|
989
|
+
# Handle argument list
|
990
|
+
expressions = self.extract_expressions(tokens[2:], ",")
|
991
|
+
results = self.evaluate_expressions(expressions)
|
992
|
+
args = [str(result) for result in results]
|
993
|
+
for index, arg in enumerate(args):
|
994
|
+
# Set the environment variable for the argument
|
995
|
+
os.environ[f"CSTOL_ARG_{index}"] = arg
|
996
|
+
start(proc_name, bind_variables=False)
|
997
|
+
|
998
|
+
def handle_switch(self, tokens, line_no):
|
999
|
+
action = tokens[1].upper()
|
1000
|
+
if action not in ['ON', 'OFF']:
|
1001
|
+
raise ValueError(f"Invalid SWITCH action '{action}' at line {line_no}")
|
1002
|
+
interface_name = tokens[2]
|
1003
|
+
if action == 'ON':
|
1004
|
+
connect_interface(interface_name)
|
1005
|
+
elif action == 'OFF':
|
1006
|
+
disconnect_interface(interface_name)
|
1007
|
+
|
1008
|
+
def handle_wait(self, tokens, line_no):
|
1009
|
+
if len(tokens) == 1:
|
1010
|
+
# Indefinite wait
|
1011
|
+
wait()
|
1012
|
+
elif len(tokens) == 2:
|
1013
|
+
# Timestamp wait - Still need to handle a possible expression like a variable
|
1014
|
+
seconds = self.evaluate_expression([tokens[1]])
|
1015
|
+
if isinstance(seconds, (int, float, complex)) and not isinstance(seconds, bool):
|
1016
|
+
if seconds > self.ONE_YEAR_SECONDS:
|
1017
|
+
now = datetime.datetime.now().timestamp()
|
1018
|
+
seconds = seconds - now
|
1019
|
+
if seconds < 0:
|
1020
|
+
seconds = 0.0
|
1021
|
+
wait(seconds)
|
1022
|
+
else:
|
1023
|
+
raise ValueError(f"Invalid timestamp format at line {line_no}")
|
1024
|
+
else:
|
1025
|
+
# Conditional expression wait
|
1026
|
+
expressions = self.extract_expressions(tokens[1:], seperator="OR")
|
1027
|
+
python_expression = self.build_python_expression(expressions[0])
|
1028
|
+
if len(expressions) > 1:
|
1029
|
+
# Timeout given with "OR FOR" or "OR UNTIL"
|
1030
|
+
seconds = self.evaluate_expression(expressions[1][1:]) # Drop assumed FOR or UNTIL token
|
1031
|
+
if isinstance(seconds, (int, float, complex)) and not isinstance(seconds, bool):
|
1032
|
+
if seconds > self.ONE_YEAR_SECONDS:
|
1033
|
+
now = datetime.datetime.now().timestamp()
|
1034
|
+
seconds = seconds - now
|
1035
|
+
if seconds < 0:
|
1036
|
+
seconds = 0.0
|
1037
|
+
result = wait_expression(python_expression, seconds, self.variables.get_special_variable("$$CHECK_INTERVAL"), globals={'math': math, 'os': os, 'tlm': tlm})
|
1038
|
+
if result:
|
1039
|
+
self.variables.set_special_variable("$$ERROR", "NO_ERROR")
|
1040
|
+
else:
|
1041
|
+
self.variables.set_special_variable("$$ERROR", "TIME_OUT")
|
1042
|
+
else:
|
1043
|
+
raise ValueError(f"Invalid timestamp format at line {line_no}")
|
1044
|
+
else:
|
1045
|
+
result = wait_expression(python_expression, 1000000000, self.variables.get_special_variable("$$CHECK_INTERVAL"), globals={'math': math, 'os': os, 'tlm': tlm}) # Effective infinite wait
|
1046
|
+
if result:
|
1047
|
+
self.variables.set_special_variable("$$ERROR", "NO_ERROR")
|
1048
|
+
else:
|
1049
|
+
self.variables.set_special_variable("$$ERROR", "TIME_OUT")
|
1050
|
+
|
1051
|
+
def handle_write(self, tokens, line_no):
|
1052
|
+
expressions = self.extract_expressions(tokens[1:], ",")
|
1053
|
+
results = []
|
1054
|
+
for expr in expressions:
|
1055
|
+
result = ''
|
1056
|
+
if expr[0][0] == '%':
|
1057
|
+
# Format string
|
1058
|
+
# %X or %x Output in hexadecimal values Value is converted to an integer prior to applying the format
|
1059
|
+
# %O or %o Output in octal values Value is converted to an integer prior to applying the format
|
1060
|
+
# %B or %b Output in binary values Value is converted to an integer prior to applying the format
|
1061
|
+
# %I or %i Output in decimal values Value is converted to an integer prior to applying the format
|
1062
|
+
# %D or %d Output in decimal values Value is converted to an integer prior to applying the format
|
1063
|
+
# %F or %f Output in floating point values Default for integer or raw value
|
1064
|
+
# %E or %e Output in floating point values Default for float or EU value
|
1065
|
+
format = expr[0][1:].upper()
|
1066
|
+
if format not in ['X', 'O', 'B', 'I', 'D', 'F', 'E']:
|
1067
|
+
raise ValueError(f"Invalid format %'{format}' in WRITE command at line {line_no}")
|
1068
|
+
if len(expr) < 2:
|
1069
|
+
raise ValueError(f"Missing value for format %'{format}' in WRITE command at line {line_no}")
|
1070
|
+
value = self.evaluate_expression(expr[1:])
|
1071
|
+
if format in ['X', 'O', 'B', 'I', 'D']:
|
1072
|
+
# Convert to integer
|
1073
|
+
value = int(value)
|
1074
|
+
if format == 'X':
|
1075
|
+
result = (f"{value:X}")
|
1076
|
+
elif format == 'O':
|
1077
|
+
result = (f"{value:o}")
|
1078
|
+
elif format == 'B':
|
1079
|
+
result = (f"{value:b}")
|
1080
|
+
elif format in ['I', 'D']:
|
1081
|
+
result = str(value)
|
1082
|
+
elif format in ['F', 'E']:
|
1083
|
+
# Convert to float
|
1084
|
+
value = float(value)
|
1085
|
+
if format == 'F':
|
1086
|
+
result = f"{value:.6f}"
|
1087
|
+
elif format == 'E':
|
1088
|
+
result = f"{value:.6e}"
|
1089
|
+
else:
|
1090
|
+
result = str(self.evaluate_expression(expr))
|
1091
|
+
results.append(result)
|
1092
|
+
# Join the results with spaces
|
1093
|
+
output = ' '.join(results)
|
1094
|
+
print(output)
|
1095
|
+
|
1096
|
+
def run_text(self, text, filename = None, line_no = 1, end_line_no = None, bind_variables = False):
|
1097
|
+
saved_variables = self.variables
|
1098
|
+
try:
|
1099
|
+
if not bind_variables:
|
1100
|
+
self.variables = CstolVariables()
|
1101
|
+
super().run_text(text, filename, line_no, end_line_no, bind_variables)
|
1102
|
+
finally:
|
1103
|
+
self.variables = saved_variables
|
1104
|
+
|
1105
|
+
# Override this method in the subclass to implement the script engine
|
1106
|
+
def run_line(self, line, lines, filename, line_no):
|
1107
|
+
tokens = self.cstol_tokenizer(line)
|
1108
|
+
|
1109
|
+
# Skip completely blank lines
|
1110
|
+
if len(tokens) == 0:
|
1111
|
+
return line_no + 1
|
1112
|
+
|
1113
|
+
# Handle continuation lines
|
1114
|
+
if self.saved_tokens:
|
1115
|
+
tokens = self.saved_tokens + tokens
|
1116
|
+
self.saved_tokens = None
|
1117
|
+
if tokens[-1] == '&':
|
1118
|
+
self.saved_tokens = tokens[:-1] # Everything before the '&' is saved for the next line
|
1119
|
+
return line_no + 1 # Skip to the next line
|
1120
|
+
|
1121
|
+
# Remove any trailing comments
|
1122
|
+
if ';' in tokens:
|
1123
|
+
comment_index = tokens.index(';')
|
1124
|
+
tokens = tokens[:comment_index]
|
1125
|
+
|
1126
|
+
if len(tokens) != 0:
|
1127
|
+
keyword = tokens[0].upper()
|
1128
|
+
|
1129
|
+
# Handle labels
|
1130
|
+
if keyword.endswith(':'):
|
1131
|
+
if len(tokens) > 1:
|
1132
|
+
tokens = tokens[1:]
|
1133
|
+
keyword = tokens[0].upper()
|
1134
|
+
else:
|
1135
|
+
return line_no + 1
|
1136
|
+
|
1137
|
+
match keyword:
|
1138
|
+
case "ASK":
|
1139
|
+
self.handle_ask(tokens, line_no)
|
1140
|
+
case "BEGIN":
|
1141
|
+
pass
|
1142
|
+
case "CANCEL" | "CHECKPOINT" | "COMMIT" | "COMPILE" | "CSTOL" | "DECOMPILE" | "DEFINE" | "DELETE" | \
|
1143
|
+
"FLUSH" | "INSERT" | "LOCK" | "MACRO" | "RECORD" | "REPORT" | "RESTORE" | "RETREIVE" | "RETRY" | \
|
1144
|
+
"SHOW" | "SNAP" | "STOP" | "UNDEFINE" | "UNLOCK" | "UPDATE" | "ROUTE":
|
1145
|
+
# These keywords are noops for this CSTOL script engine
|
1146
|
+
print(f"Ignoring Unsupported Keyword: {keyword}")
|
1147
|
+
case "CHECK":
|
1148
|
+
self.handle_check(tokens, line_no)
|
1149
|
+
case "CLEAR":
|
1150
|
+
self.handle_clear(tokens, line_no)
|
1151
|
+
case "ACTIVATE" | "ARM" | "BOOT" | "CHANGE" | "CLOSE" | "CMD" | "DISABLE" | "DISARM" | "DRIVE" | \
|
1152
|
+
"DUMP" | "ENABLE" | "FIRE" | "FLYBACK" | "FORCE" | "GET" | "HALT" | "HOLD" | "IGNORE" | "INITIATE" | \
|
1153
|
+
"MOVE" | "NOW" | "OPEN" | "PASS" | "PERFORM" | "RESET" | "SELECT" | "SET" | "SLEW" | "STEP" | "TEST" | \
|
1154
|
+
"TOGGLE" | "TURN" | "USE":
|
1155
|
+
self.handle_cmd(tokens, line_no)
|
1156
|
+
case "DECLARE":
|
1157
|
+
self.handle_declare(tokens, line_no)
|
1158
|
+
case "DISPLAY":
|
1159
|
+
self.handle_display(tokens, line_no)
|
1160
|
+
case "ELSE" | "ELSEIF":
|
1161
|
+
return self.handle_else(tokens, lines, line_no)
|
1162
|
+
case "END" | "ENDIF" | "ENDLOOP" | "ENDMACRO" | "ENDPROC":
|
1163
|
+
return self.handle_end(tokens, line_no)
|
1164
|
+
case "ESCAPE":
|
1165
|
+
return self.handle_escape(tokens, lines, line_no)
|
1166
|
+
case "GO" | "GOTO":
|
1167
|
+
return self.handle_go(tokens, lines, line_no)
|
1168
|
+
case "IF":
|
1169
|
+
return self.handle_if(tokens, lines, line_no)
|
1170
|
+
case "LET":
|
1171
|
+
self.handle_let(tokens, line_no)
|
1172
|
+
case "LOAD":
|
1173
|
+
self.handle_load(tokens, line_no)
|
1174
|
+
case "LOOP":
|
1175
|
+
self.handle_loop(tokens, line_no)
|
1176
|
+
case "PROC":
|
1177
|
+
self.handle_proc(tokens, line_no)
|
1178
|
+
case "RETURN":
|
1179
|
+
return self.handle_return(tokens, lines, line_no)
|
1180
|
+
case "RUN":
|
1181
|
+
self.handle_run(tokens, line_no)
|
1182
|
+
case "SEND":
|
1183
|
+
self.handle_send(tokens, line_no)
|
1184
|
+
case "START":
|
1185
|
+
self.handle_start(tokens, line_no)
|
1186
|
+
case "SWITCH":
|
1187
|
+
self.handle_switch(tokens, line_no)
|
1188
|
+
case "WAIT":
|
1189
|
+
self.handle_wait(tokens, line_no)
|
1190
|
+
case "WRITE":
|
1191
|
+
self.handle_write(tokens, line_no)
|
1192
|
+
case _:
|
1193
|
+
raise ValueError(f"Unknown keyword '{keyword}' at line {line_no}")
|
1194
|
+
|
1195
|
+
return line_no + 1
|