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.

700 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. try:
  99. from importlib import reload # Python 3.4 or above
  100. except ImportError:
  101. from imp import reload # Python <3.4; harmless alias on 2.7
  102. global NOT_LOADED_WIZARDS
  103. global FULL_BACK_TRACE
  104. global KICAD_PLUGINS
  105. top_level_modules = KICAD_PLUGINS.keys()
  106. try: # If there is an error loading the script, skip it
  107. module_filename = os.path.join( Dirname, FileName )
  108. mtime = os.path.getmtime( module_filename )
  109. mods_before = set( sys.modules )
  110. if ModuleName in KICAD_PLUGINS:
  111. plugin = KICAD_PLUGINS[ModuleName]
  112. for dependency in plugin["dependencies"]:
  113. if dependency in sys.modules and dependency not in top_level_modules:
  114. del sys.modules[dependency]
  115. mods_before = set( sys.modules )
  116. mod = reload( plugin["ModuleName"] )
  117. else:
  118. if sys.version_info >= (3,0,0):
  119. import importlib
  120. mod = importlib.import_module( ModuleName )
  121. else:
  122. mod = __import__( ModuleName, locals(), globals() )
  123. mods_after = set( sys.modules ).difference( mods_before )
  124. KICAD_PLUGINS[ModuleName]={ "filename":module_filename,
  125. "modification_time":mtime,
  126. "ModuleName":mod,
  127. "dependencies": mods_after }
  128. except:
  129. if ModuleName in KICAD_PLUGINS:
  130. del KICAD_PLUGINS[ModuleName]
  131. if NOT_LOADED_WIZARDS != "" :
  132. NOT_LOADED_WIZARDS += "\n"
  133. NOT_LOADED_WIZARDS += module_filename
  134. FULL_BACK_TRACE += traceback.format_exc()
  135. def LoadPlugins(bundlepath=None, userpath=None, thirdpartypath=None):
  136. """
  137. Initialise Scripting/Plugin python environment and load plugins.
  138. Arguments:
  139. Note: bundlepath and userpath are given utf8 encoded, to be compatible with asimple C string
  140. bundlepath -- The path to the bundled scripts.
  141. The bundled Plugins are relative to this path, in the
  142. "plugins" subdirectory.
  143. WARNING: bundlepath must use '/' as path separator, and not '\'
  144. because it creates issues:
  145. \n and \r are seen as a escaped seq when passing this string to this method
  146. I am thinking this is due to the fact LoadPlugins is called from C++ code by
  147. PyRun_SimpleString()
  148. NOTE: These are all of the possible "default" search paths for kicad
  149. python scripts. These paths will ONLY be added to the python
  150. search path ONLY IF they already exist.
  151. The Scripts bundled with the KiCad installation:
  152. <bundlepath>/
  153. <bundlepath>/plugins/
  154. The Scripts relative to the KiCad Users configuration:
  155. <userpath>/
  156. <userpath>/plugins/
  157. The plugins from 3rd party packages:
  158. $KICAD_3RD_PARTY/plugins/
  159. """
  160. import os
  161. import sys
  162. import traceback
  163. import pcbnew
  164. if sys.version_info >= (3,3,0):
  165. import importlib
  166. importlib.invalidate_caches()
  167. """
  168. bundlepath and userpath are strings utf-8 encoded (compatible "C" strings).
  169. So convert these utf8 encoding to unicode strings to avoid any encoding issue.
  170. """
  171. try:
  172. bundlepath = bundlepath.decode( 'UTF-8' )
  173. userpath = userpath.decode( 'UTF-8' )
  174. thirdpartypath = thirdpartypath.decode( 'UTF-8' )
  175. except AttributeError:
  176. pass
  177. config_path = pcbnew.SETTINGS_MANAGER.GetUserSettingsPath()
  178. plugin_directories=[]
  179. """
  180. To be consistent with others paths, on windows, convert the unix '/' separator
  181. to the windows separator, although using '/' works
  182. """
  183. if sys.platform.startswith('win32'):
  184. if bundlepath:
  185. bundlepath = bundlepath.replace("/","\\")
  186. if thirdpartypath:
  187. thirdpartypath = thirdpartypath.replace("/","\\")
  188. if bundlepath:
  189. plugin_directories.append(bundlepath)
  190. plugin_directories.append(os.path.join(bundlepath, 'plugins'))
  191. if config_path:
  192. plugin_directories.append(os.path.join(config_path, 'scripting'))
  193. plugin_directories.append(os.path.join(config_path, 'scripting', 'plugins'))
  194. if userpath:
  195. plugin_directories.append(userpath)
  196. plugin_directories.append(os.path.join(userpath, 'plugins'))
  197. if thirdpartypath:
  198. plugin_directories.append(thirdpartypath)
  199. global PLUGIN_DIRECTORIES_SEARCH
  200. PLUGIN_DIRECTORIES_SEARCH=""
  201. for plugins_dir in plugin_directories: # save search path list for later use
  202. if PLUGIN_DIRECTORIES_SEARCH != "" :
  203. PLUGIN_DIRECTORIES_SEARCH += "\n"
  204. PLUGIN_DIRECTORIES_SEARCH += plugins_dir
  205. global FULL_BACK_TRACE
  206. FULL_BACK_TRACE="" # clear any existing trace
  207. global NOT_LOADED_WIZARDS
  208. NOT_LOADED_WIZARDS = "" # save not loaded wizards names list for later use
  209. global KICAD_PLUGINS
  210. for plugins_dir in plugin_directories:
  211. if not os.path.isdir( plugins_dir ):
  212. continue
  213. sys.path.append( plugins_dir )
  214. for module in os.listdir(plugins_dir):
  215. fullPath = os.path.join( plugins_dir, module )
  216. if os.path.isdir( fullPath ):
  217. if os.path.exists( os.path.join( fullPath, '__init__.py' ) ):
  218. LoadPluginModule( plugins_dir, module, module )
  219. else:
  220. if NOT_LOADED_WIZARDS != "" :
  221. NOT_LOADED_WIZARDS += "\n"
  222. NOT_LOADED_WIZARDS += 'Skip subdir ' + fullPath
  223. continue
  224. if module == '__init__.py' or module[-3:] != '.py':
  225. continue
  226. LoadPluginModule( plugins_dir, module[:-3], module )
  227. class KiCadPlugin:
  228. def __init__(self):
  229. pass
  230. def register(self):
  231. import inspect
  232. import os
  233. if isinstance(self,FilePlugin):
  234. pass # register to file plugins in C++
  235. if isinstance(self,FootprintWizardPlugin):
  236. PYTHON_FOOTPRINT_WIZARD_LIST.register_wizard(self)
  237. return
  238. if isinstance(self,ActionPlugin):
  239. """
  240. Get path to .py or .pyc that has definition of plugin class.
  241. If path is binary but source also exists, assume definition is in source.
  242. """
  243. self.__plugin_path = inspect.getfile(self.__class__)
  244. if self.__plugin_path.endswith('.pyc') and os.path.isfile(self.__plugin_path[:-1]):
  245. self.__plugin_path = self.__plugin_path[:-1]
  246. self.__plugin_path = self.__plugin_path + '/' + self.__class__.__name__
  247. PYTHON_ACTION_PLUGINS.register_action(self)
  248. return
  249. return
  250. def deregister(self):
  251. if isinstance(self,FilePlugin):
  252. pass # deregister to file plugins in C++
  253. if isinstance(self,FootprintWizardPlugin):
  254. PYTHON_FOOTPRINT_WIZARD_LIST.deregister_wizard(self)
  255. return
  256. if isinstance(self,ActionPlugin):
  257. PYTHON_ACTION_PLUGINS.deregister_action(self)
  258. return
  259. return
  260. def GetPluginPath( self ):
  261. return self.__plugin_path
  262. class FilePlugin(KiCadPlugin):
  263. def __init__(self):
  264. KiCadPlugin.__init__(self)
  265. from math import ceil, floor, sqrt
  266. uMM = "mm" # Millimetres
  267. uMils = "mils" # Mils
  268. uFloat = "float" # Natural number units (dimensionless)
  269. uInteger = "integer" # Integer (no decimals, numeric, dimensionless)
  270. uBool = "bool" # Boolean value
  271. uRadians = "radians" # Angular units (radians)
  272. uDegrees = "degrees" # Angular units (degrees)
  273. uPercent = "%" # Percent (0% -> 100%)
  274. uString = "string" # Raw string
  275. uNumeric = [uMM, uMils, uFloat, uInteger, uDegrees, uRadians, uPercent] # List of numeric types
  276. uUnits = [uMM, uMils, uFloat, uInteger, uBool, uDegrees, uRadians, uPercent, uString] # List of allowable types
  277. class FootprintWizardParameter(object):
  278. _true = ['true','t','y','yes','on','1',1,]
  279. _false = ['false','f','n','no','off','0',0,'',None]
  280. _bools = _true + _false
  281. def __init__(self, page, name, units, default, **kwarg):
  282. self.page = page
  283. self.name = name
  284. self.hint = kwarg.get('hint','') # Parameter hint (shown as mouse-over text)
  285. self.designator = kwarg.get('designator',' ') # Parameter designator such as "e, D, p" (etc)
  286. if units.lower() in uUnits:
  287. self.units = units.lower()
  288. elif units.lower() == 'percent':
  289. self.units = uPercent
  290. elif type(units) in [list, tuple]: # Convert a list of options into a single string
  291. self.units = ",".join([str(el).strip() for el in units])
  292. else:
  293. self.units = units
  294. self.multiple = int(kwarg.get('multiple',1)) # Check integer values are multiples of this number
  295. self.min_value = kwarg.get('min_value',None) # Check numeric values are above or equal to this number
  296. self.max_value = kwarg.get('max_value',None) # Check numeric values are below or equal to this number
  297. self.SetValue(default)
  298. self.default = self.raw_value # Save value as default
  299. def ClearErrors(self):
  300. self.error_list = []
  301. def AddError(self, err, info=None):
  302. if err in self.error_list: # prevent duplicate error messages
  303. return
  304. if info is not None:
  305. err = err + " (" + str(info) + ")"
  306. self.error_list.append(err)
  307. def Check(self, min_value=None, max_value=None, multiple=None, info=None):
  308. if min_value is None:
  309. min_value = self.min_value
  310. if max_value is None:
  311. max_value = self.max_value
  312. if multiple is None:
  313. multiple = self.multiple
  314. if self.units not in uUnits and ',' not in self.units: # Allow either valid units or a list of strings
  315. self.AddError("type '{t}' unknown".format(t=self.units),info)
  316. self.AddError("Allowable types: " + str(self.units),info)
  317. if self.units in uNumeric:
  318. try:
  319. to_num = float(self.raw_value)
  320. if min_value is not None: # Check minimum value if it is present
  321. if to_num < min_value:
  322. self.AddError("value '{v}' is below minimum ({m})".format(v=self.raw_value,m=min_value),info)
  323. if max_value is not None: # Check maximum value if it is present
  324. if to_num > max_value:
  325. self.AddError("value '{v}' is above maximum ({m})".format(v=self.raw_value,m=max_value),info)
  326. except:
  327. self.AddError("value '{v}' is not of type '{t}'".format(v = self.raw_value, t=self.units),info)
  328. if self.units == uInteger: # Perform integer specific checks
  329. try:
  330. to_int = int(self.raw_value)
  331. if multiple is not None and multiple > 1:
  332. if (to_int % multiple) > 0:
  333. self.AddError("value '{v}' is not a multiple of {m}".format(v=self.raw_value,m=multiple),info)
  334. except:
  335. self.AddError("value '{v}' is not an integer".format(v=self.raw_value),info)
  336. if self.units == uBool: # Check that the value is of a correct boolean format
  337. if self.raw_value in [True,False] or str(self.raw_value).lower() in self._bools:
  338. pass
  339. else:
  340. self.AddError("value '{v}' is not a boolean value".format(v = self.raw_value),info)
  341. @property
  342. def value(self): # Return the current value, converted to appropriate units (from string representation) if required
  343. v = str(self.raw_value) # Enforce string type for known starting point
  344. if self.units == uInteger: # Integer values
  345. return int(v)
  346. elif self.units in uNumeric: # Any values that use floating points
  347. v = v.replace(",",".") # Replace "," separators with "."
  348. v = float(v)
  349. if self.units == uMM: # Convert from millimetres to nanometres
  350. return FromMM(v)
  351. elif self.units == uMils: # Convert from mils to nanometres
  352. return FromMils(v)
  353. else: # Any other floating-point values
  354. return v
  355. elif self.units == uBool:
  356. if v.lower() in self._true:
  357. return True
  358. else:
  359. return False
  360. else:
  361. return v
  362. def DefaultValue(self): # Reset the value of the parameter to its default
  363. self.raw_value = str(self.default)
  364. def SetValue(self, new_value): # Update the value
  365. new_value = str(new_value)
  366. if len(new_value.strip()) == 0:
  367. if not self.units in [uString, uBool]:
  368. return # Ignore empty values unless for strings or bools
  369. if self.units == uBool: # Enforce the same boolean representation as is used in KiCad
  370. new_value = "1" if new_value.lower() in self._true else "0"
  371. elif self.units in uNumeric:
  372. new_value = new_value.replace(",", ".") # Enforce decimal point separators
  373. elif ',' in self.units: # Select from a list of values
  374. if new_value not in self.units.split(','):
  375. new_value = self.units.split(',')[0]
  376. self.raw_value = new_value
  377. def __str__(self): # pretty-print the parameter
  378. s = self.name + ": " + str(self.raw_value)
  379. if self.units in [uMM, uMils, uPercent, uRadians, uDegrees]:
  380. s += self.units
  381. elif self.units == uBool: # Special case for Boolean values
  382. s = self.name + ": {b}".format(b = "True" if self.value else "False")
  383. elif self.units == uString:
  384. s = self.name + ": '" + self.raw_value + "'"
  385. return s
  386. class FootprintWizardPlugin(KiCadPlugin, object):
  387. def __init__(self):
  388. KiCadPlugin.__init__(self)
  389. self.defaults()
  390. def defaults(self):
  391. self.module = None
  392. self.params = [] # List of added parameters that observes addition order
  393. self.name = "KiCad FP Wizard"
  394. self.description = "Undefined Footprint Wizard plugin"
  395. self.image = ""
  396. self.buildmessages = ""
  397. def AddParam(self, page, name, unit, default, **kwarg):
  398. if self.GetParam(page,name) is not None: # Param already exists!
  399. return
  400. param = FootprintWizardParameter(page, name, unit, default, **kwarg) # Create a new parameter
  401. self.params.append(param)
  402. @property
  403. def parameters(self): # This is a helper function that returns a nested (unordered) dict of the VALUES of parameters
  404. pages = {} # Page dict
  405. for p in self.params:
  406. if p.page not in pages:
  407. pages[p.page] = {}
  408. pages[p.page][p.name] = p.value # Return the 'converted' value (convert from string to actual useful units)
  409. return pages
  410. @property
  411. def values(self): # Same as above
  412. return self.parameters
  413. def ResetWizard(self): # Reset all parameters to default values
  414. for p in self.params:
  415. p.DefaultValue()
  416. def GetName(self): # Return the name of this wizard
  417. return self.name
  418. def GetImage(self): # Return the filename of the preview image associated with this wizard
  419. return self.image
  420. def GetDescription(self): # Return the description text
  421. return self.description
  422. def GetValue(self):
  423. raise NotImplementedError
  424. def GetReferencePrefix(self):
  425. return "REF" # Default reference prefix for any footprint
  426. def GetParam(self, page, name): # Grab a parameter
  427. for p in self.params:
  428. if p.page == page and p.name == name:
  429. return p
  430. return None
  431. def CheckParam(self, page, name, **kwarg):
  432. self.GetParam(page,name).Check(**kwarg)
  433. def AnyErrors(self):
  434. return any([len(p.error_list) > 0 for p in self.params])
  435. @property
  436. def pages(self): # Return an (ordered) list of the available page names
  437. page_list = []
  438. for p in self.params:
  439. if p.page not in page_list:
  440. page_list.append(p.page)
  441. return page_list
  442. def GetNumParameterPages(self): # Return the number of parameter pages
  443. return len(self.pages)
  444. def GetParameterPageName(self,page_n): # Return the name of a page at a given index
  445. return self.pages[page_n]
  446. def GetParametersByPageName(self, page_name): # Return a list of parameters on a given page
  447. params = []
  448. for p in self.params:
  449. if p.page == page_name:
  450. params.append(p)
  451. return params
  452. def GetParametersByPageIndex(self, page_index): # Return an ordered list of parameters on a given page
  453. return self.GetParametersByPageName(self.GetParameterPageName(page_index))
  454. def GetParameterDesignators(self, page_index): # Return a list of designators associated with a given page
  455. params = self.GetParametersByPageIndex(page_index)
  456. return [p.designator for p in params]
  457. def GetParameterNames(self,page_index): # Return the list of names associated with a given page
  458. params = self.GetParametersByPageIndex(page_index)
  459. return [p.name for p in params]
  460. def GetParameterValues(self,page_index): # Return the list of values associated with a given page
  461. params = self.GetParametersByPageIndex(page_index)
  462. return [str(p.raw_value) for p in params]
  463. def GetParameterErrors(self,page_index): # Return list of errors associated with a given page
  464. params = self.GetParametersByPageIndex(page_index)
  465. return [str("\n".join(p.error_list)) for p in params]
  466. def GetParameterTypes(self, page_index): # Return list of units associated with a given page
  467. params = self.GetParametersByPageIndex(page_index)
  468. return [str(p.units) for p in params]
  469. def GetParameterHints(self, page_index): # Return a list of units associated with a given page
  470. params = self.GetParametersByPageIndex(page_index)
  471. return [str(p.hint) for p in params]
  472. def GetParameterDesignators(self, page_index): # Return a list of designators associated with a given page
  473. params = self.GetParametersByPageIndex(page_index)
  474. return [str(p.designator) for p in params]
  475. def SetParameterValues(self, page_index, list_of_values): # Update values on a given page
  476. params = self.GetParametersByPageIndex(page_index)
  477. for i, param in enumerate(params):
  478. if i >= len(list_of_values):
  479. break
  480. param.SetValue(list_of_values[i])
  481. def GetFootprint( self ):
  482. self.BuildFootprint()
  483. return self.module
  484. def BuildFootprint(self):
  485. return
  486. def GetBuildMessages( self ):
  487. return self.buildmessages
  488. def Show(self):
  489. text = "Footprint Wizard Name: {name}\n".format(name=self.GetName())
  490. text += "Footprint Wizard Description: {desc}\n".format(desc=self.GetDescription())
  491. n_pages = self.GetNumParameterPages()
  492. text += "Pages: {n}\n".format(n=n_pages)
  493. for i in range(n_pages):
  494. name = self.GetParameterPageName(i)
  495. params = self.GetParametersByPageName(name)
  496. text += "{name}\n".format(name=name)
  497. for j in range(len(params)):
  498. text += ("\t{param}{err}\n".format(
  499. param = str(params[j]),
  500. err = ' *' if len(params[j].error_list) > 0 else ''
  501. ))
  502. if self.AnyErrors():
  503. text += " * Errors exist for these parameters"
  504. return text
  505. class ActionPlugin(KiCadPlugin, object):
  506. def __init__( self ):
  507. KiCadPlugin.__init__( self )
  508. self.icon_file_name = ""
  509. self.dark_icon_file_name = ""
  510. self.show_toolbar_button = False
  511. self.defaults()
  512. def defaults( self ):
  513. self.name = "Undefined Action plugin"
  514. self.category = "Undefined"
  515. self.description = ""
  516. def GetName( self ):
  517. return self.name
  518. def GetCategoryName( self ):
  519. return self.category
  520. def GetDescription( self ):
  521. return self.description
  522. def GetShowToolbarButton( self ):
  523. return self.show_toolbar_button
  524. def GetIconFileName( self, dark ):
  525. if dark and self.dark_icon_file_name:
  526. return self.dark_icon_file_name
  527. else:
  528. return self.icon_file_name
  529. def Run(self):
  530. return
  531. }