Saturday, February 21, 2015

Solidworks Macros via Python

I finally figured out how to write Solidworks macros in python (yay!). Almost all the of Solidworks API works with one exception described at the end of this post. The Solidworks API is via the windows COM interface (ugh).

Here's the initial setup:

  1. Download and install Python. I used Active Python 2.7.8
  2. Get Solidworks. Python macros have worked pretty seamlessly across 2012, 2014 and 2015.
  3. Get familiar with the Solidworks online API help. E.g., http://help.solidworks.com/2014/english/api/sldworksapiprogguide/Welcome.htm is for the 2014 API. Note you can change the year in the URL to access the docs for other versions of Solidworks.
Ok, with that, we can dig right in. Before I run a macro, I make sure Solidworks is running and the document I want to modify is active (e.g., visible on the screen). I think you can use macros to start solidworks and open documents, but I'm less familiar with those commands.

Basic startup

This code snippet connects to running instance of Solidworks of the year specified. For example to connect to Solidworks 2015, set swYearLastDigit = 5:
import win32com.client
import pythoncom
swYearLastDigit = 5
sw = win32com.client.Dispatch("SldWorks.Application.%d" % (20+(swYearLastDigit-2)))  # e.g. 20 is SW2012,  23 is SW2015
You can also invoke Dispatch without the year specification, as in ....Dispatch("SldWorks.Application"). If there's only one version on your machine, this connects to that version.

At this point, the python code looks similar to the VBA code in the API docs. Sometimes you have to play with whether a function wants args or not. Here's the next piece of the boilerplate I have at the beginning of my scripts:

model = sw.ActiveDoc
modelExt = model.Extension
selMgr = model.SelectionManager
featureMgr = model.FeatureManager
sketchMgr = model.SketchManager
eqMgr = model.GetEquationMgr
As an example of difference in arguments, consider the Equation method on the IEquationMGR object (eqMgr in my code above). The 2014 API docs for the Equation member says that you read an equation by reading Equation(idx), and set by putting an equal sign after the expression. In python the binding is a bit different:
print("Equation 1 is: " + eqMgr.Equation(1))
eqMgr.Equation(1, "\"myVar\" = 42")
print("Equation 1 is now: " + eqMgr.Equation(1))
The most common difference I see between the Visual Basic docs and python are whether to put parenthesis after the member name or not. I just try both and see which works.

By the way, I see little rhyme or reason to the return values of method invocations, both at the API level as well as the values returned in practice. I usually go with the API docs, and assert return values, then delete the assertion if/when the method doesn't follow the API docs.

Creating arguments of the correct type (aka, getting SelectById2 to work)

Sometimes the method requires some fancy arguments, like reference arguments, or you otherwise just can't figure out what the thing is expecting. The Visual Basic interface is better at automatically converting types into the appropriate COM objects. The python bindings for the API don't work quite as well all the time. So here's what to do when you need to dig deeper and understand how to invoke a method:
  1. Generate the static python COM bindings for solidworks
    1. First, run python c:\Python27\Lib\site-packages\win32com\client\makepy.py
    2. Select "SldWorks 2015 Type Library" and hit OK. You'll see output like this:
      Generating to C:\Users\myhappyuser\AppData\Local\Temp\gen_py\2.7\83A33D31-27C5-11CE-BFD4-00400513BB57x0x23x0.py
      Building definitions from type library...
      Generating...
      Importing module
      
      The exact file name may change depending on your version of Solidworks.
  2. Open up that generated file in a viewer, like Komodo or Notepad
  3. Open up the web page VARIANT Type Constants in a browser
The generated python file has info on what arguments each method is expecting and that web page helps decode the arguments into something a bit more actionable.

Let's work a few common examples.

First let's try the macro command to select an object by name. The recommended version of the method is modelExt.SelectByID2. If you try putting in some actual args, you'll see:

c:\Users\happyuser\Documents\MeasuringCup>python
ActivePython 2.7.8.10 (ActiveState Software Inc.) based on
Python 2.7.8 (default, Jul  2 2014, 19:48:49) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import win32com.client
>>> sw = win32com.client.Dispatch("SldWorks.Application")
>>> model = sw.ActiveDoc
>>> modelExt = model.Extension
>>> modelExt.SelectByID2("mysketch", "SKETCH", 0, 0, 0, False, 0, None, 0)
Traceback (most recent call last):
  File "", line 1, in 
  File ">", line 2, in SelectByID2
pywintypes.com_error: (-2147352571, 'Type mismatch.', None, 8)
The last number in the Type mismatch. line indicates the argument that is causing the problem. In this case it is the None, which is the eighth argument. The API man page says it is expecting a "pointer to a callout". We just want to pass None, but the None isn't getting converted to the right COM object, so we have to do the conversion manually.

To do this, open the generated python file, for Solidworks 2015 it should be named 83A33D31-27C5-11CE-BFD4-00400513BB57x0x23x0.py, and search for 'SelectById2'. You should find a hit that looks like:

	def SelectByID2(self, Name=defaultNamedNotOptArg, Type=defaultNamedNotOptArg, X=defaultNamedNotOptArg, Y=defaultNamedNotOptArg
			, Z=defaultNamedNotOptArg, Append=defaultNamedNotOptArg, Mark=defaultNamedNotOptArg, Callout=defaultNamedNotOptArg, SelectOption=defaultNamedNotOptArg):
		'Select a specified entity'
		return self._oleobj_.InvokeTypes(68, LCID, 1, (11, 0), ((8, 1), (8, 1), (5, 1), (5, 1), (5, 1), (11, 1), (3, 1), (9, 1), (3, 1)),Name
			, Type, X, Y, Z, Append
			, Mark, Callout, SelectOption)
The list of tuples ( ((8,1), (8, 1), (5, 1), ...) above ) contains info on the expected type of each argument. The eighth tuple corresponds to the problematic eighth argument. That tuple is (9, 1). Now look up '9' in that MSDN web page titled "VARIANT Type Constants" and you'll see it matches with VT_DISPATCH. Here's the magic on how to generate the correct object manually:
    arg1 = win32com.client.VARIANT(pythoncom.VT_DISPATCH, None)
The first arg to VARIANT is the type of the object to create, and the second arg is the initial contents. So now we can use that and:
>>> arg1 = win32com.client.VARIANT(pythoncom.VT_DISPATCH, None)
>>> modelExt.SelectByID2("Sketch1", "SKETCH", 0, 0, 0, False, 0, arg1, 0)
True
Hooray!!!

Now let's try a different example. Continuing on, let's get a sketch we selected:

>>> selMgr = model.SelectionManager
>>> aSketch = selMgr.GetSelectedObject(1).GetSpecificFeature2
>>> aSketch.Name
u'Sketch1'
and let's get the plane it came from:
>>> aSketch.GetReferenceEntity(0)
Traceback (most recent call last):
  File "", line 1, in 
  File ">", line 2, in GetReferenceEntity
pywintypes.com_error: (-2147352571, 'Type mismatch.', None, 1)
Oops, that didn't work. Well looking into the generate python file and searching for GetReferenceEntity, we find:
	def GetReferenceEntity(self, LEntityType=defaultNamedNotOptArg):
		'Get entity that this sketch is created on'
		return self._ApplyTypes_(52, 1, (9, 0), ((16387, 3),), u'GetReferenceEntity', None,LEntityType
			)
And then looking at the VARIANT web page, we find that 16387 = pythoncom.VT_BYREF | pythoncom.VT_I4 So GetReferenceEntity uses an output argument to return the entity type. We can construct an output argument similar to what we did for SelectByID2:
    arg1 = win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, -1)
    refPlane = aSketch.GetReferenceEntity(arg1)
and now we can see:
>>> arg1 = win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, -1)
>>> arg1.value
-1
>>> refPlane = aSketch.GetReferenceEntity(arg1)
>>> arg1.value
4
To decode the '4', look at the doc for GetReferenceEntity, which points to the doc for an enumeration type swSelectType_e , which says that 4 maps to swSelDATUMPLANES

Constants

If you don't want to put '4' and other random constants in your python code, there are two possibilities. The first is to generate the python Solidworks COM constants bindings:
  1. Run python c:\Python27\Lib\site-packages\win32com\client\makepy.py
  2. Select "SOLIDWORKS 2015 Constant type library"
  3. Add an EnsureModule command early in your python program using the number shown in the output of the makepy command. For example, with Solidworks 2015, that is:
    swconst = win32com.client.gencache.EnsureModule('{4687F359-55D0-4CD3-B6CF-2EB42C11F989}', 0, 23, 0).constants # sw2015
    
Now, by looking at a man page you can find the appropriate constant name in that module, though there isn't much structure there. For example, you can check the return type of the GetReferenceEntity, above by doing:
    assert arg1.value == swconst.swSelDATUMPLANES
This isn't quite as nice as the Visual Basic interface, which structures the constants.

The downside of the EnsureModule approach is that it requires that anyone using your beautiful python morsel to run makepy. A more crude approach is to copy the constants you need from the man pages. For example, I have:

class swconst:
    swSelDATUMPLANES = 4
    .... more constants here ...

A prayer for GetMathUtility

And here is the one bit of the API that I can't get to work. For the life of me, I can't seem to get GetMathUtility to work, and so can not figure out how to create a MathPoint. What happens is:
>>> mathUtil = sw.GetMathUtility
>>> mathUtil.CreatePoint
Traceback (most recent call last):
  File "", line 1, in 
  File "C:\Python27\lib\site-packages\win32com\client\dynamic.py", line 511, in __getattr__
    ret = self._oleobj_.Invoke(retEntry.dispid,0,invoke_type,1)
pywintypes.com_error: (-2147417851, 'The server threw an exception.', None, None)
The output I expect is an error saying an argument is missing. I can not find any argument that placates this method, nor any other method in the mathUtil object returned. Kudos to anyone who can figure this out! It works fine in Visual Basic, so my guess is something is either screwed up in the python binding, or there's some kind of bug in the interface that the visual basic binding manages to avoid.

Tuesday, October 22, 2013

Sturdy shipping tube for art

Since my last post, I've moved to Cambridge, MA. The movers damaged two "ink on parchment" (thangkas) I had, and the only repair person I could find is in New York City (btw, it's Alan Farancz Painting Conservation Studio and they do great work). I couldn't find a decent crate to ship the pieces in, and I also couldn't find a larger diameter shipping tube (so the art wouldn't be rolled too tightly), so I made my own:

I made this:
  1. Start with two foot section of six inch pvc pipe
  2. Get a 6" pvc pipe cap, use hacksaw to reduce the depth a bit, just to save a bit of weight and make it less lopsided-feeling. Glue pipe cap on pipe with pvc cement.
  3. Cut three pieces of cardboard circles to form cap at other end. I used wood glue to stick them to each other offset by ~60 degrees so that they support each other.
On the inside, I added foam and used some stiff paper to fill the middle and provide structure so that thangkas didn't collapse on themselves. I rolled everything up using a few layers of thin bubble wrap. Total cost was << $50. The tube itself was $16. Hopefully this is helpful to anyone else who is trying to figure out how to make a sturdy shipping tube.

Thursday, September 27, 2012

Offline reference for javascript, jquery, jquery-ui and d3.js

For anyone who'se looking to do some javascript/jquery/d3 hacking on a plane without internet, here's the API references I've been able to dig up.

HTML/CSS/SVG

The official spec at www.w3.org is fine as a reference and the non-draft standards have a PDF option to download the spec. See generally http://www.w3.org/standards/webdesign/, and for HTML/CSS/SVG specifically the most recent ones as of this post seem to be:

Javascript

The best doc I could find is in CHM (windows help file format). You can read this file with xchm (e.g., sudo aptitude install xchm). The file itself is at: http://starcraft73.tripod.com/javascript/javascript.zip

Jquery and Jquery-UI

There's a single CHM file that includes docs for both jquery and jquery-ui at https://github.com/Yahasana/jqdoc-parser/blob/master/jQuery-UI-Reference-1.8.chm?raw=true There's a PDF version of just JQuery at https://bitbucket.org/greydwarf/jquerydoc/downloads/jquery_ref.pdf

D3.js

Getting D3 docs available offline requires a bit more work. Following roughly this guide, here are the steps:
  1. sudo aptitude install libxml2-dev libxslt-dev ruby1.8
  2. gem install gollum
  3. git clone https://github.com/mbostock/d3.wiki.git d3.wiki
  4. cd d3.wiki ; gollum
  5. Navigate your browser to http://localhost:4567/ or whatever port gollum starts up on (it prints it out when it starts up).

Monday, September 24, 2012

Repairing crashing Juniper VPN client on Ubuntu 12.04

In case anyone else is running into issues with their juniper VPN client on Ubuntu, here are two tricks that work for me. First, if it looks like the applet is crashing as it is establishing a connection, it could be because your /etc/resolv.conf is broken or missing. Try following: which details running sudo dpkg-reconfigure resolvconf to restore resolve.conf. There's probably a bug in juniper VPN client that makes it crash if resolv.conf is not to its liking. Another blunt thing I've sometimes had success with is good old rm -rf ~/.juniper_networks, which removes the applets and juniper config information and forces it to redownload it. Hope this helps!

Sunday, August 21, 2011

How to stream audio from mog.com on Ubuntu laptop to remote linux box

The problem: I recently got a linux set-top box that I have connected to my stereo in my living room. I use the internet music service mog.com and I'd like to be able to play it on my stereo and control it from my linux laptop.

I've tried lots of approaches that didn't work, like using vnc/nx/etc and setting up pulseaudio with module-tunnel-sink.

The way that seems to work the best for me is to first, setup pulseaudio on the settop box to install the module-native-protocol-tcp by adding the following line to /etc/pulse/default.pa:

load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;192.168.0.0/16

and restart pulseaudio with:

pulseaudio --kill ; pulseaudio --start

This tells pulseaudio to accept network connections from other machines on the local network.

Then, on my laptop, I run google-chrome as follows:

PULSE_SERVER=tcp:[settop box hostname] google-chrome --user-data-dir=/home/redstone/tmp/chrome-mog --app=http://mog.com

This starts a separate chrome session that will stream audio to the settop box. Note: you are free to simultaneously start google-chrome without the flags and start a google chrome sessions that will use your local laptops speakers as well. The reason for the user-data flags is that you can only specify the pulse server to use when chrome starts a new session, and if you don't start chrome with the user-data flags, it will just open a new window in your existing chrome session rather than start a new one.

You may want to run:
PULSE_SERVER=tcp:[settop box hostname] pavucontrol
to fiddle with the audio settings on the settop box.

I suspect that this approach should work with any other internet music streaming service like spotify/pandora/etc.

Saturday, August 20, 2011

Back and internal pictures of Niles Audio PS-1 Phono/Aux A-B Switcher

For those of you who are considering buying a Niles Audio PS-1 and are wondering what it looks like on the back and inside, I took a few pics. Here they are:







Subscribing to a twitter feed in Google reader

Google reader is a bit picky about the types of feed URLs you can paste into it to subscribe to feeds. I found that the following works:

http://api.twitter.com/1/statuses/user_timeline/[username].atom

where you substitute [username] for the actual usename. For example, to subscribe in google reader to Facebook's twitter feed (http://twitter.com/#!/facebook) you click on 'Add a subscription' in reader.google.com and paste in:

http://api.twitter.com/1/statuses/user_timeline/facebook.atom

There is more documentation on this API at https://dev.twitter.com/docs/api/1/get/statuses/user_timeline. It's listed at the bottom in the 'Extended description' section with the words 'not recommended' :). The 'recommended' API, using URL parameters, doesn't work in google reader. Further note: I initially tried getting RSS feed format to work and didn't have any luck.