You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

692 lines
24 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2012 NBEE Embedded Systems, Miguel Angel Ajo <miguelangel@nbee.es>
  5. * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU General Public License
  9. * as published by the Free Software Foundation; either version 2
  10. * of the License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, you may find one here:
  19. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  20. * or you may search the http://www.gnu.org website for the version 2 license,
  21. * or you may write to the Free Software Foundation, Inc.,
  22. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  23. */
  24. /**
  25. * This file builds the base classes for all kind of python plugins that
  26. * can be included into kicad.
  27. * they provide generic code to all the classes:
  28. *
  29. * KiCadPlugin
  30. * /|\
  31. * |
  32. * |\-FilePlugin
  33. * |\-FootprintWizardPlugin
  34. * |\-ActionPlugin
  35. *
  36. * It defines the LoadPlugins() function that loads all the plugins
  37. * available in the system
  38. *
  39. */
  40. /*
  41. * Remark:
  42. * Avoid using the print function in python wizards
  43. *
  44. * Be aware print messages create IO exceptions, because the wizard
  45. * is run from Pcbnew. And if pcbnew is not run from a console, there is
  46. * no io channel to read the output of print function.
  47. * When the io buffer is full, a IO exception is thrown.
  48. */
  49. %pythoncode
  50. {
  51. KICAD_PLUGINS={} # the list of loaded footprint wizards
  52. """ the list of not loaded python scripts
  53. (usually because there is a syntax error in python script)
  54. this is the python script full filenames list.
  55. filenames are separated by '\n'
  56. """
  57. NOT_LOADED_WIZARDS=""
  58. """ the list of paths used to search python scripts.
  59. Stored here to be displayed on request in Pcbnew
  60. paths are separated by '\n'
  61. """
  62. PLUGIN_DIRECTORIES_SEARCH=""
  63. """
  64. the trace of errors during execution of footprint wizards scripts
  65. Warning: strings (internally unicode) are returned as UTF-8 compatible C strings
  66. """
  67. FULL_BACK_TRACE=""
  68. def GetUnLoadableWizards():
  69. global NOT_LOADED_WIZARDS
  70. import sys
  71. if sys.version_info[0] < 3:
  72. utf8_str = NOT_LOADED_WIZARDS.encode( 'UTF-8' )
  73. else:
  74. utf8_str = NOT_LOADED_WIZARDS
  75. return utf8_str
  76. def GetWizardsSearchPaths():
  77. global PLUGIN_DIRECTORIES_SEARCH
  78. import sys
  79. if sys.version_info[0] < 3:
  80. utf8_str = PLUGIN_DIRECTORIES_SEARCH.encode( 'UTF-8' )
  81. else:
  82. utf8_str = PLUGIN_DIRECTORIES_SEARCH
  83. return utf8_str
  84. def GetWizardsBackTrace():
  85. global FULL_BACK_TRACE # Already correct format
  86. return FULL_BACK_TRACE
  87. def LoadPluginModule(Dirname, ModuleName, FileName):
  88. """
  89. Load the plugin module named ModuleName located in the folder Dirname.
  90. The module can be either inside a file called FileName or a subdirectory
  91. called FileName that contains a __init__.py file.
  92. If this module cannot be loaded, its name is stored in failed_wizards_list
  93. and the error trace is stored in FULL_BACK_TRACE
  94. """
  95. import os
  96. import sys
  97. import traceback
  98. global NOT_LOADED_WIZARDS
  99. global FULL_BACK_TRACE
  100. global KICAD_PLUGINS
  101. try: # If there is an error loading the script, skip it
  102. module_filename = os.path.join( Dirname, FileName )
  103. mtime = os.path.getmtime( module_filename )
  104. mods_before = set( sys.modules )
  105. if ModuleName in KICAD_PLUGINS:
  106. plugin = KICAD_PLUGINS[ModuleName]
  107. for dependency in plugin["dependencies"]:
  108. if dependency in sys.modules:
  109. del sys.modules[dependency]
  110. mods_before = set( sys.modules )
  111. if sys.version_info >= (3,0,0):
  112. import importlib
  113. mod = importlib.import_module( ModuleName )
  114. else:
  115. mod = __import__( ModuleName, locals(), globals() )
  116. mods_after = set( sys.modules ).difference( mods_before )
  117. dependencies = [m for m in mods_after if m.startswith(ModuleName)]
  118. KICAD_PLUGINS[ModuleName]={ "filename":module_filename,
  119. "modification_time":mtime,
  120. "ModuleName":mod,
  121. "dependencies": dependencies }
  122. except:
  123. if ModuleName in KICAD_PLUGINS:
  124. del KICAD_PLUGINS[ModuleName]
  125. if NOT_LOADED_WIZARDS != "" :
  126. NOT_LOADED_WIZARDS += "\n"
  127. NOT_LOADED_WIZARDS += module_filename
  128. FULL_BACK_TRACE += traceback.format_exc()
  129. def LoadPlugins(bundlepath=None, userpath=None, thirdpartypath=None):
  130. """
  131. Initialise Scripting/Plugin python environment and load plugins.
  132. Arguments:
  133. Note: bundlepath and userpath are given utf8 encoded, to be compatible with asimple C string
  134. bundlepath -- The path to the bundled scripts.
  135. The bundled Plugins are relative to this path, in the
  136. "plugins" subdirectory.
  137. WARNING: bundlepath must use '/' as path separator, and not '\'
  138. because it creates issues:
  139. \n and \r are seen as a escaped seq when passing this string to this method
  140. I am thinking this is due to the fact LoadPlugins is called from C++ code by
  141. PyRun_SimpleString()
  142. NOTE: These are all of the possible "default" search paths for kicad
  143. python scripts. These paths will ONLY be added to the python
  144. search path ONLY IF they already exist.
  145. The Scripts bundled with the KiCad installation:
  146. <bundlepath>/
  147. <bundlepath>/plugins/
  148. The Scripts relative to the KiCad Users configuration:
  149. <userpath>/
  150. <userpath>/plugins/
  151. The plugins from 3rd party packages:
  152. $KICAD_3RD_PARTY/plugins/
  153. """
  154. import os
  155. import sys
  156. import traceback
  157. import pcbnew
  158. if sys.version_info >= (3,3,0):
  159. import importlib
  160. importlib.invalidate_caches()
  161. """
  162. bundlepath and userpath are strings utf-8 encoded (compatible "C" strings).
  163. So convert these utf8 encoding to unicode strings to avoid any encoding issue.
  164. """
  165. try:
  166. bundlepath = bundlepath.decode( 'UTF-8' )
  167. userpath = userpath.decode( 'UTF-8' )
  168. thirdpartypath = thirdpartypath.decode( 'UTF-8' )
  169. except AttributeError:
  170. pass
  171. config_path = pcbnew.SETTINGS_MANAGER.GetUserSettingsPath()
  172. plugin_directories=[]
  173. """
  174. To be consistent with others paths, on windows, convert the unix '/' separator
  175. to the windows separator, although using '/' works
  176. """
  177. if sys.platform.startswith('win32'):
  178. if bundlepath:
  179. bundlepath = bundlepath.replace("/","\\")
  180. if thirdpartypath:
  181. thirdpartypath = thirdpartypath.replace("/","\\")
  182. if bundlepath:
  183. plugin_directories.append(bundlepath)
  184. plugin_directories.append(os.path.join(bundlepath, 'plugins'))
  185. if config_path:
  186. plugin_directories.append(os.path.join(config_path, 'scripting'))
  187. plugin_directories.append(os.path.join(config_path, 'scripting', 'plugins'))
  188. if userpath:
  189. plugin_directories.append(userpath)
  190. plugin_directories.append(os.path.join(userpath, 'plugins'))
  191. if thirdpartypath:
  192. plugin_directories.append(thirdpartypath)
  193. global PLUGIN_DIRECTORIES_SEARCH
  194. PLUGIN_DIRECTORIES_SEARCH=""
  195. for plugins_dir in plugin_directories: # save search path list for later use
  196. if PLUGIN_DIRECTORIES_SEARCH != "" :
  197. PLUGIN_DIRECTORIES_SEARCH += "\n"
  198. PLUGIN_DIRECTORIES_SEARCH += plugins_dir
  199. global FULL_BACK_TRACE
  200. FULL_BACK_TRACE="" # clear any existing trace
  201. global NOT_LOADED_WIZARDS
  202. NOT_LOADED_WIZARDS = "" # save not loaded wizards names list for later use
  203. global KICAD_PLUGINS
  204. for plugins_dir in plugin_directories:
  205. if not os.path.isdir( plugins_dir ):
  206. continue
  207. if plugins_dir not in sys.path:
  208. sys.path.append( plugins_dir )
  209. for module in os.listdir(plugins_dir):
  210. fullPath = os.path.join( plugins_dir, module )
  211. if os.path.isdir( fullPath ):
  212. if os.path.exists( os.path.join( fullPath, '__init__.py' ) ):
  213. LoadPluginModule( plugins_dir, module, module )
  214. else:
  215. if NOT_LOADED_WIZARDS != "" :
  216. NOT_LOADED_WIZARDS += "\n"
  217. NOT_LOADED_WIZARDS += 'Skip subdir ' + fullPath
  218. continue
  219. if module == '__init__.py' or module[-3:] != '.py':
  220. continue
  221. LoadPluginModule( plugins_dir, module[:-3], module )
  222. class KiCadPlugin:
  223. def __init__(self):
  224. pass
  225. def register(self):
  226. import inspect
  227. import os
  228. if isinstance(self,FilePlugin):
  229. pass # register to file plugins in C++
  230. if isinstance(self,FootprintWizardPlugin):
  231. PYTHON_FOOTPRINT_WIZARD_LIST.register_wizard(self)
  232. return
  233. if isinstance(self,ActionPlugin):
  234. """
  235. Get path to .py or .pyc that has definition of plugin class.
  236. If path is binary but source also exists, assume definition is in source.
  237. """
  238. self.__plugin_path = inspect.getfile(self.__class__)
  239. if self.__plugin_path.endswith('.pyc') and os.path.isfile(self.__plugin_path[:-1]):
  240. self.__plugin_path = self.__plugin_path[:-1]
  241. self.__plugin_path = self.__plugin_path + '/' + self.__class__.__name__
  242. PYTHON_ACTION_PLUGINS.register_action(self)
  243. return
  244. return
  245. def deregister(self):
  246. if isinstance(self,FilePlugin):
  247. pass # deregister to file plugins in C++
  248. if isinstance(self,FootprintWizardPlugin):
  249. PYTHON_FOOTPRINT_WIZARD_LIST.deregister_wizard(self)
  250. return
  251. if isinstance(self,ActionPlugin):
  252. PYTHON_ACTION_PLUGINS.deregister_action(self)
  253. return
  254. return
  255. def GetPluginPath( self ):
  256. return self.__plugin_path
  257. class FilePlugin(KiCadPlugin):
  258. def __init__(self):
  259. KiCadPlugin.__init__(self)
  260. from math import ceil, floor, sqrt
  261. uMM = "mm" # Millimetres
  262. uMils = "mils" # Mils
  263. uFloat = "float" # Natural number units (dimensionless)
  264. uInteger = "integer" # Integer (no decimals, numeric, dimensionless)
  265. uBool = "bool" # Boolean value
  266. uRadians = "radians" # Angular units (radians)
  267. uDegrees = "degrees" # Angular units (degrees)
  268. uPercent = "%" # Percent (0% -> 100%)
  269. uString = "string" # Raw string
  270. uNumeric = [uMM, uMils, uFloat, uInteger, uDegrees, uRadians, uPercent] # List of numeric types
  271. uUnits = [uMM, uMils, uFloat, uInteger, uBool, uDegrees, uRadians, uPercent, uString] # List of allowable types
  272. class FootprintWizardParameter(object):
  273. _true = ['true','t','y','yes','on','1',1,]
  274. _false = ['false','f','n','no','off','0',0,'',None]
  275. _bools = _true + _false
  276. def __init__(self, page, name, units, default, **kwarg):
  277. self.page = page
  278. self.name = name
  279. self.hint = kwarg.get('hint','') # Parameter hint (shown as mouse-over text)
  280. self.designator = kwarg.get('designator',' ') # Parameter designator such as "e, D, p" (etc)
  281. if units.lower() in uUnits:
  282. self.units = units.lower()
  283. elif units.lower() == 'percent':
  284. self.units = uPercent
  285. elif type(units) in [list, tuple]: # Convert a list of options into a single string
  286. self.units = ",".join([str(el).strip() for el in units])
  287. else:
  288. self.units = units
  289. self.multiple = int(kwarg.get('multiple',1)) # Check integer values are multiples of this number
  290. self.min_value = kwarg.get('min_value',None) # Check numeric values are above or equal to this number
  291. self.max_value = kwarg.get('max_value',None) # Check numeric values are below or equal to this number
  292. self.SetValue(default)
  293. self.default = self.raw_value # Save value as default
  294. def ClearErrors(self):
  295. self.error_list = []
  296. def AddError(self, err, info=None):
  297. if err in self.error_list: # prevent duplicate error messages
  298. return
  299. if info is not None:
  300. err = err + " (" + str(info) + ")"
  301. self.error_list.append(err)
  302. def Check(self, min_value=None, max_value=None, multiple=None, info=None):
  303. if min_value is None:
  304. min_value = self.min_value
  305. if max_value is None:
  306. max_value = self.max_value
  307. if multiple is None:
  308. multiple = self.multiple
  309. if self.units not in uUnits and ',' not in self.units: # Allow either valid units or a list of strings
  310. self.AddError("type '{t}' unknown".format(t=self.units),info)
  311. self.AddError("Allowable types: " + str(self.units),info)
  312. if self.units in uNumeric:
  313. try:
  314. to_num = float(self.raw_value)
  315. if min_value is not None: # Check minimum value if it is present
  316. if to_num < min_value:
  317. self.AddError("value '{v}' is below minimum ({m})".format(v=self.raw_value,m=min_value),info)
  318. if max_value is not None: # Check maximum value if it is present
  319. if to_num > max_value:
  320. self.AddError("value '{v}' is above maximum ({m})".format(v=self.raw_value,m=max_value),info)
  321. except:
  322. self.AddError("value '{v}' is not of type '{t}'".format(v = self.raw_value, t=self.units),info)
  323. if self.units == uInteger: # Perform integer specific checks
  324. try:
  325. to_int = int(self.raw_value)
  326. if multiple is not None and multiple > 1:
  327. if (to_int % multiple) > 0:
  328. self.AddError("value '{v}' is not a multiple of {m}".format(v=self.raw_value,m=multiple),info)
  329. except:
  330. self.AddError("value '{v}' is not an integer".format(v=self.raw_value),info)
  331. if self.units == uBool: # Check that the value is of a correct boolean format
  332. if self.raw_value in [True,False] or str(self.raw_value).lower() in self._bools:
  333. pass
  334. else:
  335. self.AddError("value '{v}' is not a boolean value".format(v = self.raw_value),info)
  336. @property
  337. def value(self): # Return the current value, converted to appropriate units (from string representation) if required
  338. v = str(self.raw_value) # Enforce string type for known starting point
  339. if self.units == uInteger: # Integer values
  340. return int(v)
  341. elif self.units in uNumeric: # Any values that use floating points
  342. v = v.replace(",",".") # Replace "," separators with "."
  343. v = float(v)
  344. if self.units == uMM: # Convert from millimetres to nanometres
  345. return FromMM(v)
  346. elif self.units == uMils: # Convert from mils to nanometres
  347. return FromMils(v)
  348. else: # Any other floating-point values
  349. return v
  350. elif self.units == uBool:
  351. if v.lower() in self._true:
  352. return True
  353. else:
  354. return False
  355. else:
  356. return v
  357. def DefaultValue(self): # Reset the value of the parameter to its default
  358. self.raw_value = str(self.default)
  359. def SetValue(self, new_value): # Update the value
  360. new_value = str(new_value)
  361. if len(new_value.strip()) == 0:
  362. if not self.units in [uString, uBool]:
  363. return # Ignore empty values unless for strings or bools
  364. if self.units == uBool: # Enforce the same boolean representation as is used in KiCad
  365. new_value = "1" if new_value.lower() in self._true else "0"
  366. elif self.units in uNumeric:
  367. new_value = new_value.replace(",", ".") # Enforce decimal point separators
  368. elif ',' in self.units: # Select from a list of values
  369. if new_value not in self.units.split(','):
  370. new_value = self.units.split(',')[0]
  371. self.raw_value = new_value
  372. def __str__(self): # pretty-print the parameter
  373. s = self.name + ": " + str(self.raw_value)
  374. if self.units in [uMM, uMils, uPercent, uRadians, uDegrees]:
  375. s += self.units
  376. elif self.units == uBool: # Special case for Boolean values
  377. s = self.name + ": {b}".format(b = "True" if self.value else "False")
  378. elif self.units == uString:
  379. s = self.name + ": '" + self.raw_value + "'"
  380. return s
  381. class FootprintWizardPlugin(KiCadPlugin, object):
  382. def __init__(self):
  383. KiCadPlugin.__init__(self)
  384. self.defaults()
  385. def defaults(self):
  386. self.module = None
  387. self.params = [] # List of added parameters that observes addition order
  388. self.name = "KiCad FP Wizard"
  389. self.description = "Undefined Footprint Wizard plugin"
  390. self.image = ""
  391. self.buildmessages = ""
  392. def AddParam(self, page, name, unit, default, **kwarg):
  393. if self.GetParam(page,name) is not None: # Param already exists!
  394. return
  395. param = FootprintWizardParameter(page, name, unit, default, **kwarg) # Create a new parameter
  396. self.params.append(param)
  397. @property
  398. def parameters(self): # This is a helper function that returns a nested (unordered) dict of the VALUES of parameters
  399. pages = {} # Page dict
  400. for p in self.params:
  401. if p.page not in pages:
  402. pages[p.page] = {}
  403. pages[p.page][p.name] = p.value # Return the 'converted' value (convert from string to actual useful units)
  404. return pages
  405. @property
  406. def values(self): # Same as above
  407. return self.parameters
  408. def ResetWizard(self): # Reset all parameters to default values
  409. for p in self.params:
  410. p.DefaultValue()
  411. def GetName(self): # Return the name of this wizard
  412. return self.name
  413. def GetImage(self): # Return the filename of the preview image associated with this wizard
  414. return self.image
  415. def GetDescription(self): # Return the description text
  416. return self.description
  417. def GetValue(self):
  418. raise NotImplementedError
  419. def GetReferencePrefix(self):
  420. return "REF" # Default reference prefix for any footprint
  421. def GetParam(self, page, name): # Grab a parameter
  422. for p in self.params:
  423. if p.page == page and p.name == name:
  424. return p
  425. return None
  426. def CheckParam(self, page, name, **kwarg):
  427. self.GetParam(page,name).Check(**kwarg)
  428. def AnyErrors(self):
  429. return any([len(p.error_list) > 0 for p in self.params])
  430. @property
  431. def pages(self): # Return an (ordered) list of the available page names
  432. page_list = []
  433. for p in self.params:
  434. if p.page not in page_list:
  435. page_list.append(p.page)
  436. return page_list
  437. def GetNumParameterPages(self): # Return the number of parameter pages
  438. return len(self.pages)
  439. def GetParameterPageName(self,page_n): # Return the name of a page at a given index
  440. return self.pages[page_n]
  441. def GetParametersByPageName(self, page_name): # Return a list of parameters on a given page
  442. params = []
  443. for p in self.params:
  444. if p.page == page_name:
  445. params.append(p)
  446. return params
  447. def GetParametersByPageIndex(self, page_index): # Return an ordered list of parameters on a given page
  448. return self.GetParametersByPageName(self.GetParameterPageName(page_index))
  449. def GetParameterDesignators(self, page_index): # Return a list of designators associated with a given page
  450. params = self.GetParametersByPageIndex(page_index)
  451. return [p.designator for p in params]
  452. def GetParameterNames(self,page_index): # Return the list of names associated with a given page
  453. params = self.GetParametersByPageIndex(page_index)
  454. return [p.name for p in params]
  455. def GetParameterValues(self,page_index): # Return the list of values associated with a given page
  456. params = self.GetParametersByPageIndex(page_index)
  457. return [str(p.raw_value) for p in params]
  458. def GetParameterErrors(self,page_index): # Return list of errors associated with a given page
  459. params = self.GetParametersByPageIndex(page_index)
  460. return [str("\n".join(p.error_list)) for p in params]
  461. def GetParameterTypes(self, page_index): # Return list of units associated with a given page
  462. params = self.GetParametersByPageIndex(page_index)
  463. return [str(p.units) for p in params]
  464. def GetParameterHints(self, page_index): # Return a list of units associated with a given page
  465. params = self.GetParametersByPageIndex(page_index)
  466. return [str(p.hint) for p in params]
  467. def GetParameterDesignators(self, page_index): # Return a list of designators associated with a given page
  468. params = self.GetParametersByPageIndex(page_index)
  469. return [str(p.designator) for p in params]
  470. def SetParameterValues(self, page_index, list_of_values): # Update values on a given page
  471. params = self.GetParametersByPageIndex(page_index)
  472. for i, param in enumerate(params):
  473. if i >= len(list_of_values):
  474. break
  475. param.SetValue(list_of_values[i])
  476. def GetFootprint( self ):
  477. self.BuildFootprint()
  478. return self.module
  479. def BuildFootprint(self):
  480. return
  481. def GetBuildMessages( self ):
  482. return self.buildmessages
  483. def Show(self):
  484. text = "Footprint Wizard Name: {name}\n".format(name=self.GetName())
  485. text += "Footprint Wizard Description: {desc}\n".format(desc=self.GetDescription())
  486. n_pages = self.GetNumParameterPages()
  487. text += "Pages: {n}\n".format(n=n_pages)
  488. for i in range(n_pages):
  489. name = self.GetParameterPageName(i)
  490. params = self.GetParametersByPageName(name)
  491. text += "{name}\n".format(name=name)
  492. for j in range(len(params)):
  493. text += ("\t{param}{err}\n".format(
  494. param = str(params[j]),
  495. err = ' *' if len(params[j].error_list) > 0 else ''
  496. ))
  497. if self.AnyErrors():
  498. text += " * Errors exist for these parameters"
  499. return text
  500. class ActionPlugin(KiCadPlugin, object):
  501. def __init__( self ):
  502. KiCadPlugin.__init__( self )
  503. self.icon_file_name = ""
  504. self.dark_icon_file_name = ""
  505. self.show_toolbar_button = False
  506. self.defaults()
  507. def defaults( self ):
  508. self.name = "Undefined Action plugin"
  509. self.category = "Undefined"
  510. self.description = ""
  511. def GetName( self ):
  512. return self.name
  513. def GetCategoryName( self ):
  514. return self.category
  515. def GetDescription( self ):
  516. return self.description
  517. def GetShowToolbarButton( self ):
  518. return self.show_toolbar_button
  519. def GetIconFileName( self, dark ):
  520. if dark and self.dark_icon_file_name:
  521. return self.dark_icon_file_name
  522. else:
  523. return self.icon_file_name
  524. def Run(self):
  525. return
  526. }