tag:blogger.com,1999:blog-33381287504321047132024-03-13T00:55:12.438-07:00Joshua Redstone's BlogJoshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.comBlogger42125tag:blogger.com,1999:blog-3338128750432104713.post-1550788756933992632020-11-23T10:29:00.001-08:002020-11-23T10:29:26.345-08:00How to calculate input impedance in LTSpice<p> This post is a follow on to my <a href="https://joshuaredstone.blogspot.com/2020/11/how-to-calculate-output-impedance-in.html">previous post on output impedance</a>. This time it is for calculating input impedance. Suppose we have a black box circuit that we will be sending a signal to. The black box could be Rx input pin of an RF transceiver IC, for example. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivH6hqKX34EoLxEbB62PSRGqInRkNyH0PMBZWnA3Yor1GtTjHXSjUPBYcvMMFnJuMAHZycqOjLrDHCO4PEYLdH5T-Jr5aplsapMRbbewkK9SD9_0Wezv0rCJPAkURWwS4XpiOcim8L18o/s715/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="447" data-original-width="715" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivH6hqKX34EoLxEbB62PSRGqInRkNyH0PMBZWnA3Yor1GtTjHXSjUPBYcvMMFnJuMAHZycqOjLrDHCO4PEYLdH5T-Jr5aplsapMRbbewkK9SD9_0Wezv0rCJPAkURWwS4XpiOcim8L18o/s320/Capture.PNG" width="320" /></a></div><div>We want to calculate Zin, the input impedance of the black box. In the case of input impedance, we'll add a 1V AC signal at the frequency of interest (915 MHz in this example), and measure the complex current that flows.</div><div><br /></div><div>Step 1 is to add the AC source and set up the simulation parameters:</div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBD1zXaeVjG7BQTq_lopPd9YIXGFjBdIZDy_pdlmIPf914UPlPqt9wdqrIE2OcR-tA6ZGllD1BtXJxo9bsexzV-TW-Ora4X9YON_2fJ6WuG-BnfMZZXy945gYFw26ggptg4qiJsTmiFCY/s935/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="596" data-original-width="935" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBD1zXaeVjG7BQTq_lopPd9YIXGFjBdIZDy_pdlmIPf914UPlPqt9wdqrIE2OcR-tA6ZGllD1BtXJxo9bsexzV-TW-Ora4X9YON_2fJ6WuG-BnfMZZXy945gYFw26ggptg4qiJsTmiFCY/s320/Capture.PNG" width="320" /></a></div><div><br /></div><div>To make the example concrete, we'll pretend the black box consists of an inductor and resistor.</div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXNVgY9deil8_FemCre1h2QZnVxjyOD3Xj9MO5-B8MyngS7eU9v_W4E8dfEF4M8c5Idq9gC_OMydCgJbq9IUPo_2qnodU8I62LwNOUntvGs1h9MuD6-bgNirbKvTMd6SENKksJXYMYpZg/s1088/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="605" data-original-width="1088" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXNVgY9deil8_FemCre1h2QZnVxjyOD3Xj9MO5-B8MyngS7eU9v_W4E8dfEF4M8c5Idq9gC_OMydCgJbq9IUPo_2qnodU8I62LwNOUntvGs1h9MuD6-bgNirbKvTMd6SENKksJXYMYpZg/s320/Capture.PNG" width="320" /></a></div><div><br /></div><div>Step 2 is run the simlation. It should output something like:</div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOHkjqLRJ7eUtOkmPRKsLBjmnHdxwg0w5NTC3l6bdkd05zmWNOxhyphenhyphenMvZI-4B0ZPssxQi3olYtHAa-iBOR_icnGic6TjyruLWRVKn_zDpzc4zMXX_zyOSkz5LzXPWqElu3HKyr3t6IQA3U/s862/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="244" data-original-width="862" height="182" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOHkjqLRJ7eUtOkmPRKsLBjmnHdxwg0w5NTC3l6bdkd05zmWNOxhyphenhyphenMvZI-4B0ZPssxQi3olYtHAa-iBOR_icnGic6TjyruLWRVKn_zDpzc4zMXX_zyOSkz5LzXPWqElu3HKyr3t6IQA3U/w640-h182/Capture.PNG" width="640" /></a></div><p><br />Now we convert I(V1) from mag/phase into a complex number using this formula:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLnBPpu_06pefHXvlG2jRsfMkSFkBoq7skgJoBQl7dphFxULlSjItCqG-aBoRWSQeZR348Gu4E6AK4FRLfmNwx87gJUD8wyJXzcXI_t5hfNwuRqgITukubixV2o0MoaboeIDe9g5zVC08/s1316/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="165" data-original-width="1316" height="80" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLnBPpu_06pefHXvlG2jRsfMkSFkBoq7skgJoBQl7dphFxULlSjItCqG-aBoRWSQeZR348Gu4E6AK4FRLfmNwx87gJUD8wyJXzcXI_t5hfNwuRqgITukubixV2o0MoaboeIDe9g5zVC08/w640-h80/Capture.PNG" width="640" /></a></div><div><br /></div><div>Plugging in magnitude of 0.0189064 and phase of 160.969 we get 0.01787 + 0.006165j. </div><div><br /></div><div>[ hand-waving ] I think because we're measuring current through the voltage source, we have to take the complex conjugate, so 0.01787 - 0.006165j. Though I'm not totally sure that's right. [ /hand-waving ]</div><div><br /></div><div>Now we know the voltage and current, so using the complex form of V=IR, we can solve for the impedance:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeYNEdynP-25uUo8ka1OiHvI1UVmBrdnKETg74QUsbKwW6BPagtBhrihpimRvh0Uzk4Im_7vP_kbv-r48eRFgcrDH14qKrANdKav8h75iBhR2Bhp_bIQ6CaL7kJyzfvRwVnJMREhxWYIE/s888/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="126" data-original-width="888" height="56" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgeYNEdynP-25uUo8ka1OiHvI1UVmBrdnKETg74QUsbKwW6BPagtBhrihpimRvh0Uzk4Im_7vP_kbv-r48eRFgcrDH14qKrANdKav8h75iBhR2Bhp_bIQ6CaL7kJyzfvRwVnJMREhxWYIE/w400-h56/Capture.PNG" width="400" /></a></div><div><br /></div><div>As we can see, the 50 matches the resistance, and we can convert the 17.25j, since the only reactive component is the inductor, using this equation:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOGu1zF9nDkYd5Upr-RLxokK-mGC5CCyR53gzmjzCwXWYmd-e93n3lk4Jqh9okvlLlrRCaTrhD9GHaJFumMRI_Auxcv2HAHh0TnfyARaH7524ztshSKGeKwzDbpViSnf5FNIzufAF-jUY/s627/Capture.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="113" data-original-width="627" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOGu1zF9nDkYd5Upr-RLxokK-mGC5CCyR53gzmjzCwXWYmd-e93n3lk4Jqh9okvlLlrRCaTrhD9GHaJFumMRI_Auxcv2HAHh0TnfyARaH7524ztshSKGeKwzDbpViSnf5FNIzufAF-jUY/s320/Capture.PNG" width="320" /></a></div><div><br /></div>Which matches as well. Voila!<br /><div><br /></div><div><br /></div><br /><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-39211145663456633392020-11-21T16:51:00.006-08:002020-11-21T16:58:02.401-08:00How to calculate output impedance in LTSpiceNo idea why this isn't readily available on the web, but here it is. Caveat: I'm not an RF engineer, so I might be missing something basic.<div><br /></div>
<div>Suppose you have a circuit in LTSpice such as follows:
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5efyNryaBUZaO4rK73FMo4UINfdYlvIPttR6shE180yv-tdLSTJOWtiJngVVEvJDW7AENuPHkCiruCnQ9z8V1wztUS1u0_Op00XlvvuJHqjQGvRKc4ksm23Hyz2edj74r4fILRZxpOz8/s703/Capture.PNG" style="display: block; padding: 1em 0px; text-align: center;"><img alt="" border="0" data-original-height="344" data-original-width="703" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5efyNryaBUZaO4rK73FMo4UINfdYlvIPttR6shE180yv-tdLSTJOWtiJngVVEvJDW7AENuPHkCiruCnQ9z8V1wztUS1u0_Op00XlvvuJHqjQGvRKc4ksm23Hyz2edj74r4fILRZxpOz8/s320/Capture.PNG" width="320" /></a></div></div>
<div>So an AC signal is generated, passes through some black box, and you want to know the output impedance at Vout. That is, you want to know the impedance of the black box. For example, it could be that the black box is part of the RF transmit circuit and you want to know the impedance so you know what matching network to build to transform it into 50 ohms.</div><div><br /></div><div>Step 1: Add an AC simulation directive at the frequency of interest, in this case 915 MHz.</div><div>Step 2: Add a resistor to ground at Vout. The resistor value can be anything, but to avoid precision errors, probably best to put it roughly in same ballpark as anticipated real part of the impedance. Shown below:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgl4NsSkfwZHbRA6z5lsunyOrbd7oChvUnc6cYZaKDQOcH8f5KhSwTym139c27aJA4fW-ACXKdIDGqVB0A4jBqjPmDEvipkj8tqvQossLh4srvMaavTGlofZJqpPzQlqGVUOoW5zOshgV8/s850/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="458" data-original-width="850" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgl4NsSkfwZHbRA6z5lsunyOrbd7oChvUnc6cYZaKDQOcH8f5KhSwTym139c27aJA4fW-ACXKdIDGqVB0A4jBqjPmDEvipkj8tqvQossLh4srvMaavTGlofZJqpPzQlqGVUOoW5zOshgV8/s320/Capture.PNG" width="320" /></a></div><div><br /></div><div>Step 3: Simulate. To make the example concrete, we'll use a simple circuit in place of the black box, of a resistor and inductor, though in practice the circuit could of course be much more complex:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3pv6kfZqOlqwcvJrDza0S5pC9Ue7I43sJemVla5g3dh1CDHzZh5wqMVJ_h-pPtAR7nu-ZJAZ6Dl5skZ1hjVwXRsg8T8n5AZmiVG1B0V9Cu6z6GIlZi2_EdyHbWVAKwa8dyEQ3V86be9Y/s864/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="459" data-original-width="864" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3pv6kfZqOlqwcvJrDza0S5pC9Ue7I43sJemVla5g3dh1CDHzZh5wqMVJ_h-pPtAR7nu-ZJAZ6Dl5skZ1hjVwXRsg8T8n5AZmiVG1B0V9Cu6z6GIlZi2_EdyHbWVAKwa8dyEQ3V86be9Y/s320/Capture.PNG" width="320" /></a></div><br /><div>And the simulation results will look like:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFksYZA-tilyZ2ZmXvsXuVZ6F-O1Im3gL0AogRyPzsZSiAgIbTBjOYwhB2v1pP-9rPwRsyS5R3QQVJYdRXh7gUcIzbkQLRBkGqFieK8L5-ProbBPmIzAhtExiE77Zk6_o0wDSzHodI0mE/s857/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="240" data-original-width="857" height="181" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFksYZA-tilyZ2ZmXvsXuVZ6F-O1Im3gL0AogRyPzsZSiAgIbTBjOYwhB2v1pP-9rPwRsyS5R3QQVJYdRXh7gUcIzbkQLRBkGqFieK8L5-ProbBPmIzAhtExiE77Zk6_o0wDSzHodI0mE/w640-h181/Capture.PNG" width="640" /></a></div><br /><div>Now comes the fun part. First, we'll convert the current through our test resistor I(R1) from magnitude/phase into a complex quantity using the equation:</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_6DasV1bftkaW5rmK6VASe2AZyqVM9Z3WYFSTiXnc0ennQ3mCqyid_Fy9YPUkD82TU73lmOTys-_0pVlbV-0jdMwD_ykvthsWTn_2DIjBok3FufSnPblJXPsuoxkw84Ain7wo4IVYEnI/s1364/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="162" data-original-width="1364" height="76" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_6DasV1bftkaW5rmK6VASe2AZyqVM9Z3WYFSTiXnc0ennQ3mCqyid_Fy9YPUkD82TU73lmOTys-_0pVlbV-0jdMwD_ykvthsWTn_2DIjBok3FufSnPblJXPsuoxkw84Ain7wo4IVYEnI/w640-h76/Capture.PNG" width="640" /></a></div><div><br /></div>In our example, plugging in 0.00514023 and -62.4433 produces I = 0.00237 - j 0.004557.<div>Current is the same thru the circuit so we can use Z = E / I (the complex form of R = V / I ) to find the total impedance of the circuit:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTNjakm9AsMYz0EWT0D5T4t4AAdtnCWX5MgWYgp7_qYgHhGMRuUi0N4zoPh7cYgY1w6MKp3WZ9QMHVHb4BIb6L5PdaHPNDzb0wn6dcgbGV-ehsc9aDHt6w6AcrhIi_qGtABEDFRQEKJZc/s896/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="117" data-original-width="896" height="84" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiTNjakm9AsMYz0EWT0D5T4t4AAdtnCWX5MgWYgp7_qYgHhGMRuUi0N4zoPh7cYgY1w6MKp3WZ9QMHVHb4BIb6L5PdaHPNDzb0wn6dcgbGV-ehsc9aDHt6w6AcrhIi_qGtABEDFRQEKJZc/w640-h84/Capture.PNG" width="640" /></a></div><div><br /></div>And then we subtract off our fake load of impedance 40 + 0j to get:<div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPpbS6Y_miIPaDetDN3niHoSu9UrsD7ZDvoFctegSGMP2hXQypEuVnZ_EVp6ino9GgycxWuW56s69AgaVI_-_PmpF-0ajLXCZOu0VYEo1EeDi3e-WU0Ymlsb-gAlBMhHFO4yBzh2spqCE/s401/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="70" data-original-width="401" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgPpbS6Y_miIPaDetDN3niHoSu9UrsD7ZDvoFctegSGMP2hXQypEuVnZ_EVp6ino9GgycxWuW56s69AgaVI_-_PmpF-0ajLXCZOu0VYEo1EeDi3e-WU0Ymlsb-gAlBMhHFO4yBzh2spqCE/s320/Capture.PNG" width="320" /></a></div><br /><div>And in this particular case, since we know Zout consists of a resistor plus inductor, we can verify that the real part of Zout matches the resistor R2 (both 50 ohms) and we can use the imaginary part to verify the inductor value:</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdmtZfeUGby1xPkYoEu2_9jr6RdKieDEbLx0adLjptqnqiNShKNNO6BRH5ZkTU5AndxA_C2bvRBtMVMUel9MJCVDMEN9yZ-FYBffYZS1LNkIfYMxJkgT-Vu6MUinJL0MPlbo0oCnRIswY/s605/Capture.PNG" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="107" data-original-width="605" height="71" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdmtZfeUGby1xPkYoEu2_9jr6RdKieDEbLx0adLjptqnqiNShKNNO6BRH5ZkTU5AndxA_C2bvRBtMVMUel9MJCVDMEN9yZ-FYBffYZS1LNkIfYMxJkgT-Vu6MUinJL0MPlbo0oCnRIswY/w400-h71/Capture.PNG" width="400" /></a></div><div><br /></div>Voila!<br /><div><br /><div><br /></div><div><br /></div><div><br /></div><div><br /></div><div><br /><div><br /></div><div><br /></div><div><br /></div><div><br /></div><div><br /></div><div><br /></div><div><br /></div><div><br /></div>
</div></div>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-37239322966690939672019-03-19T06:19:00.001-07:002019-03-19T06:19:15.416-07:00Convert Google Inbox reminders to google calendar eventsI used reminders extensively in Inbox. Inbox is going away (end of March 2019). Google calendar has a notion of a reminder, but you can't get email notifications of those, so there's no way to keep track of which reminders you haven't done yet. However, calendar events do have email notifications you can enable. That seems close enough. Below is the python code I used to transfer over my reminders, both current and future, to calendar events or email.
<p>The code below has some extra info on the undocumented Google api for searching and deleting reminders as well. Note you need to get an API key before using this code (see below) </p>
<pre class="jcodeblock">
#!/usr/bin/python3 -t
import re
import argparse
import json
import os
import readline # to enable navigating through entered text
import time
import copy
from typing import Tuple
#from email import encoders
import base64
from email.mime.text import MIMEText
import httplib2
from oauth2client import tools
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.file import Storage
import datetime #from datetime import datetime, timezone
import dateutil.tz
USER_OAUTH_DATA_FILE = os.path.expanduser('tmp/google-reminders-cli-oauth')
def authenticate() -> httplib2.Http:
# You need a Google project and credentials. Check out
# https://console.cloud.google.com/apis/credentials
app_keys = {
"APP_CLIENT_ID": blah,
"APP_CLIENT_SECRET": moreblah
}
storage = Storage(USER_OAUTH_DATA_FILE)
credentials = storage.get()
if credentials is None or credentials.invalid:
credentials = tools.run_flow(
OAuth2WebServerFlow(
client_id=app_keys['APP_CLIENT_ID'],
client_secret=app_keys['APP_CLIENT_SECRET'],
scope=['https://www.googleapis.com/auth/reminders',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/calendar.events',
'https://www.googleapis.com/auth/gmail.insert',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/gmail.labels',
],
user_agent='google reminders cli tool'),
storage,
)
auth_http = credentials.authorize(httplib2.Http())
return auth_http
calendarVersion = "WRP / /WebCalendar/calendar_190310.14_p4"
reminderHeaders = {
'content-type': 'application/json+protobuf',
}
def printReminder(auth_http, aReminderId):
request = {"1":{"4":calendarVersion},"2":[{"2":aReminderId}]}
response, content = auth_http.request(
uri='https://reminders-pa.clients6.google.com/v1internalOP/reminders/get',
method='POST',
body=json.dumps(request),
headers=reminderHeaders,
)
assert(response.status == 200)
obj = json.loads(content)
print(obj)
def main():
auth_http = authenticate()
# https://developers.google.com/oauthplayground/ for figuring out scopes and apis using the scopes
response, content = auth_http.request(
uri='https://www.googleapis.com/userinfo/v2/me',
method='GET',
headers={'content-type': 'application/json'},
)
assert response.status == 200, (response, content)
obj = json.loads(content)
email = obj['email']
print('Got email', email)
jreminderLabelId = None
if not jreminderLabelId:
response, content = auth_http.request(
uri='https://www.googleapis.com/gmail/v1/users/me/labels',
method='GET',
headers={}, #'content-type': 'application/json'},
)
assert(response.status == 200)
obj = json.loads(content)
labels = obj["labels"]
jreminderLabelId = None
for labelInfo in labels:
if labelInfo["name"] == "jreminder":
jreminderLabelId = labelInfo["id"]
break
assert jreminderLabelId
print('Got jreminder label id', jreminderLabelId)
reminderCalendarId = None
if not reminderCalendarId:
response, content = auth_http.request(
uri='https://www.googleapis.com/calendar/v3/users/me/calendarList',
method='GET',
headers={}, #'content-type': 'application/json'},
)
assert(response.status == 200)
obj = json.loads(content)
calendars = obj["items"]
for aCal in calendars:
#print(aCal["id"], aCal["summary"])
if aCal["summary"] == "jReminders":
reminderCalendarId = aCal["id"]
break
assert reminderCalendarId
print('Got jReminders calendar id', reminderCalendarId)
baseListRequest = {
"1": { "4": calendarVersion},
"2": [{"1":3},{"1":16},{"1":1},{"1":8},{"1":11},{"1":5},{"1":6},{"1":13},{"1":4},{"1":12},{"1":7},{"1":17}],
# 3: due_before_ms
# 4: due_after_ms
"5": 0, # include_archived Reminder status (0: Incomplete only / 1: including completion)
"6": 20, # max_results limit
# 7: continuation_token
# 9: include_completed
# 10: include_deleted
# 12: recurrence_id
# 13: recurrence_options
# 14: continuation
# 15: exclude_due_before_ms
# 16: exclude_due_after_ms
# 18: require_snoozed
# 19: require_included_in_bigtop
# 20: include_email_reminders
# 21: project_id
# 22: require_excluded_in_bigtop
# 23: raw_query
#"24" archived_before_ms
#"25" archived_after_ms
# 26: exclude_archived_before_ms
# 27: exclude_archived_after_ms
# 28: utc_due_before_ms
# 29: utc_due_after_ms
# 30: exclude_utc_due_before_ms
# 31: exclude_utc_due_after_ms
# 32: skip_storage_read_parameters
}
#print(content)
# "1"."2": reinder id
# 2: maybe what app created it?
# 3: reminder text
# yr month day 24hr min sec? unix ms
# 5: reminder snooze expired time : {"1":2019,"2":3,"3":16,"4":{"1":14,"2":6,"3":0},"7":"1552759560000"}
# 8: done=1 ?
# 10: 1 (deleted?)
# 11: done time?
# 13: ?
# 16: something about repeat frequency?
# 17: extra bonus info?
# 18: creation ms
# 23: time ms right before creation
# 26: last modified?
print('Getting past reminders')
pastListRequest = copy.deepcopy(baseListRequest)
pastListRequest["16"] = str(int(time.time()) + 0 * 24 * 3600) + "000" # time msec
response, content = auth_http.request(
uri='https://reminders-pa.clients6.google.com/v1internalOP/reminders/list',
method='POST',
body=json.dumps(pastListRequest),
headers=reminderHeaders,
)
assert response.status == 200, (response, content)
obj = json.loads(content)
reminders = obj["1"] if "1" in obj else []
for aReminder in reminders:
aReminderId = aReminder["1"]["2"]
print(time.strftime('%Y-%m-%d', time.localtime(int(aReminder["18"])/1000)), aReminderId, aReminder["3"])
print(aReminder)
# Reminder is not marked done
assert "8" not in aReminder
assert "11" not in aReminder
#assert aReminder["2"]["1"] == 1 # reminder is created in inbox. Otherwise, unsure if delete will work
isRepeat = "16" in aReminder
if '5' in aReminder:
rTime = aReminder["5"]["7"]
assert rTime and len(rTime) > 6
assert re.match(r'^[0-9]+$', rTime)
rTime = int(rTime) / 1000
isPast = time.time() > rTime
else:
rTime = int(aReminder['18']) / 1000
assert rTime > 1000000
isPast = time.time() > rTime
assert isPast
x = input('return to email ["n" to skip]')
if x != 'n':
message = MIMEText(aReminder["3"])
message['To'] = email
message['From'] = email
message['Subject'] = aReminder["3"]
#print (message.as_string())
data = {'raw': base64.urlsafe_b64encode(bytearray(message.as_string(), 'utf-8')).decode('utf-8')}
#print (data)
data["labelIds"] = [ "UNREAD", "INBOX", jreminderLabelId ]
response, content = auth_http.request(
#uri='https://www.googleapis.com/gmail/v1/users/me/messages/send',
uri='https://content.googleapis.com/gmail/v1/users/me/messages?alt=json',
method='POST',
body=json.dumps(data),
headers={'content-type': 'application/json'},
)
assert response.status == 200, (response, content)
x = input('return to delete ["n" to skip]')
if x != 'n':
deleteRequest = {"1":{"4": calendarVersion},
"2":[{"2": aReminderId}]}
response, content = auth_http.request(
uri='https://reminders-pa.clients6.google.com/v1internalOP/reminders/delete',
method='POST',
body=json.dumps(deleteRequest),
headers=reminderHeaders,
)
assert response.status == 200, (response, content)
printReminder(auth_http, aReminderId)
print('Getting future reminders')
futureListRequest = copy.deepcopy(baseListRequest)
#futureListRequest["15"] = str(int(time.time()) + 3 * 365 * 24 * 3600) + "000" # time msec
futureListRequest["16"] = str(int(time.time()) + 30 * 365 * 24 * 3600) + "000" # time msec
response, content = auth_http.request(
uri='https://reminders-pa.clients6.google.com/v1internalOP/reminders/list',
method='POST',
body=json.dumps(futureListRequest),
headers=reminderHeaders,
)
assert response.status == 200, (response, content)
obj = json.loads(content)
reminders = obj["1"] if "1" in obj else []
#print(obj)
for aReminder in reminders:
aReminderId = aReminder["1"]["2"]
print(time.strftime('%Y-%m-%d', time.localtime(int(aReminder["18"])/1000)), aReminderId, aReminder["3"])
print(aReminder)
# Reminder is not marked done
assert "8" not in aReminder
assert "11" not in aReminder
#assert aReminder["2"]["1"] == 1 # reminder is created in inbox. Otherwise, unsure if delete will work
isRepeat = "16" in aReminder
if '7' in aReminder['5']:
rTime = aReminder["5"]["7"]
assert rTime and len(rTime) > 6
assert re.match(r'^[0-9]+$', rTime)
rTimeSecs = int(rTime) / 1000
isPast = time.time() > rTimeSecs
assert not isPast
eTime = datetime.datetime.fromtimestamp(rTimeSecs, dateutil.tz.gettz('America/New_York'))
else:
print('Warning, missing unix timestamp, double check date')
eTime = datetime.datetime(aReminder['5']['1'], aReminder['5']['2'], aReminder['5']['3'],
aReminder['5']['4']['1'], aReminder['5']['4']['2'], aReminder['5']['4']['3'],
tzinfo = dateutil.tz.gettz('America/New_York'))
#nowT = datetime.now().astimezone().isoformat(timespec='seconds')
request = {
'start' : {
'dateTime': (eTime).isoformat(timespec='seconds'),
'timeZone': 'America/New_York',
},
'end' : {
'dateTime': (eTime + datetime.timedelta(minutes=15)).isoformat(timespec='seconds'),
'timeZone': 'America/New_York',
},
'description': aReminder["3"], # detailed description
'summary': aReminder["3"], # top line name of event
'reminders': {
'overrides': [ { 'method': 'email', 'minutes': 5 } ],
'useDefault': False
}
}
if isRepeat:
ruleTxt = 'RRULE:'
repeatInfo = aReminder['16']['1']
assert aReminder['5']['4']['1'] == repeatInfo['5']['1']['1']
assert aReminder['5']['4']['2'] == repeatInfo['5']['1']['2']
assert aReminder['5']['4']['3'] == repeatInfo['5']['1']['3']
# time zone issue maybe
#assert eTime.hour == repeatInfo['5']['1']['1'], (eTime.hour, repeatInfo['5']['1']['1'])
assert eTime.minute == repeatInfo['5']['1']['2']
assert eTime.second == repeatInfo['5']['1']['3']
if repeatInfo['1'] == 3:
ruleTxt += 'FREQ=YEARLY'
elif repeatInfo['1'] == 2:
ruleTxt += 'FREQ=MONTHLY'
elif repeatInfo['1'] == 1:
ruleTxt += 'FREQ=WEEKLY'
elif repeatInfo['1'] == 0:
ruleTxt += 'FREQ=DAILY'
else:
assert False
if '2' in repeatInfo:
ruleTxt += ';INTERVAL=' + str(repeatInfo['2'])
if '7' in repeatInfo:
assert repeatInfo['1'] == 2 # monthly
day = repeatInfo['7']['1'][0]
assert day > 0 and day <= 28
ruleTxt += ';BYMONTHDAY=' + str(day)
if '6' in repeatInfo:
assert repeatInfo['1'] == 1 # weekly
weekday = repeatInfo['6']['1'][0]
assert weekday > 0 and weekday < 7
ruleTxt += ';WKST=MO;BYDAY=' + ['MO','TU','WE','TH','FR','SA','SU'][weekday - 1]
if '8' in repeatInfo:
assert repeatInfo['1'] == 3 # yearly
request['recurrence'] = [ ruleTxt ] # [ 'RRULE:FREQ=DAILY;INTERVAL=3' ]
#nowT = datetime.datetime.now()
#nowT = datetime.datetime(nowT.year, nowT.month, nowT.day, 9, 0, 0)
print(request)
x = input('Create calendar event ["n" to skip]')
if x != 'n':
response, content = auth_http.request(
uri='https://www.googleapis.com/calendar/v3/calendars/' + reminderCalendarId + '/events',
method='POST',
body=json.dumps(request),
headers={'content-type': 'application/json'},
)
assert response.status == 200, (response, content)
print('Created calendar event: ', json.loads(content)['htmlLink'])
if isRepeat:
mm = re.match(r'^([^/]+)/([0-9]+)$', aReminderId)
assert mm
aReminderId = mm.group(1)
subId = aReminder['5']['7']
assert len(subId) > 6
delUrl = 'https://reminders-pa.clients6.google.com/v1internalOP/reminders/recurrence/delete'
deleteRequest = {"1":{"4": calendarVersion},
"2":{"1": aReminderId},
"4":{ "1":1, "2": 0, "3": subId }
}
else:
delUrl = 'https://reminders-pa.clients6.google.com/v1internalOP/reminders/delete'
deleteRequest = {"1":{"4": calendarVersion},
"2":[{"2": aReminderId}]}
print(deleteRequest)
#print(json.dumps(deleteRequest))
x = input('Delete future reminder')
response, content = auth_http.request(
uri=delUrl,
method='POST',
body=json.dumps(deleteRequest),
headers=reminderHeaders,
)
assert response.status == 200, (response, content)
if __name__ == '__main__':
main()
</pre>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-58946079751237588752018-05-30T11:08:00.001-07:002018-05-30T11:24:46.919-07:00IP to country mapping in under 1MB (node js)Hi all,<br/>
I recently wrote some node js code to do IP to country code mapping that uses less than 1MB of RAM for data storage. It also uses the <a href="https://www.arin.net/">ARIN registry</a> data directly and so doesn't require attribution, unlike some of the commercial sources out there. Because it takes up little memory, it's feasible to keep the table in-memory on my tiny web server, which means I avoid any RPC costs associated with IP lookup.
<p>The gist is to first pull a copy of the ARIN IP registry data:
<pre class="jcodeblock">wget ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest
wget ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest
wget ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest
wget ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest
wget ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest
</pre>
<p>Then do a bit of filtering and sorting:
<pre class="jcodeblock">cat delegated-* | grep '|ipv4' | sort -t '|' -k 4,4 -k 5 -V > ./comb3
cat delegated-* | grep '|ipv6' | sort -t '|' -k 4,4 -k 5 -V > ./comb3.ipv6
</pre>
<p>Then build the memory structures and serialize them to disk. I handle IPv4 and IPv6 differently. Both use the raw Uint8Array type.
<h3>IPv4</h3>
The ARIN data specifies IP address ranges as an IP followed by a length, and the length is not necessarily a power of two. Since IP address are pretty densely allocated, the datastructure I use is a 65kB array mapping the first two numbers in an IP address to a run-length-coded list of (range + countryCode) blocks and gaps between ranges. Countrycode takes up one byte and the range or gap length is one byte (storing log of the length), plus a bunch of special cases for non-power-of-two lengths and so forth.
<h3>IPv6</h3>
ARIN specifies IPv6 ranges in CIDR notation, so it's a prefix followed by the number of significant bits in the prefix. Because of that and because the IPv6 range is very sparse, I use a simple trie with one level for each IP group (hextet, e.g. "2a0a"). Each level is a hash table with a 2-byte key (for the hextet) and a 3-byte value. The value is either one byte of CIDR routing prefix (e.g. the '20' in '/20') followed by two bytes of country code, or it is 3-bytes of offset indicating the location of the next level down in the trie. There's enough extra space in there to have a special bit to indicate an empty entry.
<p>Because this data structure is read-only in production use, each hash table is resized to something like 1.3 times the number of entries.
<h3>Space usage and performacne</h3>
As of May 30, 2018, the ipv4 table is 634kB and the ipv6 table is 302kB.
<p>With some dumb benchmarking, it looks like lookup is about 30us for either of ipv4 or ipv6. I will note that there are certain ipv4 blocks where the linked list gets long. I think I noted a max length of 512 entries for one, so user beware. One could probably add some skip-list-like indexes to the longer lists to limit the worst-case lookup time, or better yet, use the fact that most of the time ranges are power-of-two length so probably a list is not the best representation.
<p>The node source code for all this is about 600 lines. I could probably open-source it if there's enough interest.
Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-81129927743484838962018-05-09T06:26:00.001-07:002018-05-13T08:55:47.564-07:00Testing old Safari browser (6.1.6) on current Linux / Ubuntu 18.04I run Ubuntu Bionic 18.04, and I recently needed to test my website on Safari 6.1.6, released back in 2014. The closest I got was by installing an old version of the <a href="https://wiki.gnome.org/Apps/Web">Ephiphany/Web browser</a>, since both Epiphany and Safari are based on the <a href="https://webkit.org/">Webkit browser engine</a>. The steps I followed, after much trial and error were:<br />
<ol>
<li>Find release date of desired Safari version looking at the <a href="https://en.wikipedia.org/wiki/Safari_version_history">wiki page</a>. Safari 6.1.6 was released around August, 2014.</li>
<li>Find a version of the epiphany-browser package from a similar date by browsing the <a href="http://snapshot.debian.org/package/epiphany-browser/">debian package history</a> If you click on the links there it'll show the date the package appeared. epiphany-browser 3.12.1-1 is from around July, 2014.</li>
<li>Look in the <a href="https://packages.ubuntu.com/search?keywords=epiphany-browser">Ubuntu history of the epiphany package</a> to see if there's an Ubuntu release that has a version close to what you want. I'm currently running Bionic, and saw the Trusty has a close version, 3.10.3-0ubuntu2.</li>
<li>Add the old Trusty repository to your /etc/apt/sources.list by adding the following lines:
<br /><pre class="jcodeblock">deb http://us.archive.ubuntu.com/ubuntu trusty universe
deb http://us.archive.ubuntu.com/ubuntu trusty multiverse
deb http://security.ubuntu.com/ubuntu trusty-security main
deb http://cz.archive.ubuntu.com/ubuntu trusty main
</pre>
</li>
<li>The run some apt command to update and get the correct version. I found the right packages to include by trial and error as the apt-get install command would fail:
<br /><pre class="jcodeblock">apt-get remove epiphany-browser epiphany-browser-data
apt-get update
sudo apt-get install epiphany-browser=3.10.3-0ubuntu2 epiphany-browser-data=3.10.3-0ubuntu2 libwebkit2gtk-3.0-25 libjavascriptcoregtk-3.0-0=2.4.10-0ubuntu0.14.04.1
sudo apt-mark hold epiphany-browser epiphany-browser-data
</pre></li>
</ol>
And voila! epiphany-browser was available for testing / debugging.<br />
My guess is that it's not a good idea to leave the trusty source lines in sources.list long-term since it might confuse future upgrades and may also confuse apt-cache. I plan to remove and try to cleanup once I'm done testing.
Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-4313796459073713462017-09-29T11:51:00.002-07:002017-10-04T08:06:45.945-07:00The optimal shape of a kitchen measuring cupFour days ago I launched <a href="http://bit.ly/2fvYJCze">Euclid, more accurate measuring cup</a> on Kickstarter, and it just crossed 100% of it’s fund-raising goal, which is amazing. The future has changed from *if* this idea ever becomes a real thing to “It’s going to be real!”<br />
<br />
Euclid’s shape is computationally generated, based on math resulting from a neat geometry insight. I quit my software engineering job four years ago to create it, and though it emerged from custom software, the cup itself is not digital in any way. No bluetooth, app or touchscreen. The Kickstarter page has a summary of the idea. Here I wanted to add a bit more detail about the project and how it came about.<br />
<br />
My background is as a infrastructure software engineer. I was at Google and Facebook for 10 years building a number of distributed systems including <a href="https://en.wikipedia.org/wiki/Google_File_System">GFS</a>, <a href="https://users.soe.ucsc.edu/~niejiazhong/slides/chandra.pdf">Sibyl</a> and <a href="https://research.fb.com/publications/holistic-configuration-management-at-facebook/">Configerator</a>.<br />
<br />
I also love math and tinkering with things. Growing up, I spent a lot of (mostly voluntary) time in the basement, screwing around doing dumb stuff. What ever anyone says, it is a terrible idea to pull the spark plug wire off a running lawn mower with your bare hands.<br />
<br />
One day I was baking in the kitchen and had a 2-cup measuring cup out and the recipe called for 1/4-cup. I felt like I should switch to a smaller measuring cup and for whatever reason stopped to wonder why. Why do smaller measuring cups seem better at measuring small amounts than larger measuring cups?<br />
<br />
I realized it was about accuracy and that clearly defining measurement error would help in reasoning this through. Brace yourself, I’m going to get a bit technical, because understanding the problem was the key to solving it.<br />
<div>
<br />
<h2>
The problem</h2>
Let’s consider measurement error as the vertical distance between the target measurement line and the true height of the liquid. For example, when measuring 1-cup, you may think you hit the target line, but actually you overshot by 1mm because the measuring cup was not quite at eye level. Other possible reasons for missing the line include liquid sloshing, hand shaking, and the coriolis force (ok I’m kidding about that last one… I think).<br />
<br />
The graphic below shows an example of overshooting by 1mm:<br />
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi97qH1IYfHj9f90g3wUkjgYXFYlWaJVP3RfcpgzZh83N5-_M1cSIG0IICMwxOl9xPGqyGIrqLSPhtnyjpIu8F1wczAd3WuHAXlCt8DypstaGHyqx0DTb5AiCnUs5JXlcBOqXHVq7QZBaY/s1600/1mmslice3-hirez.png"><img border="0" height="116" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi97qH1IYfHj9f90g3wUkjgYXFYlWaJVP3RfcpgzZh83N5-_M1cSIG0IICMwxOl9xPGqyGIrqLSPhtnyjpIu8F1wczAd3WuHAXlCt8DypstaGHyqx0DTb5AiCnUs5JXlcBOqXHVq7QZBaY/s640/1mmslice3-hirez.png" width="640" /></a><br />
<br />
The amount of extra liquid in the cup equals 1mm times the surface area of the liquid. Divide that by the target volume, and you have your fractional error (e.g., 5%).<br />
<br />
If we assume that over-shooting (or under-shooting) happens just as easily at the top of a measuring cup as at the bottom, then that means the extra liquid height (1mm in the graphic) will be roughly the same at the top and the bottom.<br />
<br />
Looking at the equation in the graphic, this means that <b>measurement error depends primarily on the ratio of surface area to volume at the target line</b>. Let’s call that the S/V ratio.<br />
<br />
Here are a few interesting implications that may correspond to your intuition:<br />
<br />
<ul>
<li>Narrow measuring cups are more accurate than wide measuring cups.</li>
<li>Small-capacity measuring cups are better at measuring small volumes because they are narrower. This answers the conundrum that started this whole quest.</li>
<li>Cylindrical measuring cups measure smaller amounts less accurately than larger amounts. Why? Because the S/V ratio is larger at smaller amounts. This is why it’s hard to measure ¼-cup accurately in a 2-cup measuring cup. It’s also why larger measuring cups don’t have lines for ⅛-cup or 1-tablespoon or 1-teaspoon.</li>
<li>Many measuring cups have sloped sides or are conical. All of these shapes have the same problem. The S/V ratio increases for smaller amounts, though not as quickly as in a cylindrical cup.</li>
</ul>
This animation shows the relationship between shape and error:<br />
<br />
<div style="text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN3jhQOohXWzLU6fzrHIUYVLiVTSYuOKrKVj-nAIaknvwMiN6Oap4m-iyS0W8GRm-bjUUs8dERjasMEksFPEKlxaglwEP6InhtmPVy4LdCem2DoffwMqO_IYTv1R7u4wIMnPfDk_XbLcQ/s1600/anim-2d-euclid2-both-hirez.gif"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN3jhQOohXWzLU6fzrHIUYVLiVTSYuOKrKVj-nAIaknvwMiN6Oap4m-iyS0W8GRm-bjUUs8dERjasMEksFPEKlxaglwEP6InhtmPVy4LdCem2DoffwMqO_IYTv1R7u4wIMnPfDk_XbLcQ/s400/anim-2d-euclid2-both-hirez.gif" /></a></div>
<br />
A natural question is, if narrow means more accurate, then why not use test tubes for measuring everything? Well, a test tube that holds 2-cups would be 10 feet high. I’m not sure about your cupboards, but 10-feet won’t fit in my cupboards, even if I rearrange things. But even if you bit the bullet and remodeled the kitchen to include a 10-foot high cupboard, the test tube would still be hard to use. A large fraction of whatever you’re measuring might end up stuck to the sides of the tube because there’s so much tube. So practical measuring cup design trades off accuracy for convenience.</div>
<div>
<br />
<h2>
Designing for the S/V ratio</h2>
We just observed that the ratio of surface area to volume (S/V ratio) is important to measuring accuracy, and that measuring cups today are less accurate measuring smaller amounts because the S/V ratio is larger for smaller amounts.<br />
<br />
That suggests that a solution would be a measuring cup shaped so that the S/V ratio is the same at every line marking. That would make it just as accurate measuring small amounts as large amounts. More precisely, it would be optimal in the sense that it minimizes the variance of error across all measurement amounts, for a chef who can get liquid to within k millimeters of each measurement line, for some constant k that varies with each chef's ability / effort.<br />
<br />
Figuring out what kind of shape would have this property involves math. I started with some simplifying assumptions, like assuming the measuring cup is circular. It still was tricky and took me a number of months working on weekends. The math is not as complicated as it might sound. If I were honest, I’d say I didn’t know what I was doing initially and it took me a while to think about the problem the right way. But I’m not sure I’m ready to admit that :).<br />
<br />
Below is an animation of how the surface area and volume changes. The Kickstarter page has a more detailed description of how the solution works. Below, I’ll talk about the process a bit.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJLWnnvBNfwP1cj06XaJv0Rp_GJlZ_ZJooJdP50BhDkRmBY9R6YZq3tipR8IkNSf8Nq5a0plabrVA1wVsvaHnKUnjbocRkrUDfmEa5DGrLNm53V_QV243ESf8j4Tnx0MAcA3sb8ctWfWE/s1600/anim-3d-ratio2-hirez.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="520" data-original-width="1080" height="192" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJLWnnvBNfwP1cj06XaJv0Rp_GJlZ_ZJooJdP50BhDkRmBY9R6YZq3tipR8IkNSf8Nq5a0plabrVA1wVsvaHnKUnjbocRkrUDfmEa5DGrLNm53V_QV243ESf8j4Tnx0MAcA3sb8ctWfWE/s400/anim-3d-ratio2-hirez.gif" width="400" /></a></div>
<br /></div>
<div>
<br />
<h2>
The first design</h2>
There are many potential designs that have a constant S/V ratio. As I was developing the equations, I started playing with visualizations to understand the design space, first 2D via javascript and SVG, and then 3D using <a href="https://www.blender.org/">Blender</a>, an open-source modelling program. Blender was cool partly because it has a python API, which made it possible to programmatically generate curves and surfaces that have the right geometry. The first rendering I have from this time is:<br />
<br />
<img src="https://lh6.googleusercontent.com/z_2qsAcBTJWy-ve-6JGx7pfJODSsof33CN6gEGMVVBEbWjOXH1Z8StsD9cz_YXALXIdxAAflEfwxNb2_nKNfjnpJPEykz_isattgwXAcQCBnls0UY7k2WfOCIbySckq9aBRkHkQT" width="100" /><br />
<br />
<h2>
Bye-bye job</h2>
Around this point I left Facebook to work on this full-time. I knew I wanted to try starting some kind of company, I loved the measuring cup idea, and I also loved that it was far afield from what software engineers normally start companies around. Part of me liked the quizzical, surprised and somewhat distributed expression people got on their faces when I expressed my intentions.<br />
<br />
I thought of the measuring cup somewhat as a passion project and break from SWE, so I jumped into it with no business plan or customer research. Around this time I also incorporated an S-corp, filed patent applications and so forth.</div>
<div>
<br />
<h2>
Designing how to design</h2>
I started having industrial designer contractors help design the measuring cup and it quickly became apparent that there was a huge gap. Neither they nor their design software could couple design with the surface area & volume (S/V) geometry constraints in the way we needed.<br />
<br />
There were some expensive process failures here. They would design shapes that I didn’t know how to modify to obey the S/V constraints. The scheme I finally came up with was to separate design into three steps. First, the industrial designers created a cross-sectional closed curve of the measuring cup, such as:</div>
<div>
<br />
<img height="186" src="https://lh3.googleusercontent.com/hVShEj89OQsnuvaIZvtLs3a-EJUh-EApnmnfnekwBLWCcOC8iOmhg130ILRBcxr_OYt9znSwRN8Txq7wBAd6gQkVUWdzjG5egJ3_6GUl2P-EPf_O0h4Lg4wQny-cjjIyDAqQ_foN" width="200" /></div>
<div>
<br />
I ran custom Python that replicated the curve as a sequence of ribs, positioning and scaling each rib according to the S/V constraints. For example:</div>
<div>
<br />
<img height="200" src="https://lh6.googleusercontent.com/woztwG2JQFjkczfQEyzvuORDwnbZx_lmLLPJLo2hF2Brx_u_ZqP1mwfV_0HO1E4cRHIGSJPqph_aoE2a406B9pDn8KEPXT-nAdgLS5CL3wxidi5R92MZFqyhZ92b0ShvJrF0DlPg" width="143" /> -----> <img height="200" src="https://lh4.googleusercontent.com/dq2x7rW7TkVO91OaG_iKWsfTU_YcLLB0ISD53gZ2S9PcDoesBiybcJKl83RFDyzSb578tDtamLJw4SfeZXjBVuVUTIgLnPUyWH6XucJQEnYwvs_YFlwvO44HpQ-PiJVxKMUR77LJ" width="145" /></div>
<div>
<br />
While 3D design software is terrible for designing with mathematical constraints, it is excellent for calculating surface area and volume of things. So once I had generated a shape, it was possible to write code to double check that the surface area and volume at every height was indeed what it was supposed to be.</div>
<div>
<br />
<h2>
Prototyping</h2>
I went through 25+ physical prototypes and countless 3D models. The bread & butter prototypes were 3D-printed in white plastic, sometimes via someone’s Makerbot, via <a href="https://www.3dhubs.com/">3D Hubs</a>, a 3D printing marketplace. A much small number of high quality prototypes, including the one in the video, were milled from a block of <a href="https://en.wikipedia.org/wiki/Poly(methyl_methacrylate)">PMMA</a>. I wasn’t able to figure out a way to make transparent, food-safe prototypes economically.<br />
<br />
The original shapes were circular, such as<br />
<img height="100" src="https://lh4.googleusercontent.com/M4bs3cnyp538bel24M25OFsgSlOXqqCAlPIN4c1NtAxPbYIYiH9WOE2ZuOcm7XSWj_fFyO5mbNjdzVcP2kBD_lin1PxpTNBIm6-yWGMovXDgRMdGnJfK0AO3hm9FVhmql3oKCPUu" /><br />
But it became apparent that industrial printing techniques could not print accurately on surfaces with compound curvature (i.e., non-zero <a href="https://en.wikipedia.org/wiki/Gaussian_curvature">Gaussian curvature</a>, meaning it curves in two directions). So we switched to the flat-sided design you see on Kickstarter. This design also had a bonus that the markings much easier to read than on both our previous prototypes and most existing measuring cups.<br />
<img height="176" src="https://lh3.googleusercontent.com/mcfgq5Qe__8hOBuXgiugxYDsMQ-bXU_CTcyXg57SVyjl-rmBeZcC7Y_YHVvde-JRtgk8yOOIjrRdULx9QdlS1cq1IYlhSu3k5_xNGFsP-RlJfC-6FRIXxU5nubMLNVnlNtN7dovx" width="400" /></div>
<div>
<br />
<h2>
Manufacturing</h2>
Talking to manufacturers was both thrilling and challenging. It was thrilling because they run heavy machinery that takes as input raw materials and usually some software and produces useful physical objects. As a software engineer, my best efforts just push around electrons.<br />
<br />
Talking to manufacturers was challenging because the domain knowledge is very different from that of software engineering. For example, there are engineers who specialize in something called <a href="https://en.wikipedia.org/wiki/Design_for_manufacturability">Design For Manufacturing (DFM)</a>. You might think you come up with a design, prototype, do some testing and then hand it off to a manufacturer. Oh no no, my friend. If you don’t have in mind the technical constraints of manufacturing when you design, you probably are in for a surprise and some redesign work.<br />
<br />
Manufacturers of course want business, but there is also a cost to them for talking with you. That includes the time of their in-house engineer to analyze your design, the risk that you might cause problems for them down the line because you’re inexperienced, and of course the opportunity cost of not working with someone who might place a larger order than you intend to.<br />
<br />
I generally found manufacturers very polite and willing to both quote my designs and give useful feedback if there were issues. Through many iterations and discussions with many manufacturers, along with advice from DFM experts, the design was refined to the point where they now don't see any issues.</div>
<div>
<br /></div>
<div>
I’ll save a detailed discussion of manufacturing for another post, but below are two of the bigger limitations to injection molding. Limitations? To injection molding? Yup.<br />
<div>
<br />
First, constant wall thickness is important. Injection-molding does not tolerate well large variations in thickness of the part. Partly this is because plastic shrinks as it cools and thicker sections shrink differently than thinner sections. Thick blobs of plastic also can have problems with air bubbles, and thick blobs also increase cost since the part needs to cool in the mold longer, which is time the machine can’t be making the next part.<br />
<br />
Second, the way plastic flows when it's being injected matters. The way it works is this. The mold is a block of steel with a cavity into which hot, liquid plastic is squirted under high pressure. The plastic enters through a hole in the mold (called a “port”) and flows to fill the entire mold. As the plastic flows, it cools because the steel is relatively cold. The steel is cool partly to help the part solidify quickly so the machine can move on to make the next part. As the plastic cools, it becomes thicker and starts to harden. One reason the plastic enters under high pressure is so it fills the entire mold before it hardens.<br />
<br />
The consequence of this is that sections of the mold that are further from the injection port are going to receive plastic that is cooler and thicker, and so the shape that the plastic can form is more limited. Also, shrinkage of plastic that is farther from the injection port differs from that of plastic closer to the injection port.<br />
<br />
Luckily there are great software flow analysis tools out there that simulate how the plastic fills a mold and can predict what sort of issues might arise.<br />
<div>
<h2>
The rest</h2>
<div>
I skipped over a lot of detail about manufacturing and haven't even talking about printing and industrial inks. Nor have I talked about selecting a specific plastic and the range of material properties of plastics. Stay tuned or ask questions...</div>
</div>
</div>
</div>
Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com5tag:blogger.com,1999:blog-3338128750432104713.post-72197643521462521182017-02-17T11:45:00.001-08:002017-02-17T11:45:33.760-08:00Converting video to high-quality gif on linux (via ffmpeg)A brief post today. Recently, I've been taking videos of my screen and converting them to animated gifs.
<br>The command I use to record the video, for example to record 15 seconds of video at 25 fps, sized 830x150 pixels at offset from the top of x=230,y=360 is:
<pre class="jcodeblock">
avconv -f x11grab -y -r 25 -s 830x150 -i :0.0+230,360 -vcodec libx264 -crf 0 -threads 4 -t 15 myvideo.mp4
</pre>
<p>For a while I was using a one-liner to convert the video to animated gifs using ffmpeg and ImageMagick's convert command based on <a href="http://superuser.com/questions/556029/how-do-i-convert-a-video-to-gif-using-ffmpeg-with-reasonable-quality/730389#730389">this post</a>. Below it samples the mp4 at 15 fps.
<pre class="jcodeblock">
ffmpeg -i myvideo.mp4 -r 15 -f image2pipe -vcodec ppm - | convert -delay 7 -loop 0 - gif:- | convert -layers Optimize - myvideo.gif
</pre>
<p>The problem I ran into was the the color palette choice in the gifs was poor and creating weird artifacts.
<p>So I came across <a href="http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html">http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html</a>, which layed out another strategy, resulting in the following two commands:
<pre class="jcodeblock">
ffmpeg -v warning -i myvideo.mp4 -vf "fps=25,palettegen" -y /tmp/pallette.png
ffmpeg -v warning -i myvideo.mp4 -i /tmp/pallette.png -lavfi "fps=25 [x]; [x][1:v] paletteuse" -y myvideo.gif
or, if you don't need to change the frame rate:
ffmpeg -v warning -i myvideo.mp4 -vf "palettegen" -y /tmp/pallette.png
ffmpeg -v warning -i myvideo.mp4 -i /tmp/pallette.png -lavfi paletteuse -y myvideo.gif
</pre>
<p>I found this approach much better. The color artifacts were gone and also the file sizes were much smaller! It chopped about 50-80% off of the file size compared to the previous ffmpeg/convert pipeline.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-79402136539827183102016-09-13T16:33:00.001-07:002016-09-13T16:33:04.670-07:00Connecting to wifi without NetworkManager on Ubuntu 16.04 (using wpa_supplicant, dnsmasq, etc)Suppose <a href="https://help.ubuntu.com/community/NetworkManager">NetworkManager</a> in Ubuntu is getting in your way and not letting you configure your wifi how you'd like. You'd like to temporarily turn off NetworkManager and connect to wifi in a lower-level way, without making any permanent changes that will interfere with NetworkManager. Below is the series of commands that worked for me. The approach below plays well with the underlying <a href="http://www.thekelleys.org.uk/dnsmasq/doc.html">dnsmasq</a> and <a href="https://en.wikipedia.org/wiki/Wpa_supplicant">wpa_supplicant</a> that NetworkManager uses.
<p>The first step is turning off NetworkManager. This seems to persist across sleeping/waking my laptop.
<pre class="jcodeblock">sudo service NetworkManager stop</pre>
<p>Next, get the name of your wireless interface. On my laptop it is wlp3s0, but it might be some other 'w' name like wlan0. All the code snippets below use wlp3s0. Change appropriately if your interface name is different. To find the interface name, you could do "<code>ip link</code>" and look for an entry that begins with "w", or more programatically:
<pre class="jcodeblock">
$ iw dev | awk '/Interface/{print $2;}'
wlp3s0
</pre>
<p>Next, take down the interface, make changes, and bring it back up.
<pre class="jcodeblock">
sudo ip link set dev wlp3s0 down
sudo ip link set dev wlp3s0 blah blah blah # make any changes you want here
sudo ip link set dev wlp3s0 up
</pre>
<p>Now for some magic. wpa_supplicant is a process that knows how to connect/associate with an access point using wpa security (unlike iwconfig, which apparently only knows about the weaker WEP). It looks like, when you turn off NetworkManager, it tells wpa_supplicant to forget about wireless interfaces. This means that command-line tools like wpa_cli, that talk with wpa_supplicant, won't work. So the way to tell wpa_supplicant about the wireless interface again is:<pre class="jcodeblock">
$ sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path /fi/w1/wpa_supplicant1 --method fi.w1.wpa_supplicant1.CreateInterface "{'Ifname':<'wlp3s0'>,'Driver':<'nl80211,wext'>}"
(objectpath '/fi/w1/wpa_supplicant1/Interfaces/25',)
</pre>
Note down the path it returns (<code>/fi/w1/wpa_supplicant1/Interfaces/25</code> in this example). You'll use it when restoring NetworkManager, below.
<p>Now wpa_cli should start working. It looks like NetworkManager configures wpa_supplicant to listen on a control socket different from the default place wpa_cli looks, so you'll need an extra arg to wpa_cli, "-p /run/wpa_supplicant", as below.
<p>wpa_supplicant and thus wpa_cli have the notion of a 'network', which is an access point you'd like wpa_supplicant to try to connect to. Taking down NetworkManager should have removed any networks, but to be sure, try:
<pre class="jcodeblock">
sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 list_networks
</pre>
<p>At this point, you can scan for wifi networks you'd like to connect to (I fuzzed the output below to not reveal actual BSSIDs or SSIDs):
<pre class="jcodeblock">
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 scan
OK
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 scan_results
bssid / frequency / signal level / flags / ssid
20:e5:2a:2b:34:d1 2442 -58 [WPA2-PSK-CCMP][WPS][ESS] Josh's Network
22:86:8c:e1:33:70 2462 -78 [ESS] xfinitywifi
</pre>
<p>Create a 'network' to associate with. This prints an integer. It should be I think 0, since NetworkManager removed networks when we stopped it. That '0' appears in the wpa_cli commands, below. Change if add_network returns something other than 0.
<pre class="jcodeblock">
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 add_network
0
</pre>
<p>Pick an ssid and set the wpa password
<pre class="jcodeblock">
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 set_network 0 ssid "\"Josh's Network\""
OK
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 set_network 0 psk '"a passphrase"'
OK
# Alternatively, to connect without any passphrase, you can say
# sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 set_network 0 key_mgmt NONE
</pre>
<p>Enabling the network should cause wpa_supplicant to connect
<pre class="jcodeblock">
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 enable_network 0
OK
</pre>
<p>To check status of connection (output below a bit fuzzed to not reveal actual address info):
<pre class="jcodeblock">
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 status
bssid=20:e5:2a:2b:34:d1
freq=2442
ssid=Josh's Network
id=0
mode=station
pairwise_cipher=CCMP
group_cipher=CCMP
key_mgmt=WPA2-PSK
wpa_state=COMPLETED
address=00:19:d1:d4:d9:22
uuid=61294555-153f-568c-9ed7-36af41fff2e0
</pre>
<p>Once you're connected, which I think is when the "<code>wpa_state=COMPLETED</code>" shows up in status, get an IP
<pre class="jcodeblock">
sudo dhclient -v wlp3s0
</pre>
<p>Next set up DNS. NetworkManager is set up to use dnsmasq. To communicate with dnsmasq, do something like this (to set it to use opendns servers):
<pre class="jcodeblock">
$ sudo dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager.dnsmasq /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetDomainServers array:string:"208.67.222.222","208.67.220.220"
method return time=1473770256.196485 sender=:1.165 -> destination=:1.197 serial=11 reply_serial=2
</pre>
That's it!
<pre class="jcodeblock">
$ ping google.com
PING google.com (209.85.232.139) 56(84) bytes of data.
64 bytes from qt-in-f139.1e100.net (209.85.232.139): icmp_seq=1 ttl=41 time=32.0 ms
64 bytes from qt-in-f139.1e100.net (209.85.232.139): icmp_seq=2 ttl=41 time=32.3 ms
</pre>
<p>To then to tear things down when done and restart NetworkManager:
<pre class="jcodeblock">
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 disable_network 0
OK
$ sudo wpa_cli -p /run/wpa_supplicant -i wlp3s0 remove_network 0
OK
sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path /fi/w1/wpa_supplicant1 --method fi.w1.wpa_supplicant1.RemoveInterface "'/fi/w1/wpa_supplicant1/Interfaces/13'"
()
$ sudo service network-manager start
</pre>
<hr>
<p>Below is a script putting this all together, for your reference. I didn't focus much on robustness, so it's a little fragile:
<pre class="jcodeblock">
#!/bin/bash
set -o pipefail # Make it so return code from pipe is last one to fail
# https://w1.fi/wpa_supplicant/devel/dbus.html - super handy API reference for talking with wpa_supplicant via gdbus
setup() {
wif=$(iw dev | awk '/Interface/{print $2;}')
expectNoNetworks=0
if (sudo service network-manager status | egrep '^\s+Active: active ' > /dev/null); then
echo "Stopping NetworkManager"
sudo service network-manager stop || exit
sleep 1 # Give /run/wpa_supplicant time to disappear
if [ -e /run/wpa_supplicant ]; then
echo "error: /run/wpa_supplicant exists"
exit 1
fi
sudo ip link set dev $wif down || exit
#
# Make any changes to the device in here.....
#
#
sudo ip link set dev $wif up || exit
expectNoNetworks=1
fi
echo "Checking wpa_supplicant interface for $wif"
if ! pre="$(sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path /fi/w1/wpa_supplicant1 --method fi.w1.wpa_supplicant1.GetInterface "'$wif'" 2> /dev/null )"; then
echo " Creating interface"
pre="$(sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path /fi/w1/wpa_supplicant1 --method fi.w1.wpa_supplicant1.CreateInterface "{'Ifname':<'$wif'>,'Driver':<'nl80211,wext'>}")" || exit
fi
# Converts something like (objectpath '/fi/w1/wpa_supplicant1/Interfaces/25',) ==> /fi/w1/wpa_supplicant1/Interfaces/25
ifPath=$(echo $pre | sed -e "s/^.*'\(.*\)'.*$/\1/")
if [ ! -e /run/wpa_supplicant ]; then
echo "error: /run/wpa_supplicant does not exist"
exit 1
fi
if ! netInt=$(sudo wpa_cli -p /run/wpa_supplicant -i $wif list_networks | awk '/^[0-9]+\s/ { print $1;}'); then
echo "List networks failed"
exit 1
fi
if [ "$netInt" != "0" ] || [ $expectNoNetworks -eq 1 ]; then
if [ "$netInt" ]; then
echo "Removing any networks"
sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path $ifPath --method fi.w1.wpa_supplicant1.Interface.RemoveAllNetworks || exit
fi
# We don't expect NetworkManager to have left around any networks, but get rid of them in any case
netInt="$(sudo wpa_cli -p /run/wpa_supplicant -i $wif add_network)" || exit
fi
echo "netInt is $netInt, ifPath is $ifPath"
}
command="start"
if [ "$1" ]; then
command="$1"
fi
case $command in
setup)
setup
exit 0
;;
list)
setup
echo "Doing a AP scan"
sudo wpa_cli -p /run/wpa_supplicant -i $wif status
sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state'
sudo wpa_cli -p /run/wpa_supplicant -i $wif scan > /dev/null || exit
while sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state=SCANNING' > /dev/null; do
sleep 0.1
done
# Reformat scan results to be a bit prettier.
sudo wpa_cli -p /run/wpa_supplicant -i $wif scan_results | grep -v 'bssid / frequency / signal level / flags / ssid' | sort -nr -k 3 | awk 'BEGIN { FS="\t"; printf "Signal\n"; printf "%6s %-30s %-50s %s %s\n", "Level", "SSID", "Flags", "Frequency", "BSSID"; } { printf "%6s %-30s %-50s %-9s %s\n", $3, $5, $4, $2, $1; }'
read -p "Enter a SSID to connect to: " aSsid
if [ -z "$aSsid" ]; then
echo "Can not have empty ssid, i think"
exit 1
fi
#sudo wpa_cli -p /run/wpa_supplicant -i $wif status
sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state'
if ! sudo wpa_cli -p /run/wpa_supplicant -i $wif status | egrep 'wpa_state=(INACTIVE|DISCONNECTED)' > /dev/null; then
echo "Disconnecting current connection"
sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path $ifPath --method fi.w1.wpa_supplicant1.Interface.Disconnect > /dev/null || exit
fi
echo "Disabling network"
sudo wpa_cli -p /run/wpa_supplicant -i $wif disable_network 0 > /dev/null || exit
sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state'
echo "Setting ssid"
sudo wpa_cli -p /run/wpa_supplicant -i $wif set_network 0 ssid "\"$aSsid\"" > /dev/null || exit
read -sp "Enter WPA password, or leave blank to try connecting with no password (chars will not echo): " aPassword
if [ -z "$aPassword" ]; then
echo "using no passowrd"
sudo wpa_cli -p /run/wpa_supplicant -i $wif set_network 0 key_mgmt NONE > /dev/null || exit
else
sudo wpa_cli -p /run/wpa_supplicant -i $wif set_network 0 psk "\"$aPassword\"" > /dev/null || exit
fi
echo "Enabling network"
sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state'
sudo wpa_cli -p /run/wpa_supplicant -i $wif enable_network 0 > /dev/null || exit
if sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state=DISCONNECTED' > /dev/null ; then
echo "Selecting network $netInt"
sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path $ifPath --method fi.w1.wpa_supplicant1.Interface.SelectNetwork "$ifPath/Networks/$netInt" > /dev/null || exit
fi
echo "Waiting for wpa_supplicant to get into COMPLETED state"
while ! sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state=COMPLETED' > /dev/null ; do
sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state'
sleep 0.5
done
echo "Now getting IP"
sudo dhclient -v $wif || exit
echo "Setting openDNS"
sudo dbus-send --system --print-reply --dest=org.freedesktop.NetworkManager.dnsmasq /uk/org/thekelleys/dnsmasq uk.org.thekelleys.SetDomainServers array:string:"208.67.222.222","208.67.220.220" > /dev/null || exit
echo "Done!"
exit 0
;;
done)
if (sudo service network-manager status | egrep '^\s+Active: active ' > /dev/null); then
echo "Looks like NetworkManager is active, so I think we're done"
exit 0
fi
setup
echo "Disabling Network"
sudo wpa_cli -p /run/wpa_supplicant -i $wif disable_network 0 > /dev/null || exit
echo "Removing all networks"
sudo wpa_cli -p /run/wpa_supplicant -i $wif remove_network $netInt || exit
#sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path $ifPath --method fi.w1.wpa_supplicant1.Interface.RemoveAllNetworks || exit
sudo wpa_cli -p /run/wpa_supplicant -i $wif status | grep 'wpa_state'
echo "Removing interface"
sudo gdbus call --system --dest=fi.w1.wpa_supplicant1 --object-path /fi/w1/wpa_supplicant1 --method fi.w1.wpa_supplicant1.RemoveInterface "'$ifPath'" || exit
sleep 1
echo "Restarting NetworkManager"
sudo service network-manager start || exit
echo "Done"
exit 0
;;
*)
echo "Command arg is (setup | list | done)"
exit 1
;;
esac
</pre>
Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-83834173283839285032016-06-27T15:00:00.000-07:002017-03-19T13:37:02.854-07:00Running a tiny node.js webserver hosting multiple domains with SSL on google compute engineEDIT: 3/19/2017 - improved server security
<p>I recently migrated a few low-traffic domains from being hosted on namecheap shared hosting to running on a private node.js server on google compute engine. This was partly because I wanted to run a node.js app on one of the domains, and partly because I was tired of the poor quality of namecheap hosting. I wanted to share what ended up working with anyone else who's trying to do something similar.
<p>I picked Google Compute Engine for hosting partially because an app I'm developing uses google cloud datastore and partly because they had a small machine type that looked like it might work. Their <a href="https://cloud.google.com/compute/pricing#predefined_machine_types">pricing</a> lists a machine type, f1-micro, with 0.6 GB which costs $.0056/hour, or around $4/month.
<p>Let's jump in. The webserver we create will be serving pages for multiple domains. Later we'll get the IP address for the server and point multiple domains at it. Let's assume we have four domains: example1.com, www.example1.com, example2.com, www.example2.com. Let's assume our server is server.js and we run it by <code class="jcodeblock">node server.js</code>. Note: I've simplified the code in this post a bit from what I actually use, to remove parts that aren't relevant to this post. There's a chance there are bugs in it. Please let me know if you find anything amiss.
<p>Let's say our directory structure for our app is:
<pre class="jcodeblock">server.js
package.json
static-example1/
static-example2/
</pre>
where the static directories are where we'll serve static files for our domains.
<ol>
<li><code>server.js</code> config. I use the <a href="http://expressjs.com/">express</a> framework. We'll set it up so that HTTP redirects to HTTPS and also www.example* redirects to example*. SSL/acme/letsencrypt explained below.
<pre class="jcodeblock">var express = require("express");
var http = require('http');
var https = require('https');
var app = express();
// letsencrypt verification requests will be HTTP. Let them proceed without any of the redirection/https checking below.
app.use('/.well-known/acme-challenge/', express.static('static-acme-challenge/.well-known/acme-challenge'));
app.use('/.well-known/acme-challenge/',function(req,res, next) {
res.status(404).send('letsencrypt challenge file missing');
});
app.use(function redirects(req, res, next) {
var host = req.headers.host;
if ((host == 'example1.com' || host == 'example2.com') && req.secure) {
// good to go
next();
} else if (host == 'www.example1.com') {
// redirect both to HTTPS as well as get rid of the www subdomain.
res.redirect('https://example1.com' + req.url);
} else if (host == 'www.example2.com') {
// redirect both to HTTPS as well as get rid of the www subdomain.
res.redirect('https://example2.com' + req.url);
} else if (!req.secure) {
res.redirect('https://' + req.headers.host + req.url);
} else {
should never get here....
}
});
var vhost = require('vhost');
// You could change this up so that instead of serving static files you do more interesting routing. Beyond scope of this blog..
app.use(vhost('example1.com', express.static('static-example1')));
app.use(vhost('example1.com', function(req, res, next) {
res.status(404).send('no static file found for example1.com');
}));
app.use(vhost('example2.com', express.static('static-example2')));
app.use(vhost('example2.com', function(req, res, next) {
res.status(404).send('no static file found for example2.com');
}));
app.use("*",function(req,res) {
this should never happen - all requests should have been caught by one of the clauses above.
});
var httpServer = http.createServer(app);
var httpsServer = null;
if (fs.existsSync("./le-config/live/example1.com/privkey.pem")) {
httpsServer = https.createServer({
key: fs.readFileSync("./le-config/live/example1.com/privkey.pem"),
cert: fs.readFileSync("./le-config/live/example1.com/fullchain.pem"),
ca: fs.readFileSync("./le-config/live/example1.com/chain.pem")
}, app);
} else {
console.log('No SSL certs found. Assuming we are bootstrapping with no https');
}
httpServer.listen(80);
httpsServer.listen(443);
</pre>
<li>Get code somewhere that google compute engine can find it. We put it in a git repository, with server.js in the root dir of the repository. Then it's easy to use <a href="https://cloud.google.com/source-repositories/docs/">Google cloud repositories</a> to get it to the VM. According to <a href="https://cloud.google.com/nodejs/getting-started/run-on-compute-engine#pushing_your_code">this</a>, a cloud repository is created for your project. To set up for pushing to it:
<pre class="jcodeblock">git config credential.helper gcloud.sh
git remote add cloud https://source.developers.google.com/p/your-project-id/
</pre>
Substitute "your-project-id" with your google project id (probably is something like <code>myproj-65432</code>. Then you can push with
<pre class="jcodeblock">git push google master</pre>
<li>Now we need to create a startup script that the VM instance will run when it first starts up. Here's the simplified version of the script I use. I put it in the file <code>startup-script.sh</code>, in the same directory as server.js, though it doesn't need to be.
<pre class="jcodeblock">#! /bin/bash
# [START startup]
set -v
# Talk to the metadata server to get the project id
PROJECTID=$(curl -s "http://metadata.google.internal/computeMetadata/v1/project/project-id" -H "Metadata-Flavor: Google")
USERNAME=something # Replace with your username you use to log in to the server
# Set up a 512MB swap partition. The 600 MB of ram the VM has is not quite enough to do the 'npm install' command below.
fallocate -l 512m /mnt/512MiB.swap
chmod 600 /mnt/512MiB.swap
mkswap /mnt/512MiB.swap
swapon /mnt/512MiB.swap
# [START the rest]
# Debian has old version of node. Get fresh one. This does an apt-get update
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
# Some necessary packages
apt-get install -yq ca-certificates git nodejs build-essential supervisor libcap2-bin authbind unattended-upgrades
apt-get install certbot -t jessie-backports
# Create a nodeapp user. Node will run as this user. This account does not have privileges to run a shell.
useradd -M -d /usr/sbin/nologin nodeapp
# Users seem to be added in default gropus that include a group with sudo privileges. Remove nodeapp from those groups
for agr in `groups nodeapp | cut -f 2 -d ':'`; do
if [ $agr != "nodeapp" ]; then
echo "Removing nodeapp from group $agr"
gpasswd -d nodeapp $agr
fi;
done
# Set default group to nodeapp
/usr/sbin/usermod -g nodeapp $USERNAME
# Set default permissions for new files so only nodeapp group can read them
grep -q '^\s*umask ' /home/$USERNAME/.profile && sed -i 's/^\s*umask .*/umask 0026/' /home/$USERNAME/.profile || echo 'umask 0026' >> /home/$USERNAME/.profile
# /opt/app will hold the git repo containing server.js
mkdir /opt/app
chown nodeapp:nodeapp /opt/app
cd /opt/app
# For authbind - let nodeapp bind to only ports 80 and 443
touch /etc/authbind/byport/80
touch /etc/authbind/byport/443
chown nodeapp /etc/authbind/byport/80
chown nodeapp /etc/authbind/byport/443
chmod 755 /etc/authbind/byport/80
chmod 755 /etc/authbind/byport/443
# Create the script that cron will run every 2 weeks to renew our SSL certs
# Note it restarts the server every 2 weeks as part of this.
cat >/tmp/renew-cert.sh << EOF
#! /bin/bash
cd /opt/app
echo "`whoami`:`pwd`: Checking renewal on `date`:"
certbot renew --non-interactive --email subadmin@example1.com --agree-tos --debug --test-cert --config-dir ./le-config --work-dir ./le-work --logs-dir ./le-logs --webroot --webroot-path ./static-acme-challenge
sudo supervisorctl restart nodeapp
EOF
chown $USERNAME:nodeapp /tmp/renew-cert.sh
# Run a bunch more setup not as root
su - $USERNAME << EOF
# Get the application source code from the Google Cloud Repository.
git config --global credential.helper gcloud.sh
git clone https://source.developers.google.com/p/$PROJECTID /opt/app
# Create directory for the letsencrypt challenges to be served by server.js
mkdir static-acme-challenge
# Install app dependencies specified in package.json
# --production means to skip dev dependencies
npm install --production
# setup the cron script
mv /tmp/renew-cert.sh .
chmod u+x renew-cert.sh
(crontab -l && echo "01 02 2,16 * * /opt/app/renew-cert.sh >> /opt/app/le-logs/cron-renews 2>&1") | crontab -
EOF
# Now that the npm installation has finished, we no longer need swap, so get rid of it.
swapoff -a
rm /mnt/512MiB.swap
# Configure supervisor to run the node app. Limit the amount of RAM node uses via the flags specified.
cat >/etc/supervisor/conf.d/node-app.conf << EOF
[program:nodeapp]
directory=/opt/app
command=authbind node --trace_gc --max_old_space_size=256 --max_semi_space_size=16 --max_executable_size=256 server.js
autostart=true
autorestart=true
user=nodeapp
environment=USER="nodeapp",NODE_ENV="production",ONGOOG="yes"
stdout_logfile=syslog
stderr_logfile=syslog
EOF
# Start the server.js. Note it does not have SSL certs yet, it is running just so certbot can do the letsencrypt challenges
supervisorctl reread
supervisorctl update
# Give server a chance to start up
sleep 5
# Make unattended apt upgrades reboot when they happen
egrep -q "^Unattended-Upgrade::Automatic-Reboot" /etc/apt/apt.conf.d/50unattended-upgrades || echo 'Unattended-Upgrade::Automatic-Reboot "true";' >> /etc/apt/apt.conf.d/50unattended-upgrades
egrep -q "^Unattended-Upgrade::Automatic-Reboot-Time" /etc/apt/apt.conf.d/50unattended-upgrades || echo 'Unattended-Upgrade::Automatic-Reboot-Time "02:15";' >> /etc/apt/apt.conf.d/50unattended-upgrades
# Generate the SSL certificates for the domains we wish to serve.
domains=('example1.com', 'www.example1.com',
'example2.com', 'www.example2.com')
# Generated a string containing each domain prefixed with a '-d'
extraargs=""; for i in "${domains[@]}"; do extraargs="$extraargs -d $i"; done
#debugflags="--debug --test-cert"
debugflags=""
# Generate the SSL certs. Run as nodeapp
su - $USERNAME -c "certbot certonly --non-interactive --email subadmin@example1.com --agree-tos --config-dir ./le-config --work-dir ./le-work --logs-dir ./le-logs --webroot --webroot-path ./static-acme-challenge $debugflags $extraargs"
# Restart server to pick up the SSL certs
supervisorctl restart
# Application should now be running under supervisor
# [END startup]
</pre>
<p>A few notes on the script. First, the main trick in getting node to run on the f1-micro instance is managing memory. We do this by temporarily adding a swap partition to absorb the extra memory the <code>npm install</code> uses, and by passing flags to node (they actually are v8 flags) to limit the resident memory usage of the node binary. Node/v8 does not have great tools for managing memory usage. In particular, I saw no way to limit the virtual memory size. I tried <code>ulimit</code> and node would not start, failing when allocating memory.
<p>Regarding SSL. I went with <a href="https://letsencrypt.org/">letsencrypt</a> since it's free.
I wanted really badly to be able to use the <a href="https://github.com/Daplie/letsencrypt-express">letsencrypt-express</a> npm package. It promised so much convenience. I banged my head against it for a few days and could not get it to work. I dug into the source code, tried fixing bugs, and eventually gave up. The source didn't seem very maintainable and did not inspire confidence. I switched to the letsencrypt natively supported <a href="https://certbot.eff.org/">certbot</a>, specifically with their <a href="https://certbot.eff.org/#debianjessie-other">other debian-8</a> mode. Worked like a charm.
<p>letsencrypt requires that you be able to prove you control the domains you want certs for. The way I use to do this is by letting <code>certbot</code> put files on the server that the letsencrypt CA can then find. I put the challenges in the <code>static-acme-challenge</code> directory.
<li>Now let's set up the server. First, <a href="https://cloud.google.com/compute/docs/gcloud-compute/">setup the gcloud command-line tool</a>. This will involve creating a project and other steps detailed in the Getting Started Guide they refer to there. In this blog, let's assume your project is called myproj. Terminology: Google calls a VM running on a machine an "instance". We'll be creating an instance that will run our node server.
<li><a href="https://cloud.google.com/compute/docs/gcloud-compute/#set_default_zone_and_region_in_your_local_client">Set the default region and zone</a>. I picked a zone by running <code class="jcodeblock">gcloud compute machine-types list</code>
and choosing the region listed for the machine type f1-micro (<code>us-east1-b</code> when I did it). Not sure if that's necessary.
<li>Reserve an static external IP address. Without this, the instance will not necessary show up at the same IP each time it's (re)started. Note you are charged ($0.01/hr) if the IP address is not used by a running instance, so don't leave it lying around if you don't need it.
<pre class="jcodeblock">gcloud compute addresses create myproj-static-ip</pre>
<p>You may now set any A records to point that address. This is necessary partly so the letsencrypt can verify you own the domains when generating the cert inside of startup-script.sh. That means downtime on your websites. You can avoid that by using another mechanism to prove to letsencrypt you control the domains (e.g., adding TXT records to DNS, or acting on the letsencrypt challenges using your old server), and modifying startup-script.sh, but that's outside the scope of this tutorial.
<li>Create the VM instance. There are <a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/create">lots of options</a> when running the create command. I create a VM based on debian because it's the default and thus I figured the least likely to have issues. I include "datastore" below because I need to access the datastore, but you can omit it. This command
<pre class="jcodeblock">gcloud compute instances create myproj-instance --machine-type=f1-micro --image-family=debian-8 --image-project=debian-cloud --scopes userinfo-email,datastore,cloud-platform --zone us-east1-b --metadata-from-file startup-script=startup-script.sh --tags https-server --address myproj-static-ip</pre>
<li>Now you can do things like <pre class="jcodeblock"># Get the console output from the running VM
gcloud compute instances get-serial-port-output myprof-instance
# ssh into the VM. You can omit the 'nodeapp@' bit.
gcloud compute ssh nodeapp@myproj-instance
</pre>
<li>Tell google to expose your new server to the internet, but opening up the ports in the firewall
<pre class="jcodeblock">gcloud compute firewall-rules create http-and-https --allow tcp:80,tcp:443 --description "Allow http and
https access to http-server" --target-tags https-server
</pre>
<li>If you want to rip the thing down, you do:
<pre class="jcodeblock">gcloud compute firewall-rules delete http-and-https
gcloud compute instances delete myproj-instance
gcloud compute addresses delete myproj-static-ip
</pre>
</ol>
<p>Enjoy and hope it's helpful.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-60210485310913735622015-10-15T17:10:00.000-07:002016-01-01T07:24:27.334-08:00Backing up your master password and secret sharingI created a tool implementing a cryptographic secret sharing algorithm, which I use for backing up my master password. The tool is at: <a href="http://equatinelabs.com/secretshare.html">equatinelabs.com/secretshare.html</a>.
<p>Here's the motivation and how it works:
<p>Let's say you use a password manager to remember your passwords, and the password manager requires a master password. If you forget the master password, you loose access to the other passwords. So how do you backup your master password?
<p>One possibility is giving your master password to trusted people such as family members. A problem with that approach is that one of the people you share it with might accidentally reveal it, post it on the internet, leave the piece of paper on which it is written somewhere a malicious person can see it, etc.
<p>Another possibility is to write your password down, tear it in half and give each half to a trusted person. Then any single person doesn't know the entire password, so if they reveal it, your password still is not entirely revealed. However, if someone malicious sees one half of the password, it makes it much easier to guess the other half. Another problem with this approach is that if either of the two people loose the slip of paper, then you can no longer reconstruct your master password.
<p>Enter <a href="https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing">Shamir's scheme for secret sharing</a>, based on an <a href="http://dl.acm.org/citation.cfm?id=359176">old but neat paper</a>. It shows how to divide a secret into a number of "shares" such that one share provides no information about the secret, and any two shares are sufficient to reconstruct the secret. It actually generalizes to any number of minimum shares, but we'll use the version that requires a minimum of two shares to reconstruct the secret.
<p>The idea is that a line is defined by two points. Knowing one point does not help determine the slope or intercept of the line. Knowing two points is enough to determine both. So in Shamir's scheme each point is a share and the line slope or intercept is the secret. The equation for a line in 2D is: <code>y = ax + b</code>.
<p>
<p>The tool I created uses this as follows. The user types in a secret. The tool generates a long (128-bit) random number for both of <code>a</code> and <code>b</code> (i.e., the slope and intercept of a line). The slope (parameter <code>a</code>) will be the encryption key used to encrypt the secret. Each share contains the coordinates of one distinct point along the line. Thus knowing one share doesn't tell you anything about the encryption key, and with two shares, it is straight-forward to figure out the slope and then decrypt to recover the secret.
<p>The tool encrypts the secret with AES-128 using the 128-bit random key. Shares are represented in base-64. I took all the easy-to-generate characters on the keyboard, threw away a few confusing ones (such as o, O, l, and .), and that left 64. I could shrink the alphabet down to something like hex, but then the length of the shares would become longer.
<p>The math to manipulate the shares and the secret key is done using a finite field based on the prime 2^128-159. Another note, I also threw in a few bits/bytes of checksum, both into the encrypted secret as well as the shares, to detect mistypes.
<p>Feel free to browse the javascript source code of the tool, it is not minified. It uses the crypto routines in the <a href="https://github.com/digitalbazaar/forge">Forge (client-side javascript library)</a>. It does not send any information over the network, except for Google analytics, though feel free to verify that for yourself. I'm happy to provide more details.
<p>Lastly, anything crypto-related is notoriously difficult to get right, including this tool. Please use with care, and it'd be great to have another pair of eyes on the javascript source code if anyone cares to browse!
Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-77406648392513276182015-02-21T15:39:00.003-08:002015-02-21T15:39:47.603-08:00Solidworks Macros via PythonI 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).
<p>Here's the initial setup:
<ol>
<li>Download and install Python. I used <a href="http://www.activestate.com/activepython/downloads">Active Python</a> 2.7.8
<li>Get Solidworks. Python macros have worked pretty seamlessly across 2012, 2014 and 2015.
<li>Get familiar with the Solidworks online API help. E.g., <a href="http://help.solidworks.com/2014/english/api/sldworksapiprogguide/Welcome.htm">http://help.solidworks.com/2014/english/api/sldworksapiprogguide/Welcome.htm</a> is for the 2014 API. Note you can change the year in the URL to access the docs for other versions of Solidworks.
</ol>
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.
<h2>Basic startup</h2>
This code snippet connects to running instance of Solidworks of the year specified. For example to connect to Solidworks 2015, set <code>swYearLastDigit = 5</code>:
<pre class="jcodeblock">
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
</pre>
You can also invoke Dispatch without the year specification, as in <code>....Dispatch("SldWorks.Application")</code>. If there's only one version on your machine, this connects to that version.
<p>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:
<pre class="jcodeblock">
model = sw.ActiveDoc
modelExt = model.Extension
selMgr = model.SelectionManager
featureMgr = model.FeatureManager
sketchMgr = model.SketchManager
eqMgr = model.GetEquationMgr
</pre>
As an example of difference in arguments, consider the <code>Equation</code> method on the <code>IEquationMGR</code> object (<code>eqMgr</code> in my code above). The <a href="http://help.solidworks.com/2014/english/api/sldworksapi/SolidWorks.Interop.sldworks~SolidWorks.Interop.sldworks.IEquationMgr~Equation.html">2014 API docs for the Equation member</a> 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:
<pre class="jcodeblock">
print("Equation 1 is: " + eqMgr.Equation(1))
eqMgr.Equation(1, "\"myVar\" = 42")
print("Equation 1 is now: " + eqMgr.Equation(1))
</pre>
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.
<p>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 <code>assert</code> return values, then delete the assertion if/when the method doesn't follow the API docs.
<h2>Creating arguments of the correct type (aka, getting <code>SelectById2</code> to work)</h2>
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:
<ol><li>Generate the static python COM bindings for solidworks
<ol><li>First, run <code>python c:\Python27\Lib\site-packages\win32com\client\makepy.py</code>
<li>Select "SldWorks 2015 Type Library" and hit OK. You'll see output like this:
<pre class="jcodeblock">
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
</pre>
The exact file name may change depending on your version of Solidworks.
</ol>
<li>Open up that generated file in a viewer, like Komodo or Notepad
<li>Open up the web page <a href="http://msdn.microsoft.com/en-us/library/cc237865.aspx">VARIANT Type Constants</a> in a browser
</ol>
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.
<p>Let's work a few common examples.
<p>First let's try the macro command to select an object by name. The recommended version of the method is <a href="http://help.solidworks.com/2014/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~selectbyid2.html">modelExt.SelectByID2</a>. If you try putting in some actual args, you'll see:
<pre class="jcodeblock">
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 "<stdin>", line 1, in <module>
File "<COMObject <unknown>>", line 2, in SelectByID2
pywintypes.com_error: (-2147352571, 'Type mismatch.', None, 8)
</pre>
The last number in the <code>Type mismatch.</code> line indicates the argument that is causing the problem. In this case it is the <tt>None</tt>, 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.
<p>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:
<pre class="jcodeblock">
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)
</pre>
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:
<pre class="jcodeblock">
arg1 = win32com.client.VARIANT(pythoncom.VT_DISPATCH, None)
</pre>
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:
<pre class="jcodeblock">
>>> arg1 = win32com.client.VARIANT(pythoncom.VT_DISPATCH, None)
>>> modelExt.SelectByID2("Sketch1", "SKETCH", 0, 0, 0, False, 0, arg1, 0)
True
</pre>
Hooray!!!
<p>Now let's try a different example. Continuing on, let's get a sketch we selected:
<pre class="jcodeblock">
>>> selMgr = model.SelectionManager
>>> aSketch = selMgr.GetSelectedObject(1).GetSpecificFeature2
>>> aSketch.Name
u'Sketch1'
</pre>
and let's get the plane it came from:
<pre class="jcodeblock">
>>> aSketch.GetReferenceEntity(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<COMObject <unknown>>", line 2, in GetReferenceEntity
pywintypes.com_error: (-2147352571, 'Type mismatch.', None, 1)
</pre>
Oops, that didn't work. Well looking into the generate python file and searching for GetReferenceEntity, we find:
<pre class="jcodeblock">
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
)
</pre>
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:
<pre class="jcodeblock">
arg1 = win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, -1)
refPlane = aSketch.GetReferenceEntity(arg1)
</pre>
and now we can see:
<pre class="jcodeblock">
>>> arg1 = win32com.client.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_I4, -1)
>>> arg1.value
-1
>>> refPlane = aSketch.GetReferenceEntity(arg1)
>>> arg1.value
4
</pre>
To decode the '4', look at the doc for <a href="http://help.solidworks.com/2014/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isketch~getreferenceentity.html">GetReferenceEntity</a>, which points to the doc for an enumeration type <a href="http://help.solidworks.com/2014/english/api/swconst/SolidWorks.Interop.swconst~SolidWorks.Interop.swconst.swSelectType_e.html">swSelectType_e </a>, which says that 4 maps to <code>swSelDATUMPLANES</code>
<h2>Constants</h2>
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:
<ol><li>Run <code>python c:\Python27\Lib\site-packages\win32com\client\makepy.py</code>
<li>Select "SOLIDWORKS 2015 Constant type library"
<li>Add an <code>EnsureModule</code> command early in your python program using the number shown in the output of the makepy command. For example, with Solidworks 2015, that is:
<pre class="jcodeblock">
swconst = win32com.client.gencache.EnsureModule('{4687F359-55D0-4CD3-B6CF-2EB42C11F989}', 0, 23, 0).constants # sw2015
</pre>
</ol>
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:
<pre class="jcodeblock">
assert arg1.value == swconst.swSelDATUMPLANES
</pre>
This isn't quite as nice as the Visual Basic interface, which structures the constants.
<p>The downside of the <code>EnsureModule</code> 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:
<pre class="jcodeblock">
class swconst:
swSelDATUMPLANES = 4
.... more constants here ...
</pre>
<h2>A prayer for GetMathUtility</h2>
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:
<pre class="jcodeblock">
>>> mathUtil = sw.GetMathUtility
>>> mathUtil.CreatePoint
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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)
</pre>
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.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com18tag:blogger.com,1999:blog-3338128750432104713.post-32013123300900952022013-10-22T15:50:00.000-07:002013-10-22T15:50:04.088-07:00Sturdy shipping tube for artSince 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:<p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKjGomHrJAwXQ3yOcfASfbM_Rd0VCfg8OKOGPhxgj6xtNwtFfGOaGF4LAYvQexSanNV4__myZsJkP9vCpAJ5NAw1sk8p-bwNacbUjp7v6Kw7umRBsMU8Qj6z1ihKPAmM0HGbjHymgw8Kg/s1600/tube.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgKjGomHrJAwXQ3yOcfASfbM_Rd0VCfg8OKOGPhxgj6xtNwtFfGOaGF4LAYvQexSanNV4__myZsJkP9vCpAJ5NAw1sk8p-bwNacbUjp7v6Kw7umRBsMU8Qj6z1ihKPAmM0HGbjHymgw8Kg/s400/tube.jpg" /></a></div>
I made this:<ol>
<li>Start with two foot section of six inch pvc pipe
<li>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.
<li>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.
</ol>
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.
Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com2tag:blogger.com,1999:blog-3338128750432104713.post-20482455569214828042012-09-27T15:53:00.000-07:002012-10-14T16:16:01.104-07:00Offline reference for javascript, jquery, jquery-ui and d3.jsFor 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.
<h4>HTML/CSS/SVG</h4>
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:
<ul>
<li>HTML 5 (draft, 3/2012): <a href="http://www.w3.org/TR/2012/WD-html5-20120329/Overview.html">http://www.w3.org/TR/2012/WD-html5-20120329/Overview.html</a>
<li>CSS 2.1 (6/2011): <a href="http://www.w3.org/TR/2011/REC-CSS2-20110607/css2.pdf">http://www.w3.org/TR/2011/REC-CSS2-20110607/css2.pdf</a>
<li>SVG 1.1 (8/2011): <a href="http://www.w3.org/TR/2011/REC-SVG11-20110816/REC-SVG11-20110816.pdf">http://www.w3.org/TR/2011/REC-SVG11-20110816/REC-SVG11-20110816.pdf</a>
</ul>
<h4>Javascript</h4>
The best doc I could find is in CHM (windows help file format). You can read this file with xchm (e.g., <code>sudo aptitude install xchm</code>). The file itself is at:
<a href="http://starcraft73.tripod.com/javascript/javascript.zip">http://starcraft73.tripod.com/javascript/javascript.zip</a>
<h4>Jquery and Jquery-UI</h4>
There's a single CHM file that includes docs for both jquery and jquery-ui at <a href="https://github.com/Yahasana/jqdoc-parser/blob/master/jQuery-UI-Reference-1.8.chm?raw=true">https://github.com/Yahasana/jqdoc-parser/blob/master/jQuery-UI-Reference-1.8.chm?raw=true</a>
There's a PDF version of just JQuery at <a href="https://bitbucket.org/greydwarf/jquerydoc/downloads/jquery_ref.pdf">https://bitbucket.org/greydwarf/jquerydoc/downloads/jquery_ref.pdf</a>
<h4><a href="http://d3js.org/">D3.js</a></h4>
Getting D3 docs available offline requires a bit more work. Following roughly <a href="https://github.com/blog/699-making-github-more-open-git-backed-wikis">this guide</a>, here are the steps:
<ol>
<li><code>sudo aptitude install libxml2-dev libxslt-dev ruby1.8</code>
<li><code>gem install gollum</code>
<li><code>git clone https://github.com/mbostock/d3.wiki.git d3.wiki</code>
<li><code>cd d3.wiki ; gollum</code>
<li>Navigate your browser to http://localhost:4567/ or whatever port gollum starts up on (it prints it out when it starts up).
</ol>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com4tag:blogger.com,1999:blog-3338128750432104713.post-80792779702998133732012-09-24T06:36:00.001-07:002012-09-24T06:36:44.068-07:00Repairing crashing Juniper VPN client on Ubuntu 12.04In 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:
<ul><li>
<a href="http://askubuntu.com/questions/137037/network-manager-not-populating-resolv-conf">http://askubuntu.com/questions/137037/network-manager-not-populating-resolv-conf</a>
</ul>
which details running <code>sudo dpkg-reconfigure resolvconf</code> 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 <code>rm -rf ~/.juniper_networks</code>, which removes the applets and juniper config information and forces it to redownload it.
Hope this helps!Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-35022490663980964202011-08-21T21:52:00.000-07:002011-08-21T22:06:46.907-07:00How to stream audio from mog.com on Ubuntu laptop to remote linux boxThe 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.
<br />
<br />I've tried lots of approaches that didn't work, like using vnc/nx/etc and setting up pulseaudio with module-tunnel-sink.
<br />
<br />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:
<br />
<br /><code>load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;192.168.0.0/16</code>
<br />
<br />and restart pulseaudio with:
<br />
<br /><code>pulseaudio --kill ; pulseaudio --start</code>
<br />
<br />This tells pulseaudio to accept network connections from other machines on the local network.
<br />
<br />Then, on my laptop, I run google-chrome as follows:
<br />
<br /><code>PULSE_SERVER=tcp:[settop box hostname] google-chrome --user-data-dir=/home/redstone/tmp/chrome-mog --app=http://mog.com</code>
<br />
<br />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.
<br />
<br />You may want to run:
<br /><code>PULSE_SERVER=tcp:[settop box hostname] pavucontrol</code>
<br />to fiddle with the audio settings on the settop box.
<br />
<br />I suspect that this approach should work with any other internet music streaming service like spotify/pandora/etc.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-2598035036507353102011-08-20T21:52:00.001-07:002011-08-20T21:54:44.146-07:00Back and internal pictures of Niles Audio PS-1 Phono/Aux A-B SwitcherFor 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:
<br />
<br />
<br />
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-rtuqkbNFV4b0x7OXC7y7tfqlLMUqEGjR_D3tOWV7yF_u_rMeb1KRS-aYNyyjaNwlFSpCOvTaJTZSyJJh-UuwZKLEyQhBbojwR0iDL59F8k9wY_mruuzv6EMpvc2-M_ET7hNtOzzyeAo/s1600/IMG_20110820_214444.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-rtuqkbNFV4b0x7OXC7y7tfqlLMUqEGjR_D3tOWV7yF_u_rMeb1KRS-aYNyyjaNwlFSpCOvTaJTZSyJJh-UuwZKLEyQhBbojwR0iDL59F8k9wY_mruuzv6EMpvc2-M_ET7hNtOzzyeAo/s320/IMG_20110820_214444.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5643167910898077138" /></a>
<br />
<br />
<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEic2n0jl9lrIesbk4P-TlUNR7ZuCLUD00j6RfFegPpGHnyxTt62BAkvVQCxo9NN5cE3CqfYvoeaAqnldGDq0PkTS777VMrKvtwnhErlJbCAoMWR1zYZ6L08mKEB8bDVgh98W7-T9rmXvqs/s1600/IMG_20110820_214709.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEic2n0jl9lrIesbk4P-TlUNR7ZuCLUD00j6RfFegPpGHnyxTt62BAkvVQCxo9NN5cE3CqfYvoeaAqnldGDq0PkTS777VMrKvtwnhErlJbCAoMWR1zYZ6L08mKEB8bDVgh98W7-T9rmXvqs/s320/IMG_20110820_214709.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5643167979130411298" /></a>
<br />Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-46136313751776580282011-08-20T12:21:00.000-07:002011-08-20T12:36:27.058-07:00Subscribing to a twitter feed in Google readerGoogle 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:
<br />
<br /><code>http://api.twitter.com/1/statuses/user_timeline/[username].atom</code>
<br />
<br />where you substitute [username] for the actual usename. For example, to subscribe in google reader to Facebook's twitter feed (<a href="http://twitter.com/#!/facebook">http://twitter.com/#!/facebook</a>) you click on 'Add a subscription' in reader.google.com and paste in:
<br />
<br /><code>http://api.twitter.com/1/statuses/user_timeline/facebook.atom</code>
<br />
<br />There is more documentation on this API at <a href="https://dev.twitter.com/docs/api/1/get/statuses/user_timeline">https://dev.twitter.com/docs/api/1/get/statuses/user_timeline</a>. 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.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com2tag:blogger.com,1999:blog-3338128750432104713.post-29386453413718887832011-08-20T09:22:00.000-07:002011-08-20T09:29:36.821-07:00Plotting the ratio of two stocksI found a website, stockcharts.com, that let's you plot the ratio of two stocks. In the 'symbol' field, you specify the two stocks separated by a ':'. For example, "goog:$SPX" to plot the ratio of Google and the S&P 500. As a side note, there is a link partway down, 'linkable version' that let's you bookmark the chart. A recent one I was interested in:
<br />
<br /> <a href="http://stockcharts.com/h-sc/ui?s=GOOG:$SPX&p=D&yr=1&mn=0&dy=0&id=p20725672205">GOOG versus S&P 500 over the past year</a>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-24858481935011939262011-03-07T19:24:00.000-08:002011-07-26T21:28:49.419-07:00Hot-water shower in my car for after surfingOne of the pain points to surfing in the winter is showering off in the ice-cold showers that are common in beach parking lots. So I built a hot-water shower contraption that I can load in the trunk of my car to provide a hot shower after surfing.<p><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzU_spYpVnvsW9xt5AVG-v0Qj2s0mwt_1R6z07pnbztzbTqn6bgUIy3yxUwPXPwP2acqNVpFonJFEU1lhbbWIC-ep2b83qV3PkUhUuWc1bL9zy98Y1e2Abb1jn8FukCh-8DnMB7Ue2_wM/s1600/193332_10150440079500157_753470156_17638980_818391_o.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 200px; height: 150px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzU_spYpVnvsW9xt5AVG-v0Qj2s0mwt_1R6z07pnbztzbTqn6bgUIy3yxUwPXPwP2acqNVpFonJFEU1lhbbWIC-ep2b83qV3PkUhUuWc1bL9zy98Y1e2Abb1jn8FukCh-8DnMB7Ue2_wM/s200/193332_10150440079500157_753470156_17638980_818391_o.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5581546016219479330" /></a><br /><p>Basically, I bought a 12V water pump and attached it to a cigarette lighter power plug. I use the pump to pump water from a 5 gallon storage tank to a hand-held shower head. The idea is that I fill the tank with hot water from my bathtub before I head out surfing, I put the tank and pump in my car trunk, and, after I'm done surfing, I plug in the pump and shower off with the water from the tank. By the time I'm done surfing, the water should have cooled to a pleasant temperature.<br /><br /><p>I've tested the electrical work and full system test to come soon!<br />Here's a pic of the work area while I was putting it together:<br /><p><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_PX0fDFb4Vt3ynrk-lNhuhiTcDrMSbFowbZxEun5mn3QOB2L1BsjZc2QxBdllcANauwbQkoRP9iiWPyfn33TLvTCtUgkc_w4j7b57y-n8s5NLN0EUiSJUh9ogKTSMyuUEtZ9FqLwnlZ8/s1600/194226_10150440022370157_753470156_17638291_3176224_o.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 200px; height: 150px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_PX0fDFb4Vt3ynrk-lNhuhiTcDrMSbFowbZxEun5mn3QOB2L1BsjZc2QxBdllcANauwbQkoRP9iiWPyfn33TLvTCtUgkc_w4j7b57y-n8s5NLN0EUiSJUh9ogKTSMyuUEtZ9FqLwnlZ8/s200/194226_10150440022370157_753470156_17638291_3176224_o.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5581546874261804098" /></a><br /><br /><p><b>Update 3/12/2011</b>: <br />Surf shower v1.5. The water system test was a success! The pump has quick-connect plugs to which you attach the hoses. Mechanically, they are rather fragile. To protect the quick connects, I've mounted the pump inside a rubbermaid tub and used electric conduit clamps padded with a bit of neoprene to guide the hoses so that the hoses can flex and move around without straining the connectors. I also mounted a lighted power switch on the other side of the tub. I used a copy of the disappointing 'The World is Flat' book as a backstop for the drill and cutting board for the neoprene.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhw4_xv-7RILaxYm4lLeVS3H6IWldpj9YqKZMvo2NxDq68rhkzWf5EAmPGdC3pStVQZDdQ29RdO7qcpjpp6YlGfX4ySgg7Iavscx07Ab3w8mHVE2qL6TDr_5fzH-5U0Cj4JDB4X5wu2S-s/s1600/IMG_20110312_155949.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 200px; height: 150px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhw4_xv-7RILaxYm4lLeVS3H6IWldpj9YqKZMvo2NxDq68rhkzWf5EAmPGdC3pStVQZDdQ29RdO7qcpjpp6YlGfX4ySgg7Iavscx07Ab3w8mHVE2qL6TDr_5fzH-5U0Cj4JDB4X5wu2S-s/s200/IMG_20110312_155949.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5583349289402280034" /></a><br /><br /><br /><p><b>Update 7/12/2011</b>: <br />I've been using the shower regularly after surfing and it works wonderfully. The 5-gallon tank has turned out to be a good size. Most full-flow shower heads flow at a rate of 2 to 2.5 gallons-per-minute. So a 5-gallon tank (which weighs 40 pounds, when full) will last a bit over 2 minutes of continuous usage. I find that by turning off the water switch on the shower head while I soap, I end up using about 3 to 4 gallons of water, and usually have plenty left over at the end.<br /><br /><p>Here's the final setup:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdZ9j2MyYPvPVn15E4ST217eY4VHp7mwtPSoQP4RjEjRCkzKXdHFNAzg6RkFz-66jAQelpyYXrOHogL8wuqmh2kDzp6JY_vQFi8qnmPcU5LWeR3qHMTlPdKvVWNZ9qdQFEp6pDHKAzXws/s1600/IMG_20110726_212109.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdZ9j2MyYPvPVn15E4ST217eY4VHp7mwtPSoQP4RjEjRCkzKXdHFNAzg6RkFz-66jAQelpyYXrOHogL8wuqmh2kDzp6JY_vQFi8qnmPcU5LWeR3qHMTlPdKvVWNZ9qdQFEp6pDHKAzXws/s320/IMG_20110726_212109.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5633883643124055378" /></a><br /><br />The water flows out of the resevoir, through the strainer (the black circular device attached to the tube), to the pump, then from the pump to the hand-held shower head. There is a lighted rocker switch mounted on the right side of the tub.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com3tag:blogger.com,1999:blog-3338128750432104713.post-62894307035720464222010-12-24T13:32:00.000-08:002010-12-24T13:59:20.224-08:00Making slipper-socks from socksI decided to try my hand at making slipper socks. I wasn't satisfied with just wearing through athletic socks around the house and thought slippers were too hot and didn't cover the ankles. To make slipper socks:<br /><ol><br /><li>Start with a <a href="https://www.smartwool.com/phd/">choice sock</a><br /><li>Put them on and trace out the outline of your feet on a piece of cardboard.<br /><li>Cut out the shapes<br /><li>Use the outlines to cut out a piece of fabric that has non-slip surface on one side (got it from a fabric store)<br /><li>Put the cardboard into the sock to hold the shape, and then glue the fabric to the sock, using fabric glue. <br /><li>For good measure, I then added a felling stitch around the edges to help hold the fabric on in case the glue doesn't hold.<br /></ol><br />And the results:<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJsRE-HYMLRl7dwc242HQDRLbcHvrSSijtznIlNNB4wRnlrsgbRsPICDkaDtNpu9nhwLaTNuUw0sfp6vVlrVzXgW-CRV9HZluEDPfPZ0yvd5kDCFBa7TWzu7OAGZ6OJSwfwaH2tL8JFM0/s1600/IMG_20101224_125927.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 200px; height: 150px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJsRE-HYMLRl7dwc242HQDRLbcHvrSSijtznIlNNB4wRnlrsgbRsPICDkaDtNpu9nhwLaTNuUw0sfp6vVlrVzXgW-CRV9HZluEDPfPZ0yvd5kDCFBa7TWzu7OAGZ6OJSwfwaH2tL8JFM0/s200/IMG_20101224_125927.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5554367843508365170" /></a><br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipnvViuQGqJzymCcltQ_URgxr_TAsfdOZIqxVN-_JXlmrUsa9vSW-tJ-3Dq5FaJzSJcayW5VR9BF5EQbPntECSGdPHm0tD245_GlI1Qa1V9LNFO8LElY3k7umxvYpv7MEbJbBWMaUjGVk/s1600/IMG_20101224_130022.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 200px; height: 150px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipnvViuQGqJzymCcltQ_URgxr_TAsfdOZIqxVN-_JXlmrUsa9vSW-tJ-3Dq5FaJzSJcayW5VR9BF5EQbPntECSGdPHm0tD245_GlI1Qa1V9LNFO8LElY3k7umxvYpv7MEbJbBWMaUjGVk/s200/IMG_20101224_130022.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5554368055956263826" /></a><br /><br />Quiet an exceptional piece of work, if I do say so myself :). One refinement I want to try next time around: When glueing the fabric to the sock, rather than pressing it against the sock with the cardboard inside, instead press it against the sock with your foot inside. I introduced a small registration problem with I pressed the fabric - I didn't quite center it to where my foot naturally rests in the sock. It's possible you could get away without using the cardboard at all.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-60815803682746745762010-12-16T19:39:00.001-08:002010-12-17T17:51:53.268-08:00Full custom neoprene jacket for glass water bottle<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIkjMZPIKfGajL5yzGQJ-ghsZUPVC2MCvy-_PZX6VKbFm_mZXINGeLWCdc_77XkVB8VFvg3XC-DlQ6IwIkW2N4LF7exsH_BG1cCpIXUXZyNRjcjpmepiDQS4auoaSJZPddi4Ixrf6Pw0w/s1600/P1000479.JPG"><img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 150px; height: 200px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIkjMZPIKfGajL5yzGQJ-ghsZUPVC2MCvy-_PZX6VKbFm_mZXINGeLWCdc_77XkVB8VFvg3XC-DlQ6IwIkW2N4LF7exsH_BG1cCpIXUXZyNRjcjpmepiDQS4auoaSJZPddi4Ixrf6Pw0w/s200/P1000479.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5551491342218801666" /></a><br /><br /><br />On vacation with some time on my hands, and figured what better time to construct a protective jacket for the perfect water bottle (<a href="http://joshuaredstone.blogspot.com/2010/11/candidate-for-best-water-bottle.html">see my post earlier on the bottle</a>). Pictured here is the result of a sheet of neoprene laminated with a cloth-like layer, 8 coat hooks, 2 yards of 1mm yellow line and a few hours sewing sprinkled with the occasional curse.<br /><br />The intent is that the neoprene jacket will cushion the bottle if it falls on a hard surface so that it hopefully won't break. The important areas protected are the corners because they are what is likely to hit a hard surface if the bottle falls on the ground.<br /><br />The reason for the separate pieces and line connecting is so that it is easy to remove and clean, and also so that you can see the contents of the bottle.<br /><br />Update: As an alternative, I also tried a rough cut at a version using eyelets instead of stitching. In the image below, I haven't glued the sides or anything, so it appears a bit flappy. I like having a lower-profile to the protective case, but th eyelets pop out too easily. Also, not sure how the glue will work to glue the edges of each piece together...<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjG6xlYf53f53AKGuEnGCrV24jK_p1IY6fqQ8CqnYEa7_KJj5JMt83t2MI-UWY7Dw1ztUG-lU3BT34SxTtRyKTUtR3J6uUFstJgwzHfe3wit65dck0TnoRCX8ILyruAnVFMnbB0Ve7HuE/s1600/IMG_20101217_203817.jpg"><img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 150px; height: 200px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjG6xlYf53f53AKGuEnGCrV24jK_p1IY6fqQ8CqnYEa7_KJj5JMt83t2MI-UWY7Dw1ztUG-lU3BT34SxTtRyKTUtR3J6uUFstJgwzHfe3wit65dck0TnoRCX8ILyruAnVFMnbB0Ve7HuE/s200/IMG_20101217_203817.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5551833498957394930" /></a>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com1tag:blogger.com,1999:blog-3338128750432104713.post-46019349940371732382010-11-21T12:43:00.001-08:002010-11-21T13:23:02.183-08:00A candidate for best water bottle<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCgKbZzyFqRkhbghO-XpkYIITQn88GEPIEQM75vOjO8yhyddY-h4fT3tbkrGutJ0RV67ImuUJcRW5ydIT_UnG0dLevHoOATVDW6_-k_qEg0LwTp48lqPPCFucO9wYe_p8IGV9xi7k8Dm8/s1600/bottle4.jpg"><img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 59px; height: 100px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCgKbZzyFqRkhbghO-XpkYIITQn88GEPIEQM75vOjO8yhyddY-h4fT3tbkrGutJ0RV67ImuUJcRW5ydIT_UnG0dLevHoOATVDW6_-k_qEg0LwTp48lqPPCFucO9wYe_p8IGV9xi7k8Dm8/s200/bottle4.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5542106525841795490" /></a><br />My quest to find the best bottle to drink water out of has ended, at least for now. Read on... results at the bottom.<br /><br />Background:<br /><br />I used to drink water out in disposable plastic bottles. Then I started reusing the plastic bottles till I heard that disposable plastic bottles leach chemicals into the water and that reusing them is even worse. I switched to plastic bottles (e.g., Nalgene), then heard about the BPA issues. I started doing research and it seems like the next best thing was stainless steel bottles like <a href="http://www.kleankanteen.com/products/classic/klean-kanteen-27oz-classic.php">Kleen Kanteen</a>. Stainless steel doesn't leach plastic chemicals, though it does affect the taste of the water a bit. Kleen Kanteen is made out of food-grade stainless steel.<br /><br />To take it to the next level, there's a stainless-steel bottle originally made by <a href="http://www.guyotdesigns.com/product.php?id_product=88">Guyot</a> out of surgical-grade stainless-steel that I also started using. Surgical-grade sounds like it just has to be better than food-grade, doesn't it? I couldn't help myself. I think technically, the main difference between the two is that surgical-grade steel (Guyot is 18/10, also known as 316 stainless steel) is a bit more resistant to corrosion in salt-water. Yeah, I know that we're not using it to store ocean water, but it's the principle, right? Maybe there's a couple of grains of salt in your water on occasion... On the other hand, there is a concern that the nickel in the stainless steel isn't good for you.<br /><br />So what's the ideal bottle that doesn't leach chemicals or interact with the water in any way? The gold standard for this is glass. Turns out there are some consumer glass water bottle makers, like <a href="http://www.lifefactory.com/products-beveragebottles">Lifefactory</a> and <a href="http://www.takeyausa.com/products/glass-water-bottles/classic-glass-water-bottle-22oz.html">Takeya</a>. I had two issues with these bottles. First, the largest size is 22oz, which is a bit small for me. Second, the bottles apparently may break if you put boiling water into them. This is an issue because I use boiling water to clean my stainless steel water bottles. I like cleaning with boiling water because you don't have to worry about soap residue taste and you know any bad stuff is killed.<br /><br />There's gotta be a better bottle! Turns out that there is, though it's not for consumers. Remember high-school chemistry class? Glass is often used to contain chemicals. If you look at chemistry supply websites, there is a class of glassware called "media storage bottles". These bottles are intended to store potentially nasty chemicals, so the ability to store the chemical without leaching junk from the bottle is a priority. I looked around for the best media bottle. Some makers are <a href="http://www.corning.com/lifesciences/us_canada/en/index.aspx">Corning</a>, <a href="http://www.us.schott.com/labware/english/products/duran/index.html">Schott/Duran</a> and <a href="http://www.wheatonsci.com/">Wheaton</a>.<br /><br />The best bottle I could find is.... [ drum roll please ]:<br /><br /><b><a href="http://www.duran-group.com/en/products-solutions/laboratory-glassware/products/laboratory-glass-bottles/pressure-plus-laboratory-bottle.html#c2507">Duran Pressure-Plus Laboratory Bottle</a></b><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjE9Z2DP42dT4rex2GdCGdkd2hGiSOp2r3ZRomhny4uBC9zsQuBM7SeQiLEZ4t9QH0gS_wH0ShtXAyRh0DTE2nKUIFi5oUuqO-4QsqWmZ5oX8jqxQsDhCi_c5DugqMAVazZLU2uUHvFlxk/s1600/bottle-big.jpg"><img style="float:right; margin:0 0 10px 10px;cursor:pointer; cursor:hand;width: 133px; height: 200px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjE9Z2DP42dT4rex2GdCGdkd2hGiSOp2r3ZRomhny4uBC9zsQuBM7SeQiLEZ4t9QH0gS_wH0ShtXAyRh0DTE2nKUIFi5oUuqO-4QsqWmZ5oX8jqxQsDhCi_c5DugqMAVazZLU2uUHvFlxk/s200/bottle-big.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5542106701462627474" /></a><br /><br />Cool properties of this bottle:<br /><ol><li>Made from <a href="http://www.us.schott.com/labware/english/products/duran/properties/index.html">Duran glass</a>, which I think is basically Pyrex. Check out the link for more info. It can handle boiling water (and apparently contents up to 500 degrees Celsius). It's super resistant to all kinds of chemicals and very inert. It can handle thermal shock up to 100 degrees Celsius (e.g., boiling water on one side of the glass and an ice-cube on the other). Also, the glass is pretty thick and sturdy, though I haven't dropped it yet.<br /><li>The Pressure-plus bottle in particular can handle pressure between -1 bar (a vacuum inside of the bottle) and +1.5 bar (1.5 atmospheres of pressure pushing outward). One caveat is that if there is a pressure differential between the inside/outside of the bottle, the thermal shock resistance is lower (they claim up to 30 degrees Celsius difference maximum when at maximum rated pressure).<br /></ol><br /><br />The main downside to this bottle is that it's bulky and that it it doesn't come with any consumer niceties like a holding strap, a protective silicon case or a cap. It takes a standard cap size called GL-45 - I use the cap from a runner-up bottle not described here from Corning.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com4tag:blogger.com,1999:blog-3338128750432104713.post-85496443009769158302010-05-06T11:23:00.000-07:002010-05-06T12:51:14.633-07:00Getting MAME working on Ubuntu 9.10 (Karmic)Getting Mame (the arcade game emulator) working on my Thinkpad T500 took the following steps:<br /><ol><li>Install mame and the pulse audio driver:<pre class="jcodeblock">sudo aptitude install mame libsdl1.2debian-pulseaudio</pre><br /><li>At this point, sounds was crackling, but enabling multithreading fixed that:<pre class="jcodeblock">mame -multithreading</pre><br /></ol>And that's it. I modified my /etc/sdlmame/mame.ini to enable multithreading, lower the volume, and have it run in a window rather than fullscreen so I don't need to specify those options on the command line. Try 'man mame' for explanations of other options. When starting mame, 'TAB' pops up the mame option menu including all the keyboard command mappings. 'ESC' quits the menu or the game. '1' starts game play.<br /><br />Also, Mame seems to be the best supported emulator. I tried to get a few others (nestra, fceu) working without success.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com2tag:blogger.com,1999:blog-3338128750432104713.post-66171689165073172622010-04-15T19:57:00.000-07:002010-04-15T20:01:14.239-07:00Sprint mobile broadband (Sierra 598) on Ubuntu 9.10I was pleasantly surprised to find that plugging my Sprint mobile broadband dongle (it says Sierra Wireless USB 598 on the back) into my laptop running Ubuntu 9.10 worked out of the box! After plugging in, go to the network icon in the toolbar and there appeared an entry for 'Mobile Broadband'. I checked the checkbox underneath it, which took me through an easy setup wizard (selected 'USA' and 'Sprint') and it worked! Warms my heart when Ubuntu works so well.Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com0tag:blogger.com,1999:blog-3338128750432104713.post-57673574538797094282009-12-22T20:52:00.000-08:002009-12-22T21:08:16.692-08:00Replacing power-steering vacuum suction hose on 2001 Honda CivicOn a non-computer note, I had the misfortune of not replacing the power-steering reservoir in its bracket after changing the left headlight on my civic. The hose connecting it to the power-steering pump rubber one of the belts, eventually puncturing the hose. The fluid ran out of the system and my steering started making unhappy noises. Then followed seven hours of me figuring out how to fix this on a Sunday when shops were closed. The upshot was that I learned that the name of the hose is the power-steering suction hose, and it is a part available only at the dealer (costs < $5). It is a curved, molded tube about a foot long. It looks a bit like a question mark. It is distinct from the high-pressure hose and the return hose. Here are the steps I took to replace it (after my temporary hack of duck-tape and epoxy):<br /><br /><ol><li>Turn off engine<br /><li>Suck as much fluid as you can out of the power-steering reservoir with a turkey baster (literally) or some other such device. I used a turkey baster with a straw on the end to reach a bit further in. Deposit this fluid in a container that you can bring to an auto parts shop for proper disposal.<br /><li>Take a pair of pliers, grab the spring-clamp attaching the suction tube to the reservoir and lower the clamp down the tube so it is no longer clamping the tube onto the reservoir.<br /><li>Place a receptacle underneath the hose and reservoir to catch any fluid that spills out when you remove the hose from the reservoir. I used a disposable plastic cup.<br /><li>Remove the hose from the reservoir. It comes off moderately easily. A fair bit of fluid will drain out of the reservoir.<br /><li>Place reservoir on a towel or something to catch any remaining drips.<br /><li>Empty plastic cup<br /><li>Repeat procedure for other end of hose. Less fluid will spill out this time, but it may spill on the alternator, which probably is not good.<br /><li>Remove hose from car, remove clamps from hose, and slide clamps onto replacement hose.<br /><li>Reverse detachment procedure to reattach hose to pump and reservoir and move clamps back into place clamping the hose at each end.<br /><li>Place reservoir back into holding bracket.<br /><li>Fill up reservoir to min fill line with honda-compatible power steering fluid.<br /><li>Start engine and turn off after a few seconds. The fluid level in the reservoir will have dropped a bunch.<br /><li>Add more fluid to reservoir to bring it back up to somewhere between min and max level.<br /><li>Start engine and turn steering wheel stop to stop a few times. Don't hold at a stop too long - this strains the pump. The purpose of turning wheel a lot is to work out any air bubbles from the system. I could not tell if this was effective.<br /><li>Evaluate fluid level with engine off.<br /></ol>Joshuahttp://www.blogger.com/profile/05824098668540667172noreply@blogger.com1