[1078] | 1 | """Framsticks as a Python module.
|
---|
| 2 |
|
---|
| 3 | Static FramScript objects are available inside the module under their well known names
|
---|
| 4 | (frams.Simulator, frams.GenePools, etc.)
|
---|
| 5 |
|
---|
| 6 | These objects and all values passed to and from Framsticks are instances of frams.ExtValue.
|
---|
| 7 | Python values are automatically converted to Framstics data types.
|
---|
| 8 | Use frams.ExtValue._makeInt()/_makeDouble()/_makeString()/_makeNull() for explicit conversions.
|
---|
| 9 | Simple values returned from Framsticks can be converted to their natural Python
|
---|
| 10 | counterparts using _value() (or forced to a specific type with _int()/_double()/_string()).
|
---|
| 11 |
|
---|
| 12 | All non-Framsticks Python attributes start with '_' to avoid conflicts with Framsticks attributes.
|
---|
| 13 | Framsticks names that are Python reserved words are prefixed with 'x' (currently just Simulator.ximport).
|
---|
[1088] | 14 |
|
---|
| 15 | For sample usage, see frams-test.py and FramsticksLib.py.
|
---|
[1123] | 16 |
|
---|
[1146] | 17 | If you want to run many independent instances of this class in parallel, use the "multiprocessing" module and then each process
|
---|
| 18 | that uses this module will initialize it and get access to a separate instance of the Framsticks library.
|
---|
| 19 |
|
---|
[1123] | 20 | For interfaces in other languages (e.g. using the Framsticks library in your C++ code), see ../cpp/frams/frams-objects.h
|
---|
[1078] | 21 | """
|
---|
| 22 |
|
---|
| 23 | import ctypes, re, sys, os
|
---|
| 24 |
|
---|
| 25 | c_api = None # will be initialized in init(). Global because ExtValue uses it.
|
---|
| 26 |
|
---|
| 27 |
|
---|
| 28 | class ExtValue(object):
|
---|
[1083] | 29 | """All Framsticks objects and values are instances of this class. Read the documentation of the 'frams' module for more information."""
|
---|
[1081] | 30 |
|
---|
[1078] | 31 | _reInsideParens = re.compile('\((.*)\)')
|
---|
[1087] | 32 | _reservedWords = ['import'] # this list is scanned during every attribute access, only add what is really clashing with Framsticks properties
|
---|
[1078] | 33 | _reservedXWords = ['x' + word for word in _reservedWords]
|
---|
| 34 | _encoding = 'utf-8'
|
---|
| 35 |
|
---|
| 36 |
|
---|
| 37 | def __init__(self, arg=None, dontinit=False):
|
---|
| 38 | if dontinit:
|
---|
| 39 | return
|
---|
| 40 | if isinstance(arg, int):
|
---|
| 41 | self._initFromInt(arg)
|
---|
| 42 | elif isinstance(arg, str):
|
---|
| 43 | self._initFromString(arg)
|
---|
| 44 | elif isinstance(arg, float):
|
---|
| 45 | self._initFromDouble(arg)
|
---|
| 46 | elif arg == None:
|
---|
| 47 | self._initFromNull()
|
---|
| 48 | else:
|
---|
[1141] | 49 | raise ctypes.ArgumentError("Can't make ExtValue from '%s' (%s)" % (str(arg), type(arg)))
|
---|
[1078] | 50 |
|
---|
| 51 |
|
---|
| 52 | def __del__(self):
|
---|
| 53 | c_api.extFree(self.__ptr)
|
---|
| 54 |
|
---|
| 55 |
|
---|
| 56 | def _initFromNull(self):
|
---|
| 57 | self.__ptr = c_api.extFromNull()
|
---|
| 58 |
|
---|
| 59 |
|
---|
| 60 | def _initFromInt(self, v):
|
---|
| 61 | self.__ptr = c_api.extFromInt(v)
|
---|
| 62 |
|
---|
| 63 |
|
---|
| 64 | def _initFromDouble(self, v):
|
---|
| 65 | self.__ptr = c_api.extFromDouble(v)
|
---|
| 66 |
|
---|
| 67 |
|
---|
| 68 | def _initFromString(self, v):
|
---|
| 69 | self.__ptr = c_api.extFromString(ExtValue._cstringFromPython(v))
|
---|
| 70 |
|
---|
| 71 |
|
---|
| 72 | @classmethod
|
---|
| 73 | def _makeNull(cls, v):
|
---|
| 74 | e = ExtValue(None, True)
|
---|
| 75 | e._initFromNull()
|
---|
| 76 | return e
|
---|
| 77 |
|
---|
| 78 |
|
---|
| 79 | @classmethod
|
---|
| 80 | def _makeInt(cls, v):
|
---|
| 81 | e = ExtValue(None, True)
|
---|
| 82 | e._initFromInt(v)
|
---|
| 83 | return e
|
---|
| 84 |
|
---|
| 85 |
|
---|
| 86 | @classmethod
|
---|
| 87 | def _makeDouble(cls, v):
|
---|
| 88 | e = ExtValue(None, True)
|
---|
| 89 | e._initFromDouble(v)
|
---|
| 90 | return e
|
---|
| 91 |
|
---|
| 92 |
|
---|
| 93 | @classmethod
|
---|
| 94 | def _makeString(cls, v):
|
---|
| 95 | e = ExtValue(None, True)
|
---|
| 96 | e._initFromString(v)
|
---|
| 97 | return e
|
---|
| 98 |
|
---|
| 99 |
|
---|
| 100 | @classmethod
|
---|
| 101 | def _rootObject(cls):
|
---|
| 102 | e = ExtValue(None, True)
|
---|
| 103 | e.__ptr = c_api.rootObject()
|
---|
| 104 | return e
|
---|
| 105 |
|
---|
| 106 |
|
---|
| 107 | @classmethod
|
---|
| 108 | def _stringFromC(cls, cptr):
|
---|
| 109 | return cptr.decode(ExtValue._encoding)
|
---|
| 110 |
|
---|
| 111 |
|
---|
| 112 | @classmethod
|
---|
| 113 | def _cstringFromPython(cls, s):
|
---|
| 114 | return ctypes.c_char_p(s.encode(ExtValue._encoding))
|
---|
| 115 |
|
---|
| 116 |
|
---|
| 117 | def _type(self):
|
---|
| 118 | return c_api.extType(self.__ptr)
|
---|
| 119 |
|
---|
| 120 |
|
---|
| 121 | def _class(self):
|
---|
| 122 | cls = c_api.extClass(self.__ptr)
|
---|
| 123 | if cls == None:
|
---|
| 124 | return None
|
---|
| 125 | else:
|
---|
| 126 | return ExtValue._stringFromC(cls)
|
---|
| 127 |
|
---|
| 128 |
|
---|
| 129 | def _value(self):
|
---|
| 130 | t = self._type()
|
---|
| 131 | if t == 0:
|
---|
| 132 | return None
|
---|
| 133 | elif t == 1:
|
---|
| 134 | return self._int()
|
---|
| 135 | elif t == 2:
|
---|
| 136 | return self._double()
|
---|
| 137 | elif t == 3:
|
---|
| 138 | return self._string()
|
---|
| 139 | else:
|
---|
| 140 | return self
|
---|
| 141 |
|
---|
| 142 |
|
---|
| 143 | def _int(self):
|
---|
| 144 | return c_api.extIntValue(self.__ptr)
|
---|
| 145 |
|
---|
| 146 |
|
---|
| 147 | def _double(self):
|
---|
| 148 | return c_api.extDoubleValue(self.__ptr)
|
---|
| 149 |
|
---|
| 150 |
|
---|
| 151 | def _string(self):
|
---|
| 152 | return ExtValue._stringFromC(c_api.extStringValue(self.__ptr))
|
---|
| 153 |
|
---|
| 154 |
|
---|
[1150] | 155 | def _propCount(self):
|
---|
| 156 | return c_api.extPropCount(self.__ptr)
|
---|
| 157 |
|
---|
| 158 | def _propId(self,i):
|
---|
| 159 | return ExtValue._stringFromC(c_api.extPropId(self.__ptr,i))
|
---|
| 160 |
|
---|
| 161 | def _propName(self,i):
|
---|
| 162 | return ExtValue._stringFromC(c_api.extPropName(self.__ptr,i))
|
---|
| 163 |
|
---|
| 164 | def _propType(self,i):
|
---|
| 165 | return ExtValue._stringFromC(c_api.extPropType(self.__ptr,i))
|
---|
| 166 |
|
---|
| 167 | def _propHelp(self,i):
|
---|
| 168 | h=c_api.extPropHelp(self.__ptr,i) #unlike other string fields, help is sometimes NULL
|
---|
| 169 | return ExtValue._stringFromC(h) if h!=None else '';
|
---|
| 170 |
|
---|
| 171 | def _propFlags(self,i):
|
---|
| 172 | return c_api.extPropFlags(self.__ptr,i)
|
---|
| 173 |
|
---|
| 174 | def _propGroup(self,i):
|
---|
| 175 | return c_api.extPropGroup(self.__ptr,i)
|
---|
| 176 |
|
---|
| 177 | def _groupCount(self):
|
---|
| 178 | return c_api.extGroupCount(self.__ptr)
|
---|
| 179 |
|
---|
| 180 | def _groupName(self,i):
|
---|
| 181 | return ExtValue._stringFromC(c_api.extGroupName(self.__ptr,i))
|
---|
| 182 |
|
---|
| 183 | def _groupMember(self,g,i):
|
---|
| 184 | return c_api.extGroupMember(self.__ptr,g,i)
|
---|
| 185 |
|
---|
| 186 | def _memberCount(self,g):
|
---|
| 187 | return c_api.extMemberCount(self.__ptr,g)
|
---|
| 188 |
|
---|
[1078] | 189 | def __str__(self):
|
---|
| 190 | return self._string()
|
---|
| 191 |
|
---|
| 192 |
|
---|
| 193 | def __dir__(self):
|
---|
| 194 | ids = dir(type(self))
|
---|
| 195 | if self._type() == 4:
|
---|
| 196 | for i in range(c_api.extPropCount(self.__ptr)):
|
---|
| 197 | name = ExtValue._stringFromC(c_api.extPropId(self.__ptr, i))
|
---|
| 198 | if name in ExtValue._reservedWords:
|
---|
| 199 | name = 'x' + name
|
---|
| 200 | ids.append(name)
|
---|
| 201 | return ids
|
---|
| 202 |
|
---|
| 203 |
|
---|
| 204 | def __getattr__(self, key):
|
---|
| 205 | if key[0] == '_':
|
---|
| 206 | return self.__dict__[key]
|
---|
| 207 | if key in ExtValue._reservedXWords:
|
---|
| 208 | key = key[1:]
|
---|
| 209 | prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
|
---|
| 210 | if prop_i < 0:
|
---|
| 211 | raise AttributeError('no ' + str(key) + ' in ' + str(self))
|
---|
| 212 | t = ExtValue._stringFromC(c_api.extPropType(self.__ptr, prop_i))
|
---|
| 213 | if t[0] == 'p':
|
---|
| 214 | arg_types = ExtValue._reInsideParens.search(t)
|
---|
| 215 | if arg_types:
|
---|
| 216 | arg_types = arg_types.group(1).split(',') # anyone wants to add argument type validation using param type declarations?
|
---|
| 217 |
|
---|
| 218 |
|
---|
| 219 | def fun(*args):
|
---|
| 220 | ext_args = []
|
---|
| 221 | ext_pointers = []
|
---|
| 222 | for a in args:
|
---|
| 223 | if isinstance(a, ExtValue):
|
---|
| 224 | ext = a
|
---|
| 225 | else:
|
---|
| 226 | ext = ExtValue(a)
|
---|
| 227 | ext_args.append(ext)
|
---|
| 228 | ext_pointers.append(ext.__ptr)
|
---|
| 229 | ret = ExtValue(None, True)
|
---|
| 230 | args_array = (ctypes.c_void_p * len(args))(*ext_pointers)
|
---|
| 231 | ret.__ptr = c_api.extPropCall(self.__ptr, prop_i, len(args), args_array)
|
---|
| 232 | return ret
|
---|
| 233 |
|
---|
| 234 |
|
---|
| 235 | return fun
|
---|
| 236 | else:
|
---|
| 237 | ret = ExtValue(None, True)
|
---|
| 238 | ret.__ptr = c_api.extPropGet(self.__ptr, prop_i)
|
---|
| 239 | return ret
|
---|
| 240 |
|
---|
| 241 |
|
---|
| 242 | def __setattr__(self, key, value):
|
---|
| 243 | if key[0] == '_':
|
---|
| 244 | self.__dict__[key] = value
|
---|
| 245 | else:
|
---|
| 246 | if key in ExtValue._reservedXWords:
|
---|
| 247 | key = key[1:]
|
---|
| 248 | prop_i = c_api.extPropFind(self.__ptr, ExtValue._cstringFromPython(key))
|
---|
| 249 | if prop_i < 0:
|
---|
| 250 | raise AttributeError("No '" + str(key) + "' in '" + str(self) + "'")
|
---|
| 251 | if not isinstance(value, ExtValue):
|
---|
| 252 | value = ExtValue(value)
|
---|
| 253 | c_api.extPropSet(self.__ptr, prop_i, value.__ptr)
|
---|
| 254 |
|
---|
| 255 |
|
---|
| 256 | def __getitem__(self, key):
|
---|
| 257 | return self.get(key)
|
---|
| 258 |
|
---|
| 259 |
|
---|
| 260 | def __setitem__(self, key, value):
|
---|
| 261 | return self.set(key, value)
|
---|
| 262 |
|
---|
| 263 |
|
---|
| 264 | def __len__(self):
|
---|
| 265 | try:
|
---|
| 266 | return self.size._int()
|
---|
| 267 | except:
|
---|
| 268 | return 0
|
---|
| 269 |
|
---|
| 270 |
|
---|
| 271 | def __iter__(self):
|
---|
| 272 | class It(object):
|
---|
| 273 | def __init__(self, container, frams_it):
|
---|
| 274 | self.container = container
|
---|
| 275 | self.frams_it = frams_it
|
---|
| 276 |
|
---|
| 277 |
|
---|
| 278 | def __iter__(self):
|
---|
| 279 | return self
|
---|
| 280 |
|
---|
| 281 |
|
---|
| 282 | def __next__(self):
|
---|
| 283 | if self.frams_it.next._int() != 0:
|
---|
| 284 | return self.frams_it.value
|
---|
| 285 | else:
|
---|
| 286 | raise StopIteration()
|
---|
| 287 |
|
---|
| 288 | return It(self, self.iterator)
|
---|
| 289 |
|
---|
| 290 |
|
---|
[1085] | 291 | def init(*args):
|
---|
[1083] | 292 | """
|
---|
[1166] | 293 | Initializes the connection to Framsticks dll/so/dylib.
|
---|
[1083] | 294 |
|
---|
[1087] | 295 | Python programs do not have to know the Framstics path but if they know, just pass the path as the first argument.
|
---|
| 296 | Similarly '-dPATH' and '-DPATH' needed by Framsticks are optional and derived from the first path, unless they are specified as args in init().
|
---|
| 297 | '-LNAME' is the optional library name (full name including the file name extension), default is 'frams-objects.dll/.so' depending on the platform.
|
---|
| 298 | All other arguments are passed to Framsticks and not interpreted by this function.
|
---|
[1085] | 299 |
|
---|
[1083] | 300 | """
|
---|
[1085] | 301 | # goals:
|
---|
[1087] | 302 | frams_d = None
|
---|
| 303 | frams_D = None
|
---|
| 304 | lib_path = None
|
---|
[1166] | 305 | lib_name = ('frams-objects.dylib' if sys.platform == 'darwin' else 'frams-objects.so') if os.name == 'posix' else 'frams-objects.dll'
|
---|
[1087] | 306 | initargs = []
|
---|
[1085] | 307 | for a in args:
|
---|
[1087] | 308 | if a[:2] == '-d':
|
---|
| 309 | frams_d = a
|
---|
| 310 | elif a[:2] == '-D':
|
---|
| 311 | frams_D = a
|
---|
| 312 | elif a[:2] == '-L':
|
---|
| 313 | lib_name = a[2:]
|
---|
| 314 | elif lib_path is None:
|
---|
| 315 | lib_path = a
|
---|
[1085] | 316 | else:
|
---|
| 317 | initargs.append(a)
|
---|
[1087] | 318 | if lib_path is None:
|
---|
| 319 | # TODO: use environment variable and/or the zip distribution we are in when the path is not specified in arg
|
---|
| 320 | # for now just assume the current dir is Framsticks
|
---|
| 321 | lib_path = '.'
|
---|
[1085] | 322 |
|
---|
[1160] | 323 | if os.name == 'nt':
|
---|
| 324 | if sys.version_info < (3, 8):
|
---|
| 325 | original_dir = os.getcwd()
|
---|
| 326 | os.chdir(lib_path) # because under Windows, frams-objects.dll requires other dll's which reside in the same directory, so we must change current dir for them to be found while loading the main dll.
|
---|
| 327 | else:
|
---|
| 328 | os.add_dll_directory(lib_path)
|
---|
| 329 | abs_data = os.path.join(os.path.abspath(lib_path), "data") # use absolute path for -d and -D so python is free to cd anywhere without confusing Framsticks
|
---|
| 330 |
|
---|
| 331 |
|
---|
[1085] | 332 | # for the hypothetical case without lib_path the abs_data must be obtained from somewhere else
|
---|
[1087] | 333 | if frams_d is None:
|
---|
| 334 | frams_d = '-d' + abs_data
|
---|
| 335 | if frams_D is None:
|
---|
| 336 | frams_D = '-D' + abs_data
|
---|
| 337 | initargs.insert(0, frams_d)
|
---|
| 338 | initargs.insert(0, frams_D)
|
---|
| 339 | initargs.insert(0, 'dummy.exe') # as an offset, 0th arg is by convention app name
|
---|
| 340 |
|
---|
[1085] | 341 | global c_api # access global variable
|
---|
[1087] | 342 | if lib_path is not None and os.name == 'posix':
|
---|
[1167] | 343 | lib_name = os.path.join(lib_path, lib_name) # lib_path is always set ('.' when not specified). For the (currently unused) case of lib_path==None, the resulting lib_name is a bare filename without any path, which loads the library from a system-configured location.
|
---|
[1088] | 344 | try:
|
---|
[1146] | 345 | c_api = ctypes.CDLL(lib_name) # if accessing this module from multiple threads, they will all share a single c_api and access the same copy of the library and its data. If you want separate independent copies, read the comment at the top of this file on using the "multiprocessing" module.
|
---|
[1088] | 346 | except OSError as e:
|
---|
| 347 | print("*** Could not find or open '%s' from '%s'.\n*** Did you provide proper arguments and is this file readable?\n" % (lib_name, os.getcwd()))
|
---|
| 348 | raise
|
---|
[1087] | 349 |
|
---|
[1160] | 350 | if os.name == 'nt' and sys.version_info < (3, 8):
|
---|
| 351 | os.chdir(original_dir) # restore current working dir after loading the library so Framsticks sees the expected directory
|
---|
| 352 |
|
---|
[1078] | 353 | c_api.init.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
|
---|
| 354 | c_api.init.restype = None
|
---|
| 355 | c_api.extFree.argtypes = [ctypes.c_void_p]
|
---|
| 356 | c_api.extFree.restype = None
|
---|
| 357 | c_api.extType.argtypes = [ctypes.c_void_p]
|
---|
| 358 | c_api.extType.restype = ctypes.c_int
|
---|
| 359 | c_api.extFromNull.argtypes = []
|
---|
| 360 | c_api.extFromNull.restype = ctypes.c_void_p
|
---|
| 361 | c_api.extFromInt.argtypes = [ctypes.c_int]
|
---|
| 362 | c_api.extFromInt.restype = ctypes.c_void_p
|
---|
| 363 | c_api.extFromDouble.argtypes = [ctypes.c_double]
|
---|
| 364 | c_api.extFromDouble.restype = ctypes.c_void_p
|
---|
| 365 | c_api.extFromString.argtypes = [ctypes.c_char_p]
|
---|
| 366 | c_api.extFromString.restype = ctypes.c_void_p
|
---|
| 367 | c_api.extIntValue.argtypes = [ctypes.c_void_p]
|
---|
| 368 | c_api.extIntValue.restype = ctypes.c_int
|
---|
| 369 | c_api.extDoubleValue.argtypes = [ctypes.c_void_p]
|
---|
| 370 | c_api.extDoubleValue.restype = ctypes.c_double
|
---|
| 371 | c_api.extStringValue.argtypes = [ctypes.c_void_p]
|
---|
| 372 | c_api.extStringValue.restype = ctypes.c_char_p
|
---|
| 373 | c_api.extClass.argtypes = [ctypes.c_void_p]
|
---|
| 374 | c_api.extClass.restype = ctypes.c_char_p
|
---|
| 375 | c_api.extPropCount.argtypes = [ctypes.c_void_p]
|
---|
| 376 | c_api.extPropCount.restype = ctypes.c_int
|
---|
| 377 | c_api.extPropId.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 378 | c_api.extPropId.restype = ctypes.c_char_p
|
---|
[1150] | 379 | c_api.extPropName.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 380 | c_api.extPropName.restype = ctypes.c_char_p
|
---|
[1078] | 381 | c_api.extPropType.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 382 | c_api.extPropType.restype = ctypes.c_char_p
|
---|
[1150] | 383 | c_api.extPropGroup.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 384 | c_api.extPropGroup.restype = ctypes.c_int
|
---|
| 385 | c_api.extPropFlags.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 386 | c_api.extPropFlags.restype = ctypes.c_int
|
---|
| 387 | c_api.extPropHelp.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 388 | c_api.extPropHelp.restype = ctypes.c_char_p
|
---|
[1078] | 389 | c_api.extPropFind.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
---|
| 390 | c_api.extPropFind.restype = ctypes.c_int
|
---|
| 391 | c_api.extPropGet.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 392 | c_api.extPropGet.restype = ctypes.c_void_p
|
---|
| 393 | c_api.extPropSet.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p]
|
---|
| 394 | c_api.extPropSet.restype = ctypes.c_int
|
---|
| 395 | c_api.extPropCall.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p]
|
---|
| 396 | c_api.extPropCall.restype = ctypes.c_void_p
|
---|
[1150] | 397 | c_api.extGroupCount.argtypes = [ctypes.c_void_p]
|
---|
| 398 | c_api.extGroupCount.restype = ctypes.c_int
|
---|
| 399 | c_api.extGroupName.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 400 | c_api.extGroupName.restype = ctypes.c_char_p
|
---|
| 401 | c_api.extGroupMember.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
|
---|
| 402 | c_api.extGroupMember.restype = ctypes.c_int
|
---|
| 403 | c_api.extMemberCount.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
---|
| 404 | c_api.extMemberCount.restype = ctypes.c_int
|
---|
[1078] | 405 | c_api.rootObject.argtypes = []
|
---|
| 406 | c_api.rootObject.restype = ctypes.c_void_p
|
---|
| 407 |
|
---|
[1085] | 408 | c_args = (ctypes.c_char_p * len(initargs))(*list(a.encode(ExtValue._encoding) for a in initargs))
|
---|
| 409 | c_api.init(len(initargs), c_args)
|
---|
[1106] | 410 |
|
---|
[1078] | 411 | Root = ExtValue._rootObject()
|
---|
| 412 | for n in dir(Root):
|
---|
| 413 | if n[0].isalpha():
|
---|
| 414 | attr = getattr(Root, n)
|
---|
| 415 | if isinstance(attr, ExtValue):
|
---|
| 416 | attr = attr._value()
|
---|
| 417 | setattr(sys.modules[__name__], n, attr)
|
---|
[1141] | 418 |
|
---|
[1078] | 419 | print('Using Framsticks version: ' + str(Simulator.version_string))
|
---|
| 420 | print('Home (writable) dir : ' + home_dir)
|
---|
| 421 | print('Resources dir : ' + res_dir)
|
---|
| 422 | print()
|
---|