-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvariable_server.py
More file actions
1240 lines (1079 loc) · 41 KB
/
Copy pathvariable_server.py
File metadata and controls
1240 lines (1079 loc) · 41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
This module contains tools for communicating with a sim's variable
server, primarily via the Variable and VariableServer classes. See
https://nasa.github.io/trick/documentation/miscellaneous_trick_tools/Python-Variable-Server-Client.html for
a tutorial and examples.
"""
from collections import namedtuple
import os
import re
import socket
import struct
import threading
import time
# In Python 2, basestring is the parent for both str (ASCII) and unicode.
# In Python 3, str and unicode were unified into str, and basestring is gone.
try:
basestring
except NameError:
basestring = str
# In Python 3, itertools.izip became zip.
try:
from itertools import izip as zip
except ImportError:
pass
class VariableServerError(Exception):
'''
Variable Server communication I/O error.
'''
pass
class UnitsConversionError(VariableServerError):
"""
Raised when a units conversion fails.
Attributes
----------
name : str
The name of the variable for which the conversion failed.
units : str
The units to which the variable could not be converted.
"""
def __init__(self, name, units):
super(UnitsConversionError, self).__init__(
'[{0}] cannot be converted to [{1}]'.format(name, units)
)
self.name = name
self.units = units
class UnexpectedMessageError(VariableServerError):
"""
Raised when a received message is not of the expected type.
Attributes
----------
expected_id : int
The expected message indicator.
actual_id : int
The actual message indicator.
"""
def __init__(self, expected_id, actual_id):
super(UnexpectedMessageError, self).__init__(
'Unexpected message received. Expected ID = {0}. Actual ID = {1}'
.format(expected_id, actual_id)
)
self.expected_id = expected_id
self.actual_id = actual_id
class ValueCountError(VariableServerError):
"""
Raised when the number of received variable values does not match
the expected count.
Attributes
----------
expected : int
The expected count.
actual : int
The actual count.
"""
def __init__(self, expected, actual):
super(ValueCountError, self).__init__(
'Number of values received ({0}) does not match expected ({1})'
.format(actual, expected)
)
self.expected = expected
self.actual = actual
def _create_enum(name, field_names, ordinal_values=True):
"""
Create a namedtuple with automatic values.
Parameters
----------
name : str
The name of the namedtuple.
field_names : iterable of str
The field names.
ordinal_values : bool
True to assign each field an ordinal number, starting at 0. This
creates a classic number-based enum.
False to use the field names themselves as the values for the
fields. This creates a string-based enum.
"""
return namedtuple(name, field_names)(
*(range(len(field_names)) if ordinal_values else field_names)
)
class Message(namedtuple('Message', ['indicator', 'data'])):
"""
A message from the variable server.
Attributes
----------
indicator : int
The indicator.
data : str
The rest of the message.
"""
Indicator = _create_enum('Indicator', ['VAR_SEND', 'VAR_EXISTS'])
class Variable(object):
"""
A variable whose value and units will be updated from the sim. You
should not directly change any part of this class.
Attributes
----------
name : str
The fully-qualified name.
units : str
The units. Use 'xx' to specify default units.
Properties
----------
value : type returned by the type_ parameter of __init__ (or None)
The value.
"""
def __init__(self, name, units=None, type_=str):
"""
Create a new Variable.
Parameters
----------
name and units are as documented in this class's Attributes
section.
type_ : callable
A callable that accepts one argument and is used to convert
the stringified value from the variable server to the
desired type. This can be a builtin like int or float or a
custom function that does anything you like!
"""
self.name = name
self.units = units
self._type = type_
self._value = None
@property
def value(self):
'''
Get the converted value.
'''
return self._type(self._value)
@value.setter
def value(self, value):
'''
Set the value.
'''
self._value = value
def __str__(self):
return self.name
def __repr__(self):
return '{0} = {1}{2}'.format(
self.name,
self.value,
' {0}'.format(self.units) if self.units is not None else '')
def _connect_ipv4(hostname, port):
# Prefer explicit IPv4; fall back to first AF_INET match
for ai in socket.getaddrinfo(hostname, port, socket.AF_INET, socket.SOCK_STREAM):
af, socktype, proto, _, sa = ai
s = socket.socket(af, socktype, proto)
try:
s.connect(sa)
return s
except Exception:
s.close()
raise OSError(f"Could not connect to {hostname}:{port} over IPv4")
class VariableServer(object):
"""
Send commands to and receive responses from a simulation's
variable server.
You must call close on instances of this class to release resources
allocated during initialization.
"""
Channel = _create_enum('Channel', ['ASYNC', 'SYNC', 'BOTH'], False)
CopyMode = _create_enum('CopyMode', ['ASYNC', 'SCHEDULED', 'TOP_OF_FRAME'])
def __init__(self, hostname, port):
"""
Create a connection to the simulation variable server at
host:port.
Parameters
----------
hostname : str
The name of the machine that is running the simulation to
which you want to connect.
port : int
The port on which the simulation's variable server is
listening.
"""
self._variables = []
self._callbacks = {}
self._error_callbacks = {}
self._lock = threading.Lock()
port = int(port)
# self._synchronous_socket = socket.create_connection((hostname, port))
# self._asynchronous_socket = socket.create_connection((hostname, port))
self._synchronous_socket = _connect_ipv4(hostname, port)
self._asynchronous_socket = _connect_ipv4(hostname, port)
self._synchronous_file_interface = self._synchronous_socket.makefile()
self._asynchronous_file_interface = self._asynchronous_socket.makefile()
self._open = True
self.pause(channel=self.Channel.SYNC)
# Define a local function to be used by the sampling thread.
def update_variables():
'''
Continuously update variables.
'''
while True:
try:
values = self._read_values(False)
except Exception as exception:
if self._open:
for function, args in self._error_callbacks.items():
function(*args[0], exception=exception, **args[1])
return
# We must lock here to ensure that variables are not
# removed while we are processing an update.
with self._lock:
# If there are more values than variables, it must
# be that a variable was removed after this message
# was sent but before we processed it. Variables can
# be removed from any place within the list, so we
# don't know which "extra" values to ignore.
# We therefore discard the entire message.
#
# If there are fewer values than variables, it must
# be that a variable was added after this message
# was sent but before we processed it. Variables
# can only be appended to the list, so we know that
# any missing values are for variables at the end of
# the list. We can therefore still use all the
# values to update variables at the front of the
# list.
#
# We could still end up assigning values to the
# wrong variables if someone removed AND added
# variables after a message was sent but before we
# processed it, but the probability of someone
# doing that doesn't justify the work at this point.
# Besides, it would be corrected with the next
# message.
if len(values) <= len(self._variables):
for variable, value in zip(
self._variables, values):
variable.value, variable.units = \
_parse_value(value)
for function, args in self._callbacks.items():
function(*args[0], **args[1])
# Start a thread that listens for data from the variable server,
# updates the variables being sampled, and notifies listeners.
# This thread can only be terminated by calling close. A Python
# process cannot terminate while non-daemon threads are running,
# so make this tread a deamon in case the user fails to call
# close when finished with this instance.
self._thread = threading.Thread(
target=update_variables, name='Asynchronous Variable Sampler')
self._thread.daemon = True
self._thread.start()
def __del__(self):
"""
Call close in case the user forgot. Don't rely on this to clean
up for you. You should really explicitly call close yourself.
"""
try:
self.close()
except:
pass
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def get_value(self, name, units=None, type_=str):
"""
Get the value of the named variable. If units are specified, the
value is converted if possible. This function provides a simple
interface and is most useful when you just want to get the value
of one variable and don't require periodic sampling. For a more
powerful but complex function, see get_values. To sample values
periodically, see add_variables.
Parameters
----------
name, units, and type_ are as documented in Variable.
Returns
-------
type returned by type_
The result of calling type_ on the variable's value.
Raises
------
IOError
If the remote endpoint has closed the connection.
UnexpectedMessageError
If the next message is not a set of variable values.
ValueCountError
If more than one value is received.
UnitsConversionError
If units are specified and the conversion fails.
Additional errors may be raised by type_.
Examples
--------
Get value (default type is string):
>>> from variable_server import VariableServer
>>> with VariableServer('localhost', 7000) as vs:
... vs.get_value('ball.obj.state.input.position[0]')
'5'
Get value as an int:
>>> from variable_server import VariableServer
>>> with VariableServer('localhost', 7000) as vs:
... vs.get_value('ball.obj.state.input.position[0]',
... type_=int)
5
Get value as a float in millimeters:
>>> from variable_server import VariableServer
>>> with VariableServer('localhost', 7000) as vs:
... vs.get_value('ball.obj.state.input.position[0]',
... units='mm', type_=float)
5000.0
Convert value with custom function:
>>> from variable_server import VariableServer
>>> with VariableServer('localhost', 7000) as vs:
... vs.get_value('ball.obj.state.input.position[0]',
... type_=lambda x: int(x) * 2)
10
"""
# add the variable and poll its value
self._var_add(name, units, self.Channel.SYNC)
self._var_send()
self._var_clear(self.Channel.SYNC)
# make sure we only got one
values = self._read_values()
_assert_value_count(1, len(values))
# check for units conversion
value, actual_units = _parse_value(values[0])
_assert_units_conversion(name, units, actual_units)
return type_(value)
def set_value(self, name, value, units=None):
"""
Set the value of the named variable. If units are specified, the
value is converted if possible. If the convserion fails, the
value is unchanged, and no error is raised.
Attributes
----------
name : str
The fully-qualified name.
value : any
The value. This should be of the type expected by the
variable server.
units : str
The units.
"""
self.send(
'trick.var_set("{0}", {1}{2})'.format(
name,
'"{0}"'.format(value) if isinstance(value, basestring) else value,
', "{0}"'.format(units) if units is not None else ''))
def get_values(self, *variables):
"""
Get the values of variables. If units are specified, values are
converted if possible. Each argument is also updated in place,
so you can ignore the returned list of values if you were just
going to store them anyway. The list is useful, for example, for
inlining the results, eliminating the need to update and use
the variables in separate statements.
This function is more efficient than calling get_value
for each variable, but has a steeper learning curve. It is most
useful when you want to fetch multiple variables' values and
units and don't require periodic sampling. To sample values
periodically, see add_variables.
Parameters
----------
variables : zero or more Variables
The variables for which to fetch values and units.
Returns
-------
[any]
A list of the requested values in order. The type of each
element is that returned by the corresponding Variable's
type_.
Raises
------
IOError
If the remote endpoint has closed the connection.
UnexpectedMessageError
If the next message is not a set of variable values.
ValueCountError
If the number of received values does not match the number
of variables.
UnitsConversionError
If units are specified and the conversion fails. In this
case, variables before the error will have been updated,
but variables after will not. The variable for which the
error occurred will not be updated.
Additional errors may be raised by each Variables's type_, but
they do not prevent the rest of the Variables from being
updated. Variables whose type_ conversion fails will raise an
error every time their value property is accessed.
Example
-------
>>> from variable_server import Variable, VariableServer
>>> position = Variable('ball.obj.state.input.position[0]',
... type_=int)
>>> mass = Variable('ball.obj.state.input.mass', units='g',
... type_=float)
>>> with VariableServer('localhost', 7000) as vs:
... vs.get_values(position, mass)
[5, 10000.0]
>>> position
ball.obj.state.input.position[0] = 5 m
>>> mass
ball.obj.state.input.mass = 10000.0 g
"""
# check for zero arguments
if not variables:
return []
# add all the variables and poll their values
for variable in variables:
self._var_add(
variable.name,
variable.units if variable.units is not None else 'xx',
self.Channel.SYNC)
self._var_send()
self._var_clear(self.Channel.SYNC)
# make sure we got as many as expected
values = self._read_values()
_assert_value_count(len(variables), len(values))
# update each Variable, checking units conversions
for variable, entry in zip(variables, values):
value, units = _parse_value(entry)
if variable.units is not None:
_assert_units_conversion(variable.name, variable.units, units)
else:
variable.units = units
variable.value = value
return [variable.value for variable in variables]
def add_variables(self, *variables):
"""
Immediately update and begin periodically sampling the given
variables. This class retains references to all passed-in
variables and updates their values and units when new values
arrive from the variable server. All sampling is performed on a
separate thread, but accessing a variable's fields is always
safe. Adding variables which are already being sampled has no
effect. To set the sampling period, see set_period. To register
a function to be called whenever variables are updated, see
register_callback.
Parameters
----------
variables : zero or more Variables
The variables to begin sampling.
Raises
------
IOError
If the remote endpoint has closed the connection.
UnexpectedMessageError
If the next message is not a set of variable values.
ValueCountError
If the number of received values does not match the number
of variables.
UnitsConversionError
If units are specified and the conversion fails. In this
case, variables before the error will have been updated,
but variables after will not. The variable for which the
error occurred will not be updated.
Additional errors may be raised by each Variables's type_, but
they do not prevent the rest of the Variables from being
updated. Variables whose type_ conversion fails will raise an
error every time their value property is accessed.
If any error occurs, no variables are scheduled for sampling.
"""
# remove existing variables
variables = [variable for variable in variables
if variable not in self._variables]
# check for type_ and units conversion errors
self.get_values(*variables)
for variable in variables:
# No lock is needed here because:
# - appending to the variables list while
# update_variables is executing does not invalidate
# the length check
# - zip is bounded by the number of values, and the
# length check ensures there are at least as many
# variables as values
self._variables.append(variable)
self._var_add(
variable.name,
variable.units if variable.units is not None else 'xx')
def remove_variables(self, *variables):
"""
Stop sampling the given variables. Removing variables that are
not being sampled has no effect.
Parameters
----------
variables : zero or more Variables
The variables to stop sampling.
"""
for variable in variables:
if variable in self._variables:
# Variables must not be removed while update_variables
# is processing an update. See its comment for an
# explanation.
with self._lock:
self._variables.remove(variable)
self._var_remove(variable.name)
def remove_all_variables(self):
"""
Stop sampling all variables. To merely suspend sampling,
see pause.
"""
self._var_clear()
# No lock is needed here because:
# - This assignment is atomic.
# - If update_variables has already called zip, the list it is
# ierating over is unchanged as we're assigning a new list
# here instead of clearing the shared reference.
# - If update_variables has not yet called zip, when it does,
# zip will return an empty generator, terminating the loop.
self._variables = []
def set_units(self, name, units):
"""
Set the units in which the named variable is sampled. This only
applies to Variables being periodically sampled.
See add_variables.
Parameters
----------
name : str
The variable's name.
units : str
The units to which to convert the sampled value.
"""
self.send('trick.var_units("{0}", "{1}")'.format(name, units),
self.Channel.ASYNC)
def set_period(self, period):
"""
Set the sampling period (in seconds).
Parameters
----------
period : float
The inverse of the rate (in Hz) at which you want to sample
variable values.
"""
self.send('trick.var_cycle({0})'.format(float(period)),
self.Channel.ASYNC)
def register_callback(self, function, args=None, kwargs=None):
"""
Call function whenever new variable values are sampled.
Registering an aleady-registered function replaces its existing
registration. The order in which functions are called is not
specified. Functions are executed on the asynchronous sampling
thread.
Paramaters
----------
function : callable
The function to call.
args : tuple
The positional arguments to be passed to the function.
kwargs : dict
The keyword arguments to be passed to the function.
"""
if args is None:
args = []
if kwargs is None:
kwargs = {}
self._callbacks[function] = args, kwargs
def deregister_callback(self, function):
"""
Do not call function whenever new variable values are sampled.
Deregistering an unregistered function has no effect.
Parameters
----------
function : any
A function previously added via register_callback.
"""
self._callbacks.pop(function, None)
def register_error_callback(self, function, args=None, kwargs=None):
"""
Call function if an error occurs while sampling variable values.
Registering an aleady-registered function replaces its existing
registration. The order in which functions are called is not
specified. Functions are executed on the asynchronous sampling
thread.
Paramaters
----------
function : callable
The function to call. It must accept a keyword argument
named 'exception' which will contain the error.
args : tuple
The positional arguments to be passed to the function.
kwargs : dict
The keyword arguments to be passed to the function.
"""
if args is None:
args = []
if kwargs is None:
kwargs = {}
self._error_callbacks[function] = args, kwargs
def deregister_error_callback(self, function):
"""
Do not call function if an error occurs while sampling variable
values. Deregistering an unregistered function has no effect.
Parameters
----------
function : any
A function previously added via register_error_callback.
"""
self._error_callbacks.pop(function, None)
def pause(self, pause=True, channel=Channel.ASYNC):
"""
Pause or unpause sampling.
Parameters
----------
pause : bool
True to pause sampling.
False to resume sampling.
channel : Channel
The channel to affect. You should almost certainly leave
this as the default.
"""
self.send('trick.var_{0}pause()'.format('' if pause else 'un'),
channel)
def set_debug(self, level, channel=Channel.BOTH):
"""
Set the debugging level. This effects how much information the
sim's variable server outputs to the sim's standard output
stream. It is not a debugging option for this Python class and
does not produce output on this process.
Parameters
----------
level : int
The debugging level. 0 = no debugging.
channel : Channel
The channel to affect.
"""
self.send('trick.var_debug({0})'.format(int(level)), channel)
def set_tag(self, tag):
"""
Set the identifier for this variable server client. The tag is
used in log files to associate each message with its sender.
Parameters
----------
tag : str
An identifier for this client.
"""
for channel in [self.Channel.SYNC, self.Channel.ASYNC]:
self.send(
'trick.var_set_client_tag("{0}_{1}")'.format(tag, channel),
channel)
def set_copy_mode(self, mode=CopyMode.ASYNC, channel=Channel.BOTH):
"""
Set the method by which variable values are copied.
Parameters
----------
mode : CopyMode
ASYNC
Values are copied by an independent thread. This has no
effect on simulation execution, but values within a set may
not all be from the same sim frame.
SCHEDULED
Values are copied as an automatic_last job in the sim's main
thread. Values within a set are guaranteed to be from the
same sim frame.
TOP_OF_FRAME
Values are copied at the top of each sim frame in the main
thread. Values within a set are guaranteed to be from the
same sim frame.
channel : Channel
The channel to affect.
"""
self.send('trick.var_set_copy_mode({0})'.format(int(mode)), channel)
def send_on_copy(self, enable=True):
"""
Set the method by which variable values are sent.
Parameters
----------
enable : bool
True to cause the variable server to send variable values
immediately after copying them, using the same thread, which
will impact simulation execution if the copy mode is not
ASYNCHRONOUS.
False to send values asynchronously on an independent
thread.
"""
self.send('trick.var_set_write_mode({0})'.format(bool(enable)),
self.Channel.ASYNC)
def validate_addresses(self, validate=True, channel=Channel.BOTH):
"""
Set whether or not addresses are validated.
When bool(validate) is True, variable addresses will be
validated against the memory manager before being read. Those
not known to Trick are considered invalid and their values are
reported as "BAD_REF". This prevents malformed variable
requests, such as pointers with invalid offsets, from causing
segmentation faults.
Parameters
----------
validate : bool
The desired validation state.
channel : Channel
The channel to affect.
"""
self.send('trick.var_validate_address({0})'.format(bool(validate)),
channel)
def variable_exists(self, name):
"""
Determine if name is known to the memory manager.
Parameters
----------
name : str
The variable's name.
Returns
-------
bool
True if name has been registered with the memory manager.
False if not.
Raises
------
IOError
If the remote endpoint has closed the connection.
UnexpectedMessageError
If the next message on the synchronous channel is not a
response to a var_exists inquiry.
"""
self.send('trick.var_exists("{0}")'.format(name))
message = self.readline()
_assert_message_type(message, Message.Indicator.VAR_EXISTS)
return message.data == '1'
def freeze(self, freeze=True):
"""
Freeze or unfreeze the sim.
Parameters
----------
freeze : bool
True to freeze the sim.
False to unfreeze it.
"""
self.send('trick.exec_{0}()'
.format('freeze' if bool(freeze) else 'run'))
def checkpoint(self, filename):
"""
Dump a checkpoint.
Parameters
----------
filename : str
The checkpoint file name. The checkpoint will be saved in the
directory containing the sim's input file.
"""
self.send('trick.checkpoint("' + filename + '")')
def load_checkpoint(self, filename):
"""
Load a checkpoint. The path is relative to the directory containing
the sim's S_main executable.
Parameters
----------
filename : str
The checkpoint file name.
"""
self.send('trick.load_checkpoint("' + filename + '")')
def enable_real_time(self, enable=True):
"""
Toggle real-time execution.
Parameters
----------
enable : bool
True so synchronize sim execution with the real-time clock.
False to run as quickly as possible.
"""
self.send('trick.real_time_{0}able()'
.format('en' if bool(enable) else 'dis'))
def send(self, command, channel=Channel.SYNC):
"""
Append a newline to command and send it, blocking until all data
has been sent. Calling this directly is only necessary if you
want to send a command for which there is not already a method
in this class. You must not send a command on the asynchronous
channel that produces a response, as the response will be
consumed by the variable-sampling thread, which will likely
cause an error. You must ensure the socket is left in a "clean"
state before calling other methods of this class. If incoming
data is left on either channel, it is likely to cause other
methods to fail since they won't get what they're expecting when
they go to read.
Parameters
----------
command : str
The command to send.
channel : Channel
The channel on which to send. You should almost certainly
leave this as the default.
"""
command = '{0}\n'.format(command)
for channel in {
self.Channel.SYNC: [self._synchronous_socket],
self.Channel.ASYNC: [self._asynchronous_socket],
self.Channel.BOTH: [self._synchronous_socket,
self._asynchronous_socket]
}[channel]:
channel.sendall(command.encode())
def readline(self, synchronous_channel=True):
"""
Read a newline-terminated line, blocking if necessary. Calling
this directly is only necessary if you have directly called
send and expect a response from the variable server. The newline
character is stripped.
Parameters
----------
synchronous_channel : bool
True to read from the synchronous channel.
False to read from the asynchronous channel.
Returns
-------
Message
The next available message.
Raises
------
IOError
If the remote endpoint has closed the connection.
"""
file_interface = (self._synchronous_file_interface
if synchronous_channel
else self._asynchronous_file_interface)
line = file_interface.readline()
if not line:
raise IOError("The remote endpoint has closed the connection")
line = line.rstrip(os.linesep).split('\t', 1)
return Message(int(line[0]), line[1])
def _var_add(self, name, units=None, channel=Channel.ASYNC):
"""
Send a var_add command to the variable server.
Parameters
----------
name : str
The variable's name.
units : str
The units in which to report the variable's value.
channel : Channel
The channel to affect.
"""
self.send(
'trick.var_add("{0}"{1})'
.format(name, ', "{0}"'.format(units) if units is not None else ''),
channel)
def _var_remove(self, name):
"""
Send a var_remove command to the variable server.
Parameters
----------
name : str
The variable's name.
channel : Channel
The channel to affect.
"""
self.send('trick.var_remove("{0}")'.format(name),
self.Channel.ASYNC)
def _var_send(self, channel=Channel.SYNC):
"""
Send a var_send command to the variable server.
Parameters
----------
channel : Channel
The channel to affect.
"""
self.send('trick.var_send()', channel)
def _read_values(self, synchronous_channel=True):
"""
Read a set of variable values.
Parameters
----------
synchronous_channel : bool
True to read from the synchronous channel.
False to read from the asynchronous channel.