validate.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/env python3
  2. """Validate a VisiCut settings directory against VisiCut's own conventions.
  3. Mirrors de.thomas_oster.visicut.misc.Helper.toPathName and
  4. LaserPropertyManager.getLaserPropertiesFile.
  5. """
  6. import os, sys, re
  7. import xml.etree.ElementTree as ET
  8. ALLOWED = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-")
  9. def to_path_name(name):
  10. return "".join(c if c in ALLOWED else "_%d_" % ord(c) for c in name)
  11. def jfloat(x):
  12. """Java Float.toString for the values we care about."""
  13. f = float(x)
  14. s = repr(f)
  15. return s
  16. ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
  17. problems = []
  18. def P(cat, msg):
  19. problems.append((cat, msg))
  20. # ---- profiles -------------------------------------------------------------
  21. profiles = {}
  22. for fn in sorted(os.listdir(os.path.join(ROOT, "profiles"))):
  23. p = os.path.join(ROOT, "profiles", fn)
  24. try:
  25. root = ET.parse(p).getroot()
  26. except ET.ParseError as e:
  27. P("xml", "profiles/%s: malformed XML: %s" % (fn, e)); continue
  28. name = root.findtext("name")
  29. profiles[name] = root.tag
  30. if to_path_name(name) + ".xml" != fn:
  31. P("name", "profiles/%s should be named %s.xml (name=%r)" % (fn, to_path_name(name), name))
  32. # Expected XML element per profile kind. LaserPropertyManager only registers XStream
  33. # aliases for PowerSpeedFocus[Frequency]Property, FloatPowerSpeedFocusFrequencyProperty and
  34. # LaosCutterProperty -- any other short element name fails to deserialize. The Epilog driver
  35. # wants an EpilogEngraveProperty for raster parts; VisiCut converts the PowerSpeedFocusProperty
  36. # it reads here into one, copying power/speed/focus.
  37. EXPECT = {
  38. "vectorProfile": "PowerSpeedFocusFrequencyProperty",
  39. "rasterProfile": "PowerSpeedFocusProperty",
  40. "raster3dProfile":"PowerSpeedFocusProperty",
  41. }
  42. # ---- materials ------------------------------------------------------------
  43. materials = {} # name -> dict
  44. for fn in sorted(os.listdir(os.path.join(ROOT, "materials"))):
  45. if not fn.endswith(".xml"): continue
  46. p = os.path.join(ROOT, "materials", fn)
  47. try:
  48. root = ET.parse(p).getroot()
  49. except ET.ParseError as e:
  50. P("xml", "materials/%s: MALFORMED XML - VisiCut cannot load this file: %s" % (fn, e)); continue
  51. name = root.findtext("name")
  52. th = [float(x.text) for x in root.findall("materialThicknesses/float")]
  53. expect_fn = to_path_name(name) + ".xml"
  54. if expect_fn != fn:
  55. P("name", "materials/%s: VisiCut expects %s (name=%r)" % (fn, expect_fn, name))
  56. if name in materials:
  57. P("dup", "duplicate material name %r (%s and %s)" % (name, materials[name]["file"], fn))
  58. materials[name] = {"file": fn, "th": th, "dir": to_path_name(name),
  59. "thumb": root.findtext("thumbnailPath")}
  60. if th != sorted(th):
  61. P("order", "materials/%s: thicknesses not sorted: %s" % (fn, th))
  62. if len(th) != len(set(th)):
  63. P("dup", "materials/%s: duplicate thicknesses %s" % (fn, th))
  64. bydir = {}
  65. for n, m in materials.items():
  66. bydir.setdefault(m["dir"], []).append(n)
  67. # ---- laserprofiles --------------------------------------------------------
  68. lp_root = os.path.join(ROOT, "laserprofiles")
  69. covered = set()
  70. for dev in sorted(os.listdir(lp_root)):
  71. devdir = os.path.join(lp_root, dev)
  72. if not os.path.isdir(devdir): continue
  73. for mdir in sorted(os.listdir(devdir)):
  74. mpath = os.path.join(devdir, mdir)
  75. if not os.path.isdir(mpath): continue
  76. mnames = bydir.get(mdir)
  77. if not mnames:
  78. P("orphan", "laserprofiles/%s/%s/: no material has this encoded name "
  79. "-> invisible in VisiCut (would decode to %r)" % (dev, mdir, mdir))
  80. continue
  81. m = materials[mnames[0]]
  82. for tdir in sorted(os.listdir(mpath)):
  83. tpath = os.path.join(mpath, tdir)
  84. if not os.path.isdir(tpath): continue
  85. if not tdir.endswith("mm"):
  86. P("thick", "laserprofiles/%s/%s/%s: not a <thickness>mm folder" % (dev, mdir, tdir)); continue
  87. tval = tdir[:-2]
  88. if not any(jfloat(t) == tval for t in m["th"]):
  89. P("thick", "laserprofiles/%s/%s/%s: thickness not listed in materials/%s (%s)"
  90. % (dev, mdir, tdir, m["file"], ", ".join(jfloat(t) for t in m["th"])))
  91. else:
  92. covered.add((dev, mnames[0], tval))
  93. for pf in sorted(os.listdir(tpath)):
  94. if not pf.endswith(".xml"): continue
  95. pname = None
  96. for n in profiles:
  97. if to_path_name(n) + ".xml" == pf: pname = n
  98. if pname is None:
  99. P("orphan", "laserprofiles/%s/%s/%s/%s: no matching profiles/*.xml entry" % (dev, mdir, tdir, pf))
  100. continue
  101. f = os.path.join(tpath, pf)
  102. try:
  103. r = ET.parse(f).getroot()
  104. except ET.ParseError as e:
  105. P("xml", "%s: malformed XML: %s" % (f, e)); continue
  106. want = EXPECT[profiles[pname]]
  107. for child in r:
  108. if child.tag != want:
  109. P("class", "laserprofiles/%s/%s/%s/%s: <%s> but profile %r is a %s -> needs <%s>"
  110. % (dev, mdir, tdir, pf, child.tag, pname, profiles[pname], want))
  111. for k, lo, hi in (("power",0,100), ("speed",1,100), ("frequency",10,5000)):
  112. v = child.findtext(k)
  113. if v is None: continue
  114. if not (lo <= float(v) <= hi):
  115. P("range", "laserprofiles/%s/%s/%s/%s: %s=%s out of range %d..%d"
  116. % (dev, mdir, tdir, pf, k, v, lo, hi))
  117. # ---- coverage -------------------------------------------------------------
  118. for dev in sorted(os.listdir(lp_root)):
  119. if not os.path.isdir(os.path.join(lp_root, dev)): continue
  120. for n, m in sorted(materials.items()):
  121. for t in m["th"]:
  122. if (dev, n, jfloat(t)) not in covered:
  123. P("missing", "%s / %r / %smm: material offers this thickness but there are no laser settings"
  124. % (dev, n, jfloat(t)))
  125. # ---- thumbnails -----------------------------------------------------------
  126. for n, m in sorted(materials.items()):
  127. auto = os.path.join(ROOT, "materials", m["dir"] + ".png")
  128. if m["thumb"]:
  129. ref = os.path.join(ROOT, "materials", m["thumb"])
  130. if not os.path.exists(ref):
  131. P("thumb", "materials/%s: thumbnailPath %r does not exist" % (m["file"], m["thumb"]))
  132. elif not os.path.exists(auto):
  133. P("thumb", "materials/%s: no thumbnail (expected materials/%s.png)" % (m["file"], m["dir"]))
  134. order = ["xml","name","dup","class","range","orphan","thick","missing","order","thumb"]
  135. for cat in order:
  136. rows = [m for c, m in problems if c == cat]
  137. if rows:
  138. print("\n## %s (%d)" % (cat.upper(), len(rows)))
  139. for r in rows: print(" - " + r)
  140. print("\nTOTAL: %d problems, %d materials, %d profiles" % (len(problems), len(materials), len(profiles)))
  141. sys.exit(1 if problems else 0)