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.

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