All posts by awknaust

Granola

Have learned a little bit about granola after making it several times. Ingredients are essentially free-form, having too much salt or too much oil are the most likely problems. Too much esp. iodized salt can really ruin an entire batch, and it need not be an excessive amount of salt to do so. Other good ideas are heating up honey in order to help remove it and to mix the liquids.

Stirring the granola in the oven is grossly overrated. This does nothing to prevent clumps because the granola is going to be moist when you remove it from the oven unless you burned it to a crisp anyway. Clumps only form after it is out of the oven! Depending on the oven, however, stirring may be useful to bake the granola evenly. In my oven the granola in the corners tends to get a lot more heat than the rest, but stirring once in the middle is usually enough to get an even bake.

Coffee Flavored Granola

  • 1200g rolled oats
  • 600g honey
  • 200g vegatable oil
  • 2 espresso shots
  • vanilla extract, salt, cloves, cocoa

I added too much salt, unfortunately, recommend under 10g for this level of oats. Not particularly sweet, and the coffee was not really noticeable.

Coconut Granola

  • 1200g oats
  • 600g honey
  • 146g veg oil
  • 12g sea salt
  • 2tbsp vanilla
  • 1tbsp cinnamon
  • 453g walnut
  • 198g coconut gratings

Baked at 300F for about 45min, stirring once. Yields 2100g granola (2 gallon ziploc bags). Has a nice pleasant flavor, coconut and walnuts and honey are noticeable but none is too strong. Coconut flavor definitely does not jump out at you. Still might be too much salt, and not enough vanilla to really taste. Cinnamon also unnoticeable.

Coconut Granola 2

  • 1200g Oats
  • 700g honey
  • 8g salt
  • 200g veg oil
  • 400g Coconut flakes
  • 24g vanilla
  • 450g walnut

Baked at 285 for about 50mins. Coconut flavor not really distinct despite doubling coconut weight. Tastes pleasantly sweet, also could use more nuts. Right amount of salt. Very uneven baking, some is pale white some caramel colored, but all equally dried out?!

Coconut Granola 3

  • 1420g Oats
  • 493g honey
  • 203g blue agave syrup
  • 198g coconut flakes
  • 250g unsweetened coconut milk (only the fat skimmed from the top)
  • 10g salt (dissolved in water)
  • 110g veg. oil
  • 50g vanilla

Baked 50min at 285F. Agave syrup tastes like butter, but not so noticeable in end-product. Coconut flavor also subtle despite using coconut fat. Vanilla also subtle, otherwise a middling batch.

 

Vanilla Nut Granola

Trying to get some serious vanilla flavor and taking advantage of an abundant nut supply.

  • 2200g oats
  • 550g walnuts
  • 550g almonds
  • 340g agave
  • 800g honey
  • 350g veg. oil
  • 120g vanilla

Baked at 275F for about an hour for each sheet (6 sheets). Found rotating them in the oven helps a lot to keep them evenly cooked. But the vanilla doesn’t really shine through. We may need to try vanilla bean or some other kind of vanilla to make it work.

ATH AD-900 Color Mod

I’ve decided to spiff my Audio Technica AD900s up with some new color. Unfortunately there are few sources about taking these headphones apart, and what their internal layout is. So I’m giving a walkthrough on disassembling the AD900s, which turned out to be very easy, and how to modify the color of the metal mesh that hopefully looks kinda cool!

When pulling the pads off the ear pieces it seems like they will never come back on, but this is not the case.

The speaker mount is attached by four finely threaded phillips head screws to the body of the headphones, and requires a little force to be pried free. Once you’ve got it off, it remains wired to the rest of the body, but the metal mesh is easily removed. The foam backing (taking this off can alter the sound) is affixed with some adhesive behind the “audio-technica” sticker, but is easy to remove.

Overall it is very easy to take apart and put back together! To paint the mesh, I also removed the “audio-technica” logo which seems to be some sort of metal stuck to the mesh with a glue strip, which you can get off with a small screwdriver.

Building CyanogenMod without the device

Lately I’ve been working on a research project concerning android’s dalvik vm. Unfortunately, to compile cyanogenmod (at least for the Nexus 7), some proprietary drivers from the device are required (i.e. via ./extract-files.sh in the build guide for grouper ).

Now it may be inconvenient for you to actually attach the device (like if you are using a build server). So how to get the drivers? Surprisingly google didn’t offer any hints, but there is a straightforward way to do it… extract them from a cyanogenmod nightly build!

i.e. http://download.cyanogenmod.com/?device=grouper&type=nightly

To make it even easier, here is a modified extract-files.sh that pulls them from the extracted grouper build of your choice

 

# A hack to pull the binary files from a pre-existing build
# of this cyanogenmod (i.e. a nightly)
# Alex Knaust 2013-05-02

DEVICE=grouper
MANUFACTURER=asus

# Path to the unzipped nightly with the correct blobs
NIGHTLY=~/grouper-nightly

BASE=../../../vendor/$MANUFACTURER/$DEVICE/proprietary

mkdir -p $BASE
for FILE in `cat proprietary-blobs.txt | grep -v "^#"`; do
    cp -v $NIGHTLY/$FILE $BASE/
done

./setup-makefiles.sh

Codebook

Lately I’ve been doing a lot of coding competitions, google code jam, Project Euler, and at my university (UTEP). I’m starting a coding book, which is a collection of commonly used simple implementations of algorithms that are useful in coding competitions. Hopefully this can be a collaborative effort with the UTEP team.

Others are free to use it and contribute!

https://github.com/awknaust/codebook

Subsample directories of files

This script will allow you to trim a folder(s) full of files to a fixed number of files, sampling randomly. My use case was to select a subsample of training images for CV research, but it could be used for whatever you like!

#!/usr/bin/python
# Programmed by Alex Knaust 2013-03-12

import os, sys, random


def sampleFiles(k, dirs):
	'''Deletes a random sample of files, leaving k remaining
	from each directory. The directories must have identical filenames
	and identical filecounts, otherwise we are in trouble
	'''
	totalFiles = len(os.listdir(dirs[0]))
	if k >= totalFiles:
		print 'Not enough files to sample from'
		exit(1)
	
	# python magic at work
	stuff = [sorted([os.path.join(d, p) for p in os.listdir(d)]) for d in dirs]
	destroy = random.sample(zip(*stuff), totalFiles - k)
	
	return destroy
		

if __name__ == '__main__':
	if len(sys.argv) < 3:
		print('Pass number of files to sample, and directories to sample from')
		sys.exit(1)
	 
	k = int(sys.argv[1])
	dirs = sys.argv[2:]
	destroy = sampleFiles(k, dirs)
	
	ans = raw_input('Will delete %d files, OK? y/n : ' % (len(destroy) * len(dirs)))
	if ans[0].lower() == 'n':
		print 'Goodbye'
		sys.exit(1)
	
	#do the actual deletion
	for files in destroy:
		for f in files:
			os.remove(f) 

CMoy Amp

My good friend (who is an electrical engineer) and I decided to make a pair of Cmoy amps for headphones. I have a pair of Audio Technica ATH-900s which I feel are sometimes not sufficiently served by most devices. We followed the plans by tangentsoft for the most part. The site is amazing and all of the information is well written and curated.

Unfortunately the design provided there doesn’t cover the power supply, it assumes you use 9V batteries. My brief readings indicate that these don’t last particularly long, and I was most interested in using my amp with a computer : no need for portability. We created our own power supply by buying an unregulated 18V wall-wart (not switching). Our circuit to reduce the noise and regulate the input was provided by the voltage regulator datasheet, however we found it necessary to add a large cap (1000\muF) to the input to reduce the AC wave.

Overall, the amp does what I hoped it to do; it gives sufficient volume on almost any device. A lot of audiophile people comment on the change in sound provided, but I don’t really feel comfortable doing so. It sounds fine, maybe the bass is a little punchier.

Tracking Dynamic IP

So it is nice to be able to remotely connect to your home computer. Many (included my) ISPs, give dynamic IP addresses, and although these don’t change often, they do change and are a pain to remember anyway. My solution is to have my computer e-mail me whenever it discovers it has a new IP address! You can easily configure exim4 to send emails from a linux machine through gmail, a great guide is here.

Once you have that you just need a little script, mine does this with python by asking http://icanhazip.com. and writing a temporary file with the old ip in /var/tmp. It only sends you a message when the IP changes, no need to spam your email box!

 

#!/usr/bin/python

''' A super simple script to email changes in a dynamic IP to
an email address. If you drop this in /etc/cron.* , it will periodically
poll a website to check if your iP has changed, and if it has it well
send you an e-mail with the new one!

Alex Knaust - 2/28/2013
'''

import os, smtplib, urllib2, re
from email.mime.text import MIMEText

# file to save the last IP address in
IPLOC = '/var/tmp/ip'

# site from which to get IP, if you change this you might need to alter get_live_ip
IPSITE = 'http://icanhazip.com'

# Message to send yourself with the IP
MAILSTR = '''Hi, My new IP address is {0}\n'''

# Address from which the email is being sent (this should probably match your SMTP setup
FROMADDR = 'you@host.com'

# Address to which to send the message
TOADDR = 'you@host.com'


#####################################################
############### CODE ################################
#####################################################
def save_ip(ipstr):
    '''Save the Ip to IPLOC, return true if successful, false otherwise'''
    try:
        with open(IPLOC, 'w') as f:
            f.write(ipstr)
    except:
        return False
    return True
    
def get_old_ip():
    '''Gets the old IP by reading IPLOC, returns None if it could not be read'''
    try:
        with open(IPLOC, 'r') as f:
            ip = f.read()
        return ip.strip()
    except IOError:
        return None
    
def get_live_ip():
    '''Return the IP address of this computer by polling IPSITE
    
    returns None if not successful
    '''
    try:
        f = urllib2.urlopen(IPSITE)
        return f.read().strip()
    except urllib2.URLError:
        return None

def send_mail(ipstr):
    '''Tries to send an email about the change of IP to the TOADDR
    
    returns true if successful, false otherwise
    '''
    msg = MIMEText(MAILSTR.format(ipstr))
    msg['Subject'] = 'IP Address change'
    msg['From'] = FROMADDR
    msg['To'] = TOADDR
    
    try:
        s = smtplib.SMTP('localhost')
        s.sendmail(FROMADDR, [TOADDR, ], msg.as_string())
        s.quit()
    except:
        return False
    return True

def main():
    oldip = get_old_ip()
    newip = get_live_ip()
    
    if newip is not None:
        if oldip is not None:
            if newip != oldip:
                send_mail(newip)
                save_ip(newip)
        else:
            send_mail(newip)
            save_ip(newip)

if __name__ == '__main__':
    main()

Building a Desk

My previous desk setup was pretty poor, the chair was one of those leather coated ones that look like they are from an era which never existed. Besides that it had lost 3 of its wheels, and the gas cylinder had kicked the bucket, and was replaced by a piece of PVC pipe. My desk, was actually a glass kitchen table, which went up to about knee height, and thus was standing on two flower pots. Getting the right work environment actually does make a big difference in comfort and ultimately productivity, especially considering if you are like me you spend almost your entire day at one desk or another. I noticed this at UTEP, where my office has some ergonomically designed desk and expensive Herman Miller chair. It is just easier to type. Building a new desk chair is a bit beyond me, plus I had fallen in love with the Mirra chair from work, so I received one of those for Christmas. The remaining task then is to obtain a new desk. Shopping for desks sucks. No one seriously wants a desk with drawers and 2 ft of leg room, yet somehow these seem to go for upwards of $200. Your other option is to buy a folding table, not exactly a dream arrangement either (at least they are cheap). My option is to build one :) Requirements

  • Height, my googling suggests that at rest the elbow angle should be slightly more than 90 degrees. This depends on the chair, and the length of your legs, for me ~25″
  • Width, desks are all way too narrow, I think having room for multiple objects on your desk is not unreasonable, two screens, speakers : mine is 5′
  • Finish, this was the most difficult part to decide, the finish should make the table waterproof/scratchproof but epoxy tops and glass can be very expensive
  • Depth, enough for the screens, and room to work on paper in front of them ~ 28″
  • Detachable Top, essentially if you glue everything together you will never be able to transport the table

Design I’ll discuss the general joinery. I looked up some other ideas on how to make tables/work benches online. There are tons of different ways to do this, one of the easiest is to make a skirt (horizontal beams connecting the legs) and use 4 legs. This is very sturdy and simple, but it means an awkward way to put your legs under the table. Another option is to have essentially two legs which are joined in the middle by only one beam, less support, but more leg room. I chose option c), a 3 sided skirt, with the one in the front missing. Easy to do, with plenty of leg space, you only make it structurally unstable to side-to-side motion. Materials Good wood is hard to find and very expensive, so compromises are made

  1. 2 sheets of 3/4″ oak veneer plywood (tabletop)
  2. 3/4″x 1 1/2″ red oak (trimming)
  3. 4×4″ redwood (legs)
  4. 2×6″ redwood (skirt)
  5. Cherry stain, Mineral spirits and Satin polyurethane

Photos!

The skirt legs are done with a half-open mortar tenon joint. I drilled the holes first with a forstner bit, and then go back with chisels.
The skirt legs are done with a half-open mortar tenon joint. I drilled the holes first with a forstner bit, and then go back with chisels.
Table top two pieces of plywood joined together, and trimmed with a router. I used a friend's table saw to cut this, the nice thing is with a trim-bit on your router you don't have to worry about matching the sizes exactly.
Table top two pieces of plywood joined together, and trimmed with a router. I used a friend’s table saw to cut this, the nice thing is with a trim-bit on your router you don’t have to worry about matching the sizes exactly. I used cross-beams to clamp them together, but alas you always need more clamps and there is a small gap in some places.
The trimmings are joined to the edges of the tabletop
The trimmings are joined to the edges of the tabletop. I cut off the extra with a miter saw, and then a chisel. I bought some pipes for my pipeclamps and used several ratcheting band-clamp like things to glue this together.
This is the assembled and glued leg fixture
This is the assembled and glued leg fixture. Used belts and extra wood to make sure the joints were square when clamping
The top will be attached with screws into a few oak blocks
The top will be attached with screws into a few oak blocks, lining them up is a pain. I glued the blocks to the skirt, because it seemed convenient, but they could have just been drilled to both the bottom and the top
A look at the bottom of the tabletop before staining. The trimmings have been planed to be flat with the surface, and then rounded with a router. I have the wholes predrilled on the bottom with screws so that I can stain/finish both sides simultaneously.
A look at the bottom of the tabletop before staining. The trimmings have been hand-planed to be flat with the surface, and then rounded with a router. I have the wholes pre-drilled on the bottom with screws so that I can stain/finish both sides simultaneously.
The legs, ready for staining. What we don't see is the hours of sanding I did on everything...
The legs, ready for staining. What we don’t see is the hours of sanding I did on everything…
After a few coats of wipe-on poly. There are 9 coats total. The staining wasn't done very well, and it became very uneven and mixed with the first two coats of poly
After a few coats of wipe-on poly. There are 9 coats total. The staining wasn’t done very well, and it became very uneven and mixed with the first two coats of poly
Of course the stain color is different on two diff. woods. I only sand every 3 coats. Unfortunately there is a lot of dust around
Of course the stain color is different on two diff. woods. I only sand every 3 coats. Unfortunately there is a lot of dust around
My final desk setup. There are dust bits in the last part of the finish, but it is very waterproof, and I think it looks pretty good. And needless to say the dimensions are perfect
My final desk setup. There are dust bits in the last part of the finish, but it is very waterproof, and I think it looks pretty good. And needless to say the dimensions are perfect

Tips/Mistakes

  • Unfortunately I have no pictures, but this involved a lot of very creative clamping because I just don’t have the right ones. Clamps can be extended by using leather belts, racheting tie downs can be used as band clamps, etc.
  • Let the stain dry completely before finishing. Finishing takes forever but you need to be patient
  • Had never actually used a hand-plane before, but it made quick work of the trimming. Unfortunately it also makes quick work of the veneer!

    Welded together when still in the table :(
  • Redwood has tons of knots, I filled mine with wood putty, but they are not very pretty
  • The screws were a total mess, the alignment somehow shifted and they made their own holes, and then I stripped one of them and ended up having to weld a nail onto it to remove it (don’t try this at home)

Yet another backup script

I’m publishing a simple script I wrote a year or so ago to do automatic mysql backups with 7zip on a linux server. It is inspired by the original mysqlbackup script, in that it can rotate the databases on a daily/weekly etc. basis, but it also provides stuff like exporting to an ftp server, or simpler backups. It isn’t completely polished, so any forks/tips are welcome!

I’ve made a permanent page here with a download link and some more about it.

#!/usr/bin/python

#################################
# created by Alex Knaust 06/2011
#################################

# mysql login credentials
MYSQL_USERNAME = ''
MYSQL_PASSWORD = ''
MYSQL_HOST = 'localhost'
MYSQL_PORT = 3306

# directory that directories (daily, weekly, monthly, etc.) will be created in
BACKUP_DIR = '/backup/mysql'

# filename pattern to archive files as, will be formatted with datetime.strftime
FILENAME_PATTERN='%d-%m-%Y_database-dump.7z'

# arguments for the p7zip program
P7ZIP_ARGS = '-t7z -bd -m0=LZMA2 -mx=9'

#file to write the intermediate dump to
TMPFILE = '/var/tmp/dbdump.sql'

# arguments for the mysqldump command (specify which databases here)
MYSQLDUMP_ARGS = '--all-databases --force'

# octal permissions for the database dump
PERMS = 0o640



###############################################################################
# DO NOT EDIT BELOW HERE
###############################################################################
from datetime import datetime, timedelta
import shutil, os, subprocess
from ftplib import FTP
from syslog import syslog

def dumpmysqldb(user, password, port, host, args='--all-databases --force', 
            filename=TMPFILE):
    '''Runs mysqldump to backup all databases to filename, args are passed to mysqldump
        raises an Exception if the filename does not exist afterwards
    '''
    
    # set permissions of PERMS file to be written to
    with open(filename, 'w') as f:
        pass
    os.chmod(filename, PERMS)
    
    mysqldumpcommand = '''mysqldump --user="{user}" --password="{passwd}" --host={host} --port={port} {args} > {output}'''.format(
        user=user, passwd=password, output=filename, args= args, host=host, port=port)
    
    pipe = subprocess.Popen(mysqldumpcommand, shell=True, stdout=subprocess.PIPE)
    pipe.communicate()
    
    if not os.path.isfile(filename):
        syslog("{0} doesnt seem to exist although it should".format(filename))
        raise Exception("{0} doesnt seem to exist, although it should".format(filename))


def compress_7z(path, args='-bd', outfile=None):
    '''compresses a file with p7zip, outputting as .7z file, returns the name of the
        compressed file
    '''
    if not outfile:
        outfile = os.path.join(os.path.split(path)[0], 'dump.7z')
        
    p7zipcommand = '''7z a {args} "{outfile}" "{filename}"'''.format(
            args=args, filename=path, outfile=outfile)
    pipe2 = subprocess.Popen(p7zipcommand, shell=True, stdout=subprocess.PIPE)
    pipe2.communicate()
    
    if not os.path.isfile(outfile):
        syslog("{0} was not created correctly".format(outfile))
        raise Exception("{0} was not created correctly".format(outfile))
    else:
	os.chmod(outfile, PERMS)
        return outfile


class BackupUpdater:
    '''Class that handles deleting old files'''
    def __init__(self):
        raise NotImplementedError
    
    def update(self, time, file):
        '''This will be called when a new dump is created'''
        raise NotImplementedError

    
class BasicBackupUpdater(BackupUpdater):
    '''Just saves the files in the directory given'''
    def __init__(self, directory, fileformat='%d-%m-%Y_database-dump'):
        self.directory = directory
        self.fileformat = fileformat
        if not os.path.isdir(self.directory):
            os.makedirs(self.directory)
        
    def update(self, time, file):
        newfilename = time.strftime(self.fileformat)
        shutil.move(file, os.path.join(self.directory, newfilename))


class FTPBasicBackupUpdater(BackupUpdater):
    '''Saves the file to a directory on an FTP Server'''
    def __init__(self, directory, user, passwd, host, fileformat='%d-%m-%Y_database-dump'):
        self.fileformat = fileformat
        self.FTP = FTP(host=host, user=user, passwd=passwd)
        self.ftp.cwd(directory)
        
    def update(self, time, file):
        newfilename = time.strftime(self.fileformat)
        with open(newfilename, 'rb') as tfile:
            self.FTP.storbinary("STOR " + newfilename, tfile)
        ftp.quit()

    
class AdvancedBackupUpdater(BasicBackupUpdater):
    '''Works by rotating the backups into a folder hierarchy based on their
    creation time, i.e. after 1 week the daily folder will be emptied and the
    oldest file will be moved to weekly, and the newest file will now be in daily
    , does the same for weekly, monthly, yearly.
    '''
    folders = (
            ('daily', timedelta(weeks=1)),
            ('weekly', timedelta(weeks=5)),
            ('monthly', timedelta(weeks=52)),
            ('yearly', timedelta.max), #if this program works for more than 2 million years, we're in trouble
        )
        
            
    def __init__(self, directory, fileformat):
        BasicBackupUpdater.__init__(self, directory, fileformat)


    def _getAge(self, directory):
        '''Returns a datetime object representing the oldest creation date in a directory'''
        filelist = [os.path.join(directory, f) for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
        if filelist:
            filelist.sort(key= lambda f: os.path.getmtime(f))
            return datetime.fromtimestamp(os.path.getmtime((filelist[0]))), filelist[0]
        else: return datetime.now(), None


    def _updatePushing(self, time, filename, index=0):
            folder = os.path.join(self.directory, self.folders[index][0])
            delta = self.folders[index][1]
            
            if not os.path.isdir(folder):
                os.makedirs(folder)
            oldesttime, oldestfile = self._getAge(folder)
            
            #if its time to rotate, push them up with a recursive call
            if time - oldesttime > delta:
                self._updatePushing(oldesttime, os.path.join(folder, oldestfile), index+1)
                for file in [os.path.join(folder, f) for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f))]:
                    os.remove(file)
            try:
                shutil.move(filename, folder)
            except shutil.Error:
                syslog("Error : file {0} already exists".format(os.path.join(folder, filename)))
                print "Error : file {0} already exists".format(os.path.join(folder, filename))
                pass #probably already existed
    
    
    def update(self, time, filename):
        nf = os.path.join(os.path.split(filename)[0], 
            time.strftime(self.fileformat))
        shutil.move(filename, nf)
        self._updatePushing(time, nf, index=0)
        
        
if __name__=='__main__':
    print 'Dumping databases...'
    syslog('Dumping databases...')
    dumpmysqldb(user=MYSQL_USERNAME, password=MYSQL_PASSWORD,
             host=MYSQL_HOST, args=MYSQLDUMP_ARGS, port=MYSQL_PORT, filename=TMPFILE)
    
    print 'Compressing {0} with 7z...'.format(TMPFILE)
    syslog('Compressing {0} with 7z...'.format(TMPFILE))
    cmpfile = compress_7z(TMPFILE, args=P7ZIP_ARGS)
    
    print 'Updating backups with {0} in {1}'.format(cmpfile, BACKUP_DIR)
    syslog('Updating backups with {0} in {1}'.format(cmpfile, BACKUP_DIR))
    b = AdvancedBackupUpdater(BACKUP_DIR, FILENAME_PATTERN)
    b.update(datetime.now(), cmpfile)
    
    print'Success'