project.py 25.1 KB
Newer Older
Michael Büsch's avatar
Michael Büsch committed
1 2 3 4
# -*- coding: utf-8 -*-
#
# AWL simulator - project
#
5
# Copyright 2014-2016 Michael Buesch <[email protected]>
Michael Büsch's avatar
Michael Büsch committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

22
from __future__ import division, absolute_import, print_function, unicode_literals
23
from awlsim.common.compat import *
24

Michael Büsch's avatar
Michael Büsch committed
25
from awlsim.common.cpuspecs import *
Michael Büsch's avatar
Michael Büsch committed
26
from awlsim.common.sources import *
27
from awlsim.common.hwmod import *
28
from awlsim.common.util import *
Michael Büsch's avatar
Michael Büsch committed
29

30
from awlsim.library.libselection import *
31

32
import base64, binascii
Michael Büsch's avatar
Michael Büsch committed
33 34 35 36 37 38 39 40 41 42 43
import datetime
import os

if isPy2Compat:
	from ConfigParser import SafeConfigParser as _ConfigParser
	from ConfigParser import Error as _ConfigParserError
else:
	from configparser import ConfigParser as _ConfigParser
	from configparser import Error as _ConfigParserError


44 45 46
class GuiSettings(object):
	def __init__(self,
		     editorAutoIndentEn=True,
47
		     editorPasteIndentEn=True,
48 49
		     editorValidationEn=True,
		     editorFont=""):
50
		self.setEditorAutoIndentEn(editorAutoIndentEn)
51
		self.setEditorPasteIndentEn(editorPasteIndentEn)
52
		self.setEditorValidationEn(editorValidationEn)
53
		self.setEditorFont(editorFont)
54 55 56 57 58 59 60

	def setEditorAutoIndentEn(self, editorAutoIndentEn):
		self.editorAutoIndentEn = editorAutoIndentEn

	def getEditorAutoIndentEn(self):
		return self.editorAutoIndentEn

61 62 63 64 65 66
	def setEditorPasteIndentEn(self, editorPasteIndentEn):
		self.editorPasteIndentEn = editorPasteIndentEn

	def getEditorPasteIndentEn(self):
		return self.editorPasteIndentEn

67 68 69 70 71 72
	def setEditorValidationEn(self, editorValidationEn):
		self.editorValidationEn = editorValidationEn

	def getEditorValidationEn(self):
		return self.editorValidationEn

73 74 75 76 77 78
	def setEditorFont(self, editorFont):
		self.editorFont = editorFont

	def getEditorFont(self):
		return self.editorFont

Michael Büsch's avatar
Michael Büsch committed
79
class CoreLinkSettings(object):
80 81 82 83 84 85 86
	DEFAULT_INTERPRETERS	= "pypy3; pypy; $CURRENT; python3; python2; python; py"
	SPAWN_PORT_BASE		= 4151 + 32

	TUNNEL_NONE		= 0
	TUNNEL_SSH		= 1

	TUNNEL_LOCPORT_AUTO	= -1
87

Michael Büsch's avatar
Michael Büsch committed
88 89
	def __init__(self,
		     spawnLocalEn=True,
90 91
		     spawnLocalPortRange=range(SPAWN_PORT_BASE,
					       SPAWN_PORT_BASE + 4095 + 1),
92
		     spawnLocalInterpreters="$DEFAULT",
Michael Büsch's avatar
Michael Büsch committed
93
		     connectHost="localhost",
94
		     connectPort=4151,
95 96 97 98 99 100
		     connectTimeoutMs=3000,
		     tunnel=TUNNEL_NONE,
		     tunnelLocalPort=TUNNEL_LOCPORT_AUTO,
		     sshUser="pi",
		     sshPort=22,
		     sshExecutable="ssh"):
Michael Büsch's avatar
Michael Büsch committed
101 102 103 104 105
		self.setSpawnLocalEn(spawnLocalEn)
		self.setSpawnLocalPortRange(spawnLocalPortRange)
		self.setSpawnLocalInterpreters(spawnLocalInterpreters),
		self.setConnectHost(connectHost)
		self.setConnectPort(connectPort)
106
		self.setConnectTimeoutMs(connectTimeoutMs)
107 108 109 110 111
		self.setTunnel(tunnel)
		self.setTunnelLocalPort(tunnelLocalPort)
		self.setSSHUser(sshUser)
		self.setSSHPort(sshPort)
		self.setSSHExecutable(sshExecutable)
Michael Büsch's avatar
Michael Büsch committed
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130

	def setSpawnLocalEn(self, spawnLocalEn):
		self.spawnLocalEn = spawnLocalEn

	def getSpawnLocalEn(self):
		return self.spawnLocalEn

	def setSpawnLocalPortRange(self, spawnLocalPortRange):
		self.spawnLocalPortRange = spawnLocalPortRange

	def getSpawnLocalPortRange(self):
		return self.spawnLocalPortRange

	def setSpawnLocalInterpreters(self, spawnLocalInterpreters):
		self.spawnLocalInterpreters = spawnLocalInterpreters

	def getSpawnLocalInterpreters(self):
		return self.spawnLocalInterpreters

131
	def __expandInterpStr(self, interpStr):
Michael Büsch's avatar
Michael Büsch committed
132
		ret = []
133 134 135 136 137 138 139 140 141 142 143 144 145
		for inter in interpStr.split(';'):
			if inter.strip() == "$DEFAULT":
				inter = self.__expandInterpStr(self.DEFAULT_INTERPRETERS)
			elif inter.strip() == "$CURRENT":
				inter = sys.executable
			ret.append(inter)
		return ";".join(ret)

	def getSpawnLocalInterpreterList(self, replace=True):
		interpStr = self.getSpawnLocalInterpreters()
		if replace:
			interpStr = self.__expandInterpStr(interpStr)
		return [ i.strip() for i in interpStr.split(';') ]
Michael Büsch's avatar
Michael Büsch committed
146 147 148 149 150 151 152 153 154 155 156 157 158

	def setConnectHost(self, connectHost):
		self.connectHost = connectHost

	def getConnectHost(self):
		return self.connectHost

	def setConnectPort(self, connectPort):
		self.connectPort = connectPort

	def getConnectPort(self):
		return self.connectPort

159 160 161 162 163 164
	def setConnectTimeoutMs(self, connectTimeoutMs):
		self.connectTimeoutMs = connectTimeoutMs

	def getConnectTimeoutMs(self):
		return self.connectTimeoutMs

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
	def setTunnel(self, tunnel):
		self.tunnel = tunnel

	def getTunnel(self):
		return self.tunnel

	def setTunnelLocalPort(self, tunnelLocalPort):
		self.tunnelLocalPort = tunnelLocalPort

	def getTunnelLocalPort(self):
		return self.tunnelLocalPort

	def setSSHUser(self, sshUser):
		self.sshUser = sshUser

	def getSSHUser(self):
		return self.sshUser

	def setSSHPort(self, sshPort):
		self.sshPort = sshPort

	def getSSHPort(self):
		return self.sshPort

	def setSSHExecutable(self, sshExecutable):
		self.sshExecutable = sshExecutable

	def getSSHExecutable(self):
		return self.sshExecutable

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
class HwmodSettings(object):
	def __init__(self,
		     loadedModules=None):
		self.setLoadedModules(loadedModules)

	def setLoadedModules(self, loadedModules):
		if not loadedModules:
			loadedModules = []
		self.loadedModules = loadedModules

	def addLoadedModule(self, modDesc):
		self.loadedModules.append(modDesc)

	def getLoadedModules(self):
		return self.loadedModules

Michael Büsch's avatar
Michael Büsch committed
211
class Project(object):
Michael Büsch's avatar
Michael Büsch committed
212 213
	DATETIME_FMT	= "%Y-%m-%d %H:%M:%S.%f"

214
	def __init__(self, projectFile,
Michael Büsch's avatar
Michael Büsch committed
215 216
		     createDate=None,
		     modifyDate=None,
217 218 219
		     awlSources=None,
		     symTabSources=None,
		     libSelections=None,
220 221
		     cpuSpecs=None,
		     obTempPresetsEn=False,
222
		     extInsnsEn=False,
Michael Büsch's avatar
Michael Büsch committed
223
		     guiSettings=None,
224 225
		     coreLinkSettings=None,
		     hwmodSettings=None):
226
		self.setProjectFile(projectFile)
Michael Büsch's avatar
Michael Büsch committed
227 228
		self.setCreateDate(createDate)
		self.setModifyDate(modifyDate)
229 230
		self.setAwlSources(awlSources)
		self.setSymTabSources(symTabSources)
231
		self.setLibSelections(libSelections)
232 233 234
		self.setCpuSpecs(cpuSpecs)
		self.setObTempPresetsEn(obTempPresetsEn)
		self.setExtInsnsEn(extInsnsEn)
235
		self.setGuiSettings(guiSettings)
Michael Büsch's avatar
Michael Büsch committed
236
		self.setCoreLinkSettings(coreLinkSettings)
237
		self.setHwmodSettings(hwmodSettings)
238

239 240 241 242 243 244 245 246 247 248 249 250 251 252
	def clear(self):
		self.setProjectFile(None)
		self.setCreateDate(None)
		self.setModifyDate(None)
		self.setAwlSources(None)
		self.setSymTabSources(None)
		self.setLibSelections(None)
		self.setCpuSpecs(None)
		self.setObTempPresetsEn(False)
		self.setExtInsnsEn(False)
		self.setGuiSettings(None)
		self.setCoreLinkSettings(None)
		self.setHwmodSettings(None)

253 254 255 256 257 258
	def setProjectFile(self, filename):
		self.projectFile = filename

	def getProjectFile(self):
		return self.projectFile

Michael Büsch's avatar
Michael Büsch committed
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
	def setCreateDate(self, createDate):
		if createDate is None:
			createDate = datetime.datetime.utcnow()
		self.createDate = createDate

	def getCreateDate(self):
		return self.createDate

	def setModifyDate(self, modifyDate):
		if modifyDate is None:
			modifyDate = self.getCreateDate()
		if modifyDate is None:
			modifyDate = datetime.datetime.utcnow()
		self.modifyDate = modifyDate

	def getModifyDate(self):
		return self.modifyDate

277
	def setAwlSources(self, awlSources):
278 279
		if not awlSources:
			awlSources = []
280 281 282 283 284 285
		self.awlSources = awlSources

	def getAwlSources(self):
		return self.awlSources

	def setSymTabSources(self, symTabSources):
286 287
		if not symTabSources:
			symTabSources = []
288 289 290 291 292
		self.symTabSources = symTabSources

	def getSymTabSources(self):
		return self.symTabSources

293
	def setLibSelections(self, libSelections):
294 295
		if not libSelections:
			libSelections = []
296 297 298 299 300
		self.libSelections = libSelections

	def getLibSelections(self):
		return self.libSelections

301
	def setCpuSpecs(self, cpuSpecs):
302 303
		if not cpuSpecs:
			cpuSpecs = S7CPUSpecs()
304
		self.cpuSpecs = cpuSpecs
Michael Büsch's avatar
Michael Büsch committed
305

306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
	def getCpuSpecs(self):
		return self.cpuSpecs

	def setObTempPresetsEn(self, obTempPresetsEn):
		self.obTempPresetsEn = obTempPresetsEn

	def getObTempPresetsEn(self):
		return self.obTempPresetsEn

	def setExtInsnsEn(self, extInsnsEn):
		self.extInsnsEn = extInsnsEn

	def getExtInsnsEn(self):
		return self.extInsnsEn

321
	def setGuiSettings(self, guiSettings):
322 323
		if not guiSettings:
			guiSettings = GuiSettings()
324 325 326 327 328
		self.guiSettings = guiSettings

	def getGuiSettings(self):
		return self.guiSettings

Michael Büsch's avatar
Michael Büsch committed
329
	def setCoreLinkSettings(self, coreLinkSettings):
330 331
		if not coreLinkSettings:
			coreLinkSettings = CoreLinkSettings()
Michael Büsch's avatar
Michael Büsch committed
332 333 334 335 336
		self.coreLinkSettings = coreLinkSettings

	def getCoreLinkSettings(self):
		return self.coreLinkSettings

337
	def setHwmodSettings(self, hwmodSettings):
338 339
		if not hwmodSettings:
			hwmodSettings = HwmodSettings()
340 341 342 343 344
		self.hwmodSettings = hwmodSettings

	def getHwmodSettings(self):
		return self.hwmodSettings

Michael Büsch's avatar
Michael Büsch committed
345 346
	@classmethod
	def dataIsProject(cls, data):
347 348
		magic = b"[AWLSIM_PROJECT]"
		if isIronPython and isinstance(data, str):
349 350 351 352 353 354
			try:
				"a".startswith(b"a") # Test for ipy byte conversion bug
			except TypeError:
				# XXX: Workaround for IronPython byte conversion bug
				printInfo("Applying workaround for IronPython byte conversion bug")
				magic = magic.decode("UTF-8")
355
		return data.lstrip().startswith(magic)
Michael Büsch's avatar
Michael Büsch committed
356 357 358 359 360 361 362 363 364 365 366

	@classmethod
	def fileIsProject(cls, filename):
		return cls.dataIsProject(awlFileRead(filename, encoding="binary"))

	@classmethod
	def fromText(cls, text, projectFile):
		if not cls.dataIsProject(text.encode("utf-8")):
			raise AwlSimError("Project file: The data is "\
				"not an awlsim project.")
		projectDir = os.path.dirname(projectFile)
Michael Büsch's avatar
Michael Büsch committed
367 368
		createDate = None
		modifyDate = None
369 370
		awlSources = []
		symTabSources = []
371
		libSelections = []
372
		cpuSpecs = S7CPUSpecs()
373 374
		obTempPresetsEn = False
		extInsnsEn = False
375
		guiSettings = GuiSettings()
Michael Büsch's avatar
Michael Büsch committed
376
		linkSettings = CoreLinkSettings()
377
		hwmodSettings = HwmodSettings()
Michael Büsch's avatar
Michael Büsch committed
378 379
		try:
			p = _ConfigParser()
380
			p.readfp(StringIO(text), projectFile)
Michael Büsch's avatar
Michael Büsch committed
381 382

			# AWLSIM_PROJECT section
Michael Büsch's avatar
Michael Büsch committed
383 384 385 386 387 388
			version = p.getint("AWLSIM_PROJECT", "file_version")
			expectedVersion = 0
			if version != expectedVersion:
				raise AwlSimError("Project file: Unsupported version. "\
					"File version is %d, but expected %d." %\
					(version, expectedVersion))
Michael Büsch's avatar
Michael Büsch committed
389 390 391 392 393 394 395 396 397 398 399 400 401 402
			if p.has_option("AWLSIM_PROJECT", "date"):
				# Compatibility only. "date" is deprecated.
				dStr = p.get("AWLSIM_PROJECT", "date")
				createDate = datetime.datetime.strptime(dStr,
							cls.DATETIME_FMT)
				modifyDate = createDate
			if p.has_option("AWLSIM_PROJECT", "create_date"):
				dStr = p.get("AWLSIM_PROJECT", "create_date")
				createDate = datetime.datetime.strptime(dStr,
							cls.DATETIME_FMT)
			if p.has_option("AWLSIM_PROJECT", "modify_date"):
				dStr = p.get("AWLSIM_PROJECT", "modify_date")
				modifyDate = datetime.datetime.strptime(dStr,
							cls.DATETIME_FMT)
Michael Büsch's avatar
Michael Büsch committed
403

404
			# CPU section
405
			for i in range(0xFFFF):
406 407 408 409
				option = "awl_file_%d" % i
				if not p.has_option("CPU", option):
					break
				path = p.get("CPU", option)
410
				src = AwlSource.fromFile(path, cls.__generic2path(path, projectDir))
411 412
				awlSources.append(src)
			for i in range(0xFFFF):
413 414 415
				srcOption = "awl_%d" % i
				nameOption = "awl_name_%d" % i
				if not p.has_option("CPU", srcOption):
416
					break
417 418 419
				awlBase64 = p.get("CPU", srcOption)
				name = None
				if p.has_option("CPU", nameOption):
420
					with contextlib.suppress(ValueError):
421
						name = base64ToStr(p.get("CPU", nameOption))
422 423
				if name is None:
					name = "AWL/STL #%d" % i
424
				src = AwlSource.fromBase64(name, awlBase64)
425
				awlSources.append(src)
426 427 428 429 430 431
			if p.has_option("CPU", "mnemonics"):
				mnemonics = p.getint("CPU", "mnemonics")
				cpuSpecs.setConfiguredMnemonics(mnemonics)
			if p.has_option("CPU", "nr_accus"):
				nrAccus = p.getint("CPU", "nr_accus")
				cpuSpecs.setNrAccus(nrAccus)
Michael Büsch's avatar
Michael Büsch committed
432 433 434
			if p.has_option("CPU", "clock_memory_byte"):
				clockMemByte = p.getint("CPU", "clock_memory_byte")
				cpuSpecs.setClockMemByte(clockMemByte)
435 436 437 438
			if p.has_option("CPU", "ob_startinfo_enable"):
				obTempPresetsEn = p.getboolean("CPU", "ob_startinfo_enable")
			if p.has_option("CPU", "ext_insns_enable"):
				extInsnsEn = p.getboolean("CPU", "ext_insns_enable")
439 440

			# SYMBOLS section
441
			for i in range(0xFFFF):
442 443
				option = "sym_tab_file_%d" % i
				if not p.has_option("SYMBOLS", option):
444
					break
445
				path = p.get("SYMBOLS", option)
446
				src = SymTabSource.fromFile(path, cls.__generic2path(path, projectDir))
447 448
				symTabSources.append(src)
			for i in range(0xFFFF):
449 450
				srcOption = "sym_tab_%d" % i
				nameOption = "sym_tab_name_%d" % i
Michael Büsch's avatar
Michael Büsch committed
451
				if not p.has_option("SYMBOLS", srcOption):
452
					break
Michael Büsch's avatar
Michael Büsch committed
453
				symTabBase64 = p.get("SYMBOLS", srcOption)
454 455
				name = None
				if p.has_option("SYMBOLS", nameOption):
456
					with contextlib.suppress(ValueError):
457
						name = base64ToStr(p.get("SYMBOLS", nameOption))
458 459
				if name is None:
					name = "Symbol table #%d" % i
460
				src = SymTabSource.fromBase64(name, symTabBase64)
461
				symTabSources.append(src)
Michael Büsch's avatar
Michael Büsch committed
462

463 464 465 466 467 468 469 470
			# LIBS section
			for i in range(0xFFFF):
				nameOption = "lib_name_%d" % i
				blockOption = "lib_block_%d" % i
				effOption = "lib_block_effective_%d" % i
				if not p.has_option("LIBS", nameOption):
					break
				try:
471 472
					libName = base64ToStr(p.get("LIBS", nameOption))
				except ValueError as e:
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
					continue
				block = p.get("LIBS", blockOption).upper().strip()
				effBlock = p.getint("LIBS", effOption)
				try:
					if block.startswith("FC"):
						entryType = AwlLibEntrySelection.TYPE_FC
						entryIndex = int(block[2:].strip())
					elif block.startswith("FB"):
						entryType = AwlLibEntrySelection.TYPE_FB
						entryIndex = int(block[2:].strip())
					elif block.startswith("UNKNOWN"):
						entryType = AwlLibEntrySelection.TYPE_UNKNOWN
						entryIndex = int(block[7:].strip())
					else:
						raise ValueError
					if entryIndex < -1 or entryIndex > 0xFFFF:
						raise ValueError
				except ValueError:
					raise AwlSimError("Project file: Invalid "
						"library block: %s" % block)
				libSelections.append(
					AwlLibEntrySelection(libName = libName,
							     entryType = entryType,
							     entryIndex = entryIndex,
							     effectiveEntryIndex = effBlock)
				)

Michael Büsch's avatar
Michael Büsch committed
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
			# CORE_LINK section
			if p.has_option("CORE_LINK", "spawn_local"):
				linkSettings.setSpawnLocalEn(
					p.getboolean("CORE_LINK", "spawn_local"))
			if p.has_option("CORE_LINK", "spawn_local_port_range"):
				pRange = p.get("CORE_LINK", "spawn_local_port_range")
				try:
					pRange = pRange.split(":")
					begin = int(pRange[0])
					end = int(pRange[1])
					if end < begin:
						raise ValueError
					pRange = range(begin, end + 1)
				except (IndexError, ValueError) as e:
					raise AwlSimError("Project file: Invalid port range")
				linkSettings.setSpawnLocalPortRange(pRange)
			if p.has_option("CORE_LINK", "spawn_local_interpreters"):
				interp = p.get("CORE_LINK", "spawn_local_interpreters")
				try:
519 520
					interp = base64ToStr(interp)
				except ValueError as e:
Michael Büsch's avatar
Michael Büsch committed
521 522 523 524 525 526
					raise AwlSimError("Project file: "
						"Invalid interpreter list")
				linkSettings.setSpawnLocalInterpreters(interp)
			if p.has_option("CORE_LINK", "connect_host"):
				host = p.get("CORE_LINK", "connect_host")
				try:
527 528
					host = base64ToStr(host)
				except ValueError as e:
Michael Büsch's avatar
Michael Büsch committed
529 530 531 532 533 534
					raise AwlSimError("Project file: "
						"Invalid host name")
				linkSettings.setConnectHost(host)
			if p.has_option("CORE_LINK", "connect_port"):
				port = p.getint("CORE_LINK", "connect_port")
				linkSettings.setConnectPort(port)
535 536 537
			if p.has_option("CORE_LINK", "connect_timeout_ms"):
				timeout = p.getint("CORE_LINK", "connect_timeout_ms")
				linkSettings.setConnectTimeoutMs(timeout)
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
			if p.has_option("CORE_LINK", "tunnel"):
				tunnel = p.getint("CORE_LINK", "tunnel")
				linkSettings.setTunnel(tunnel)
			if p.has_option("CORE_LINK", "tunnel_local_port"):
				tunnelLocalPort = p.getint("CORE_LINK", "tunnel_local_port")
				linkSettings.setTunnelLocalPort(tunnelLocalPort)
			if p.has_option("CORE_LINK", "ssh_user"):
				sshUser = p.get("CORE_LINK", "ssh_user")
				try:
					sshUser = base64ToStr(sshUser)
				except ValueError as e:
					raise AwlSimError("Project file: "
						"Invalid ssh_user")
				linkSettings.setSSHUser(sshUser)
			if p.has_option("CORE_LINK", "ssh_port"):
				sshPort = p.getint("CORE_LINK", "ssh_port")
				linkSettings.setSSHPort(sshPort)
			if p.has_option("CORE_LINK", "ssh_executable"):
				sshExecutable = p.get("CORE_LINK", "ssh_executable")
				try:
					sshExecutable = base64ToStr(sshExecutable)
				except ValueError as e:
					raise AwlSimError("Project file: "
						"Invalid ssh_executable")
				linkSettings.setSSHExecutable(sshExecutable)
Michael Büsch's avatar
Michael Büsch committed
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
			# HWMODS section
			for i in range(0xFFFF):
				nameOption = "loaded_mod_%d" % i
				if not p.has_option("HWMODS", nameOption):
					break
				modName = base64ToStr(p.get("HWMODS", nameOption))
				modDesc = HwmodDescriptor(modName)
				for j in range(0x3FF):
					paramOption = "loaded_mod_%d_p%d" % (i, j)
					if not p.has_option("HWMODS", paramOption):
						break
					param = p.get("HWMODS", paramOption)
					try:
						param = param.split(":")
						if len(param) != 2:
							raise ValueError
						paramName = base64ToStr(param[0])
						if param[1].strip():
							paramValue = base64ToStr(param[1])
						else:
							paramValue = None
					except ValueError:
						raise AwlSimError("Project file: "
							"Invalid hw mod parameter")
					modDesc.addParameter(paramName, paramValue)
				hwmodSettings.addLoadedModule(modDesc)

591 592 593 594
			# GUI section
			if p.has_option("GUI", "editor_autoindent"):
				guiSettings.setEditorAutoIndentEn(
					p.getboolean("GUI", "editor_autoindent"))
595 596 597
			if p.has_option("GUI", "editor_paste_autoindent"):
				guiSettings.setEditorPasteIndentEn(
					p.getboolean("GUI", "editor_paste_autoindent"))
598 599 600
			if p.has_option("GUI", "editor_validation"):
				guiSettings.setEditorValidationEn(
					p.getboolean("GUI", "editor_validation"))
601 602
			if p.has_option("GUI", "editor_font"):
				guiSettings.setEditorFont(p.get("GUI", "editor_font").strip())
603

Michael Büsch's avatar
Michael Büsch committed
604 605 606 607
		except _ConfigParserError as e:
			raise AwlSimError("Project parser error: " + str(e))

		return cls(projectFile = projectFile,
Michael Büsch's avatar
Michael Büsch committed
608 609
			   createDate = createDate,
			   modifyDate = modifyDate,
610 611
			   awlSources = awlSources,
			   symTabSources = symTabSources,
612
			   libSelections = libSelections,
613 614
			   cpuSpecs = cpuSpecs,
			   obTempPresetsEn = obTempPresetsEn,
615
			   extInsnsEn = extInsnsEn,
Michael Büsch's avatar
Michael Büsch committed
616
			   guiSettings = guiSettings,
617 618
			   coreLinkSettings = linkSettings,
			   hwmodSettings = hwmodSettings)
Michael Büsch's avatar
Michael Büsch committed
619 620 621 622 623

	@classmethod
	def fromFile(cls, filename):
		return cls.fromText(awlFileRead(filename, encoding="utf8"), filename)

624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
	@classmethod
	def fromProjectOrRawAwlFile(cls, filename):
		"""Read a project (.awlpro) or raw AWL file (.awl)
		and return a Project()."""

		if Project.fileIsProject(filename):
			project = Project.fromFile(filename)
		else:
			# Make a fake project
			awlSrc = AwlSource.fromFile(name = filename,
						    filepath = filename)
			project = Project(projectFile = None,
					  awlSources = [ awlSrc, ])
		return project

Michael Büsch's avatar
Michael Büsch committed
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
	@classmethod
	def __path2generic(cls, path, relativeToDir):
		"""Generate an OS-independent string from a path."""
		if "\r" in path or "\n" in path:
			# The project file format cannot handle these
			raise AwlSimError("Project file: Path '%s' contains invalid "\
				"characters (line breaks)." % path)
		path = os.path.relpath(path, relativeToDir)
		if os.path.splitdrive(path)[0]:
			raise AwlSimError("Project file: Failed to strip drive letter. "\
				"Please make sure the project file, AWL code files and "\
				"symbol tables files all reside on the same drive.")
		path = path.replace(os.path.sep, "/")
		return path

	@classmethod
	def __generic2path(cls, path, relativeToDir):
		"""Generate a path from an OS-independent string."""
		path = path.replace("/", os.path.sep)
		path = os.path.join(relativeToDir, path)
		return path

	def toText(self, projectFile=None):
		if not projectFile:
			projectFile = self.projectFile
		projectDir = os.path.dirname(projectFile)

Michael Büsch's avatar
Michael Büsch committed
666 667
		self.setModifyDate(datetime.datetime.utcnow())

Michael Büsch's avatar
Michael Büsch committed
668 669 670
		lines = []
		lines.append("[AWLSIM_PROJECT]")
		lines.append("file_version=0")
Michael Büsch's avatar
Michael Büsch committed
671 672 673 674
		lines.append("create_date=%s" %\
			     self.getCreateDate().strftime(self.DATETIME_FMT))
		lines.append("modify_date=%s" %\
			     self.getModifyDate().strftime(self.DATETIME_FMT))
Michael Büsch's avatar
Michael Büsch committed
675
		lines.append("")
676

Michael Büsch's avatar
Michael Büsch committed
677
		lines.append("[CPU]")
678 679 680 681
		fileBackedSources = (src for src in self.awlSources if src.isFileBacked())
		embeddedSources = (src for src in self.awlSources if not src.isFileBacked())
		for i, awlSrc in enumerate(fileBackedSources):
			path = self.__path2generic(awlSrc.filepath, projectDir)
Michael Büsch's avatar
Michael Büsch committed
682
			lines.append("awl_file_%d=%s" % (i, path))
683 684
		for i, awlSrc in enumerate(embeddedSources):
			lines.append("awl_%d=%s" % (i, awlSrc.toBase64()))
685
			name = strToBase64(awlSrc.name, ignoreErrors=True)
686
			lines.append("awl_name_%d=%s" % (i, name))
687 688
		lines.append("mnemonics=%d" % self.cpuSpecs.getConfiguredMnemonics())
		lines.append("nr_accus=%d" % self.cpuSpecs.nrAccus)
Michael Büsch's avatar
Michael Büsch committed
689
		lines.append("clock_memory_byte=%d" % self.cpuSpecs.clockMemByte)
690 691
		lines.append("ob_startinfo_enable=%d" % int(bool(self.obTempPresetsEn)))
		lines.append("ext_insns_enable=%d" % int(bool(self.extInsnsEn)))
Michael Büsch's avatar
Michael Büsch committed
692
		lines.append("")
693

Michael Büsch's avatar
Michael Büsch committed
694
		lines.append("[SYMBOLS]")
695 696 697 698 699 700 701
		fileBackedSources = (src for src in self.symTabSources if src.isFileBacked())
		embeddedSources = (src for src in self.symTabSources if not src.isFileBacked())
		for i, symSrc in enumerate(fileBackedSources):
			path = self.__path2generic(symSrc.filepath, projectDir)
			lines.append("sym_tab_file_%d=%s" % (i, path))
		for i, symSrc in enumerate(embeddedSources):
			lines.append("sym_tab_%d=%s" % (i, symSrc.toBase64()))
702
			name = strToBase64(symSrc.name, ignoreErrors=True)
703
			lines.append("sym_tab_name_%d=%s" % (i, name))
704 705 706 707
		lines.append("")

		lines.append("[LIBS]")
		for i, libSel in enumerate(self.libSelections):
708
			libName = strToBase64(libSel.getLibName(), ignoreErrors=True)
709 710 711 712 713 714 715
			lines.append("lib_name_%d=%s" % (i, libName))
			lines.append("lib_block_%d=%s %d" % (
				i, libSel.getEntryTypeStr(),
				libSel.getEntryIndex()))
			lines.append("lib_block_effective_%d=%d" % (
				i, libSel.getEffectiveEntryIndex()))
		lines.append("")
716

Michael Büsch's avatar
Michael Büsch committed
717 718 719 720 721 722 723 724
		lines.append("[CORE_LINK]")
		linkSettings = self.getCoreLinkSettings()
		lines.append("spawn_local=%d" %\
			     int(bool(linkSettings.getSpawnLocalEn())))
		lines.append("spawn_local_port_range=%d:%d" %(\
			     linkSettings.getSpawnLocalPortRange()[0],
			     linkSettings.getSpawnLocalPortRange()[-1]))
		interp = linkSettings.getSpawnLocalInterpreters()
725
		interp = strToBase64(interp, ignoreErrors=True)
Michael Büsch's avatar
Michael Büsch committed
726 727
		lines.append("spawn_local_interpreters=%s" % interp)
		host = linkSettings.getConnectHost()
728
		host = strToBase64(host, ignoreErrors=True)
Michael Büsch's avatar
Michael Büsch committed
729
		lines.append("connect_host=%s" % host)
730 731 732 733
		lines.append("connect_port=%d" %\
			     int(linkSettings.getConnectPort()))
		lines.append("connect_timeout_ms=%d" %\
			     int(linkSettings.getConnectTimeoutMs()))
734 735 736 737 738 739 740 741 742 743
		lines.append("tunnel=%d" % linkSettings.getTunnel())
		lines.append("tunnel_local_port=%d" %\
			     linkSettings.getTunnelLocalPort())
		sshUser = linkSettings.getSSHUser()
		sshUser = strToBase64(sshUser, ignoreErrors=True)
		lines.append("ssh_user=%s" % sshUser)
		lines.append("ssh_port=%d" % linkSettings.getSSHPort())
		sshExecutable = linkSettings.getSSHExecutable()
		sshExecutable = strToBase64(sshExecutable, ignoreErrors=True)
		lines.append("ssh_executable=%s" % sshExecutable)
Michael Büsch's avatar
Michael Büsch committed
744 745
		lines.append("")

746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
		lines.append("[HWMODS]")
		hwSettings = self.getHwmodSettings()
		loadedMods = sorted(hwSettings.getLoadedModules(),
				    key = lambda modDesc: modDesc.getModuleName())
		for modNr, modDesc in enumerate(loadedMods):
			modName = strToBase64(modDesc.getModuleName(),
					      ignoreErrors=True)
			lines.append("loaded_mod_%d=%s" % (modNr, modName))
			modParams = sorted(list(modDesc.getParameters().items()),
					   key = lambda p: p[0])
			for paramNr, param in enumerate(modParams):
				paramName, paramValue = param
				paramName = strToBase64(paramName,
							ignoreErrors=True)
				if paramValue is None:
					paramValue = ""
				else:
					paramValue = strToBase64(paramValue,
								 ignoreErrors=True)
				lines.append("loaded_mod_%d_p%d=%s:%s" %\
					(modNr, paramNr,
					 paramName, paramValue))
		lines.append("")

770
		lines.append("[GUI]")
Michael Büsch's avatar
Michael Büsch committed
771
		guiSettings = self.getGuiSettings()
772
		lines.append("editor_autoindent=%d" %\
Michael Büsch's avatar
Michael Büsch committed
773
			     int(bool(guiSettings.getEditorAutoIndentEn())))
774
		lines.append("editor_paste_autoindent=%d" %\
Michael Büsch's avatar
Michael Büsch committed
775
			     int(bool(guiSettings.getEditorPasteIndentEn())))
776
		lines.append("editor_validation=%d" %\
Michael Büsch's avatar
Michael Büsch committed
777 778
			     int(bool(guiSettings.getEditorValidationEn())))
		lines.append("editor_font=%s" % guiSettings.getEditorFont())
779 780
		lines.append("")

Michael Büsch's avatar
Michael Büsch committed
781 782 783 784 785 786 787 788 789 790
		return "\r\n".join(lines)

	def toFile(self, projectFile=None):
		if not projectFile:
			projectFile = self.projectFile
		if not projectFile:
			raise AwlSimError("Project file: Cannot generate project file. "
				"No file name specified.")
		text = self.toText(projectFile)
		awlFileWrite(projectFile, text, encoding="utf8")
791 792 793 794
		for awlSrc in self.awlSources:
			awlSrc.writeFileBacking()
		for symSrc in self.symTabSources:
			symSrc.writeFileBacking()
795 796 797 798 799

	def __repr__(self):
		if self.projectFile:
			return 'Project("%s")' % self.projectFile
		return "Project(None)"