Tags: code

RSS - Atom - Subscribe via email

Code and circuit for a six-function Arduino-based USB footswitch

Posted: - Modified: | geek, sketches

image

I’d been thinking about footswitches for a while, but I held off on buying one because they were expensive. Turns out that building your own is easy, even for someone with limited electronics experience. I do have the unfair advantage of having a spouse who’s an electrical engineer, but I figured out this circuit and code by myself!

The hardware for this circuit is really simple. If you’re lucky, you might find a three-way foot switch at your local audio-equipment-carrying surplus shop. If not, you could make your own, but I haven’t tried doing that yet. =)

The fun part is in the code that makes this a six-function USB keyboard. The code below maps left, center, and right short presses to F13, F14, and F15, while left, center, and right long presses send F16, F17, and F18. Here’s the code:

const int redPin = 9;
const int tanPin = 10;
const int bluePin = 11;
const int orangePin = 12;
const int debounceDelay = 150;
const int longPressThreshold = 650;

int currentState;
int lastSwitch;
long lastDebounce;
long lastPressed;
int lastSwitchDebounced;

uint8_t buf[8] = { 0 };	/* Keyboard report buffer */

#define SWITCH_NONE 0
#define SWITCH_LEFT 1
#define SWITCH_CENTER 2
#define SWITCH_RIGHT 3

#define STATE_WAITING 0
#define STATE_SHORT_PRESSED 1
#define STATE_LONG_PRESSED 2

#define KEY_F13	0x68
#define KEY_F14	0x69
#define KEY_F15	0x6A
#define KEY_F16	0x6B
#define KEY_F17	0x6C
#define KEY_F18	0x6D
#define KEY_F1D	0x6E
#define KEY_PAGEUP 0x4b
#define KEY_PAGEDOWN 0x4e

void setup() {
  pinMode(redPin, INPUT); digitalWrite(redPin, HIGH);
  pinMode(tanPin, INPUT); digitalWrite(tanPin, HIGH);
  pinMode(orangePin, INPUT); digitalWrite(orangePin, HIGH);
  pinMode(bluePin, INPUT); digitalWrite(bluePin, HIGH);
  Serial.begin(9600);
  delay(200);
  lastSwitch = 0;
  lastDebounce = millis();
  currentState = 0;
}

int getCurrentSwitch() {
  if (!digitalRead(orangePin)) { return SWITCH_LEFT; }
  if (!digitalRead(tanPin)) { return SWITCH_CENTER; }
  if (!digitalRead(redPin)) { return SWITCH_RIGHT; }
  return SWITCH_NONE;
}

void sendKey(int currentSwitch, boolean isShort, boolean keyDown) {
  buf[0] = 0;
  buf[1] = 0;
  int debug = 0;
  if (keyDown) {
    switch (currentSwitch) {
        case SWITCH_LEFT: buf[2] = isShort ? KEY_F13 : KEY_F16; break;
        case SWITCH_CENTER: buf[2] = isShort ? KEY_F14 : KEY_F17; break;
        case SWITCH_RIGHT: buf[2] = isShort ? KEY_F15 : KEY_F18; break;
    }
    if (debug) { 
      Serial.println(currentSwitch); 
      Serial.println(((int) buf[2]) - KEY_F13); 
      Serial.println("Down"); 
      Serial.println(isShort ? "Short" : "Long"); 
    } 
  } else {
    buf[2] = 0;
    if (debug) { Serial.println("Up"); Serial.println(isShort ? "Short" : "Long"); }
  }
  if (!debug) { Serial.write(buf, 8); }
}

void loop() {
  int currentSwitch = getCurrentSwitch();
  if (currentSwitch != lastSwitch) {
    lastDebounce = millis();
  }
//  Serial.println(currentSwitch);
  // Debounce it
  if (millis() - lastDebounce > debounceDelay) {
    switch (currentState) {
      case STATE_WAITING:
        // No keys pressed yet
        if (currentSwitch != SWITCH_NONE) {
          lastPressed = millis();
          currentState = STATE_SHORT_PRESSED;
        }
        break;
      case STATE_SHORT_PRESSED:
        // Wait to see if this counts as a long press
        if (currentSwitch == SWITCH_NONE) {
          // Send the keystroke
          sendKey(lastSwitchDebounced, true, true);          
          sendKey(lastSwitchDebounced, true, false);          
          currentState = STATE_WAITING;
        } else if (currentSwitch != lastSwitch) {
          // Shouldn't happen, but just in case you're using a different footpedal...
          sendKey(lastSwitchDebounced, true, true);          
          sendKey(lastSwitchDebounced, true, false);          
          lastPressed = millis();
        } else if (millis() - lastPressed > longPressThreshold) {
          currentState = STATE_LONG_PRESSED;
          sendKey(lastSwitch, false, true);
        }
        break;
      case STATE_LONG_PRESSED:
        // Wait for the transition
        if (currentSwitch == SWITCH_NONE) {
          currentState = STATE_WAITING;
          sendKey(lastSwitch, false, false);
        } else if (currentSwitch != lastSwitch) {
          // Likewise, switching between inputs shouldn't happen with this footpedal, 
          // but just in case...
          sendKey(lastSwitch, false, false);          
          currentState = STATE_SHORT_PRESSED;
          lastPressed = millis();          
        }
    }
    lastSwitchDebounced = currentSwitch;
  }
  lastSwitch = currentSwitch;
}

After you upload the code to the Arduino, you’ll also need to reflash the ATMega8U2 chip so that it can act like a USB keyboard. This sounds scary, but the instructions on the Arduino site can help you. When you’ve gotten the hang of reflashing the ATMega8U2 with the standard firmware, reflash it with the Arduino-keyboard-0.3.hex (Uno) or Arduino-keyboard-0.3-mega2560.hex (Mega) firmware from Arduino Hacking. After you reflash, unplug, and re-plug your Arduino, it should now appear as a keyboard. If you made a mistake, don’t panic. Just reflash the standard firmware onto it, and you can upload new programs again.

The last step is to map the F13..F18 function keys to something useful on the computer. I do this in software instead of hardcoding it into sendKey so that I can easily change the keycodes without reflashing the device. I’m on Windows 7 for work and other reasons, so I use AutoHotkey to map the keys. For example, the following AutoHotkey code maps left and right to Page Up and Page Down, and the center to Alt-Tab.

F13::Send, {PgUp}
F14::Send, !{Tab}
F15::Send, {PgDn}

This is a great combination for, say, reading an e-book while eating noodles.

On Linux, you can use Xmodmap or XBindKeys to remap your keys. For the Mac, KeyRemap4MacBook might work – haven’t tried it, though.

Picture!

Making a USB footswitch turned out to be an easy and fun weekend Arduino project. Hope you can build on this idea for more awesomeness! =) I’m looking forward to finding my next project idea. Hmm…

Getting the WordPress Lifestream plugin to work on my blog

Posted: - Modified: | geek, wordpress

I’ve been thinking about including a digest of Twitter, Delicious bookmarks, Google Reader shared items, and other social activity in my weekly review. This lets me include the information in my archive, and it gives people more opportunities to bump into things I found interesting.

It took a bit of hacking, but I eventually got the Lifestream plugin for WordPress to work, with the help of another webpage and some source code diving. Here’s the code that powers this lifestream page:

<?php $options = array('limit' => 50); $events = $lifestream->get_events($options); foreach ($events as $event) { echo '<li>'; $label_inst = $event->get_label_instance($options); if ($event->feed->options['icon_url']) { echo '<img src="' . $event->feed->options['icon_url'] . '" alt="(' . $event->feed->options['feed_label'] . ') \ "> '; } echo '<a href="' . $event->data[0]['link'] . '">' . $event->data[0]['title'] . '</a> (' . date('D, M j, Y', $event->data[0]['date']) . ')'; echo '</li>'; } ?>

$event->render had been giving me problems, so I specified my own output format. It didn’t automatically pick up icon URLs, so I specified the URLs myself. (Bug: the settings get lost if you re-configure the feed.) The plugin seems to be broken out of the box, but there are enough pieces in there for a geek to make things work.

Because I don't want to use up two of my one-post-a-day slots on weekly reviews, I'm leaving it as a web page that I can review and manually copy into my weekly review post instead of automatically publishing something.

You can see it in action in last week’s review.

Work in progress. Hope this helps!

Emacs BBDB magic: Greeting people with nicknames

| emacs

I use Gnus to read my mail within the Emacs text editor. One of the
advantages of using a mail client that's infinitely programmable is
that you can add all sorts of little tweaks to it. Gnus can be
integrated with Emacs' Big Brother Database (BBDB), a semi-structured
text database in which I store all sorts of weird notes. This little
hack takes the nick field of the database and automatically inserts a
greeting. If someone signs himself as Mikong, I should call him that
instead of Joseph Michael. Similarly, I sign my messages as Sacha, not
Sandra Jean. This little tidbit makes it easier to remember to call
people by their nicknames.

(defun sacha/gnus-add-nick-to-message ()
  "Inserts \"Hello, NICK!\" in messages based on the recipient's nick field."
  (interactive)
  (save-excursion
    (let ((bbdb-get-addresses-headers (list (assoc 'recipients bbdb-get-addresses-headers)))
          nicks)
      (setq nicks
            (delq nil
                  (mapcar (lambda (rec) (bbdb-record-getprop rec 'nick))
                          (bbdb-update-records
                           (bbdb-get-addresses nil gnus-ignored-from-addresses 'gnus-fetch-field)
                           nil
                           nil))))
      (goto-char (point-min))
      (when (and nicks
                 (re-search-forward "--text follows this line--" nil t))
        (forward-line 1)
        (insert "Hello, "
                (mapconcat 'identity nicks ", ")
                "!\n\n")))))

(defadvice gnus-post-news (after sacha activate)
  (sacha/gnus-add-nick-to-message))

The Year in Bookmarks

| emacs, ruby
Top 10 tags for 2005 productivity(104) web2.0(88) digitalpinay(88) social(84) useful(83) business(80) blogs(70) research(69) lifehacks(68) blogging(60)

Check out my year in bookmarks for more detail. =) If you want me to analyze yours, just save http://del.icio.us/api/posts/all to all.xml and run this Ruby script. You can also e-mail me (sacha@plannerlove.com) your all.xml if you don't want to go through the hassle yourself.

Much fun!

#!/usr/bin/ruby

require 'rexml/document'
require 'date'

include REXML

YEAR = 2005
USER = "sachac"
doc = Document::new(File::new('all.xml'))

month_hash = {}
month_total = {}
tag_total = {}
doc.elements[1].elements.each {
  |x|
  date = DateTime::parse(x.attributes['time'])
  if (date.year == YEAR)
    x.attributes['tag'].split(' ').each {
      |tag|
      month_hash[date.month] ||= {}
      month_hash[date.month][tag] ||= 0
      month_hash[date.month][tag] += 1
      tag_total[tag] ||= 0
      tag_total[tag] += 1
    }
    month_total[date.month] ||= 0
    month_total[date.month] += 1
  end
}

s = "Top 10 tags for " + YEAR.to_s + " |"
tag_total.sort_by { |tag,total| -total }.slice(0, 10).each { |tag,total|
  s += ' " + tag + "(" + total.to_s + ")"
}
puts s

month_hash.sort.each { |month,tags|
  s = Date::MONTHNAMES[month] + "
(" + month_total[month].to_s + " bookmarks) |" tags.sort_by { |tag,total| -total }.each { |tag,total| s += ' " + tag + "(" + total.to_s + ")" } puts s }

Ruby: Turn bash.org quotes into a fortune file

| ruby

The following code turns XML quotes from bash.org (a popular IRC quotes server) into a fortune-cookie file.
Handy for using with ../emacs/flashcard.el and my ../emacs/flashcard-config.el, which pops up a fortune
every time I get a correct answer.

bash-org-to-fortune.rb:

require 'rss/1.0'
require 'cgi'
require 'net/http'
host = Net::HTTP.new('bash.org', 80)
if ARGV[0] then
   resp, data = host.get('http://bash.org/xml/?top&below=' + ARGV[0], nil)
else
   resp, data = host.get('http://bash.org/xml/?top', nil)
end
parsed = RSS::Parser.parse(data, false)
parsed.items.each { |x| puts CGI::unescapeHTML(x.description.gsub('
', "\n")); puts "%\n" }

Call like this:

ruby bash-org-to-fortune.rb > bash; strfile bash; fortune bash

# or to get the top quotes with score < 1000
ruby bash-org-to-fortune.rb 1000 > bash; strfile bash; fortune bash

semi-form letters: hooray for open source!

Posted: - Modified: | emacs

I needed to write 31 thank-you-for-attending-my-send-off-party e-mail
messages. Instead of having a generic letter that I'd send to lots of
people all listed in To: or Bcc:, I decided to personalize it a bit by
including their nicknames, two-year goals, and a personal message I'd
add to each letter in different places.

Fortunately, my mail client was up to the task. One of the benefits of
having both your contact information and your mail client in an
easily-programmable environment is that you can hack together a quick
program to do exactly what you want.

I had typed everyone's two-year-plans into my contact information
manager's Notes field following a special format. It was the work of a
few minutes to record and run a macro that extracted the data and
created a signature for each person who attended my send-off party.

I then wrote a couple of short functions that looped over the
currently displayed contact records and drafted messages for each of
them following a template.

This resulted in 31 drafts I could edit and send without hassles. Much
fun!

For future reference (and for the handful of Emacs geeks or learners
in the audience), here's the code I used to make it all happen:

(defun sacha/bbdb-send-form-mail (bbdb-record &optional subject template)
  "Compose a form message to the people indicated by the current BBDB record(s)."
  (interactive (list (if (bbdb-do-all-records-p)
                         (mapcar 'car bbdb-records)
                       (bbdb-current-record))
                     (read-string "Subject: ")
                     (read-string "Template (): ")))
  (if (consp bbdb-record)
      (let ((records bbdb-record))
        (while records
          (sacha/bbdb-send-form-mail-1 (car records) subject template)
          (setq records (cdr records))))
    (sacha/bbdb-send-form-mail-1 bbdb-record subject template)))

(defun sacha/bbdb-send-form-mail-1  (bbdb-record &optional subject template)
  "Compose a form message for one person."
    (if bbdb-inside-electric-display
      (bbdb-electric-throw-to-execute
       (list 'sacha/bbdb-send-form-mail bbdb-record subject)))
  ;; else...
  (cond ((null bbdb-record) (error "record unexists"))
        ((null (bbdb-record-net bbdb-record))
         (message "%s record unhas a network addresses." (or (bbdb-record-name bbdb-record) "Unknown")))
        (t (bbdb-send-mail-internal (bbdb-dwim-net-address bbdb-record)
                                    subject (list bbdb-record))
           (goto-char (point-min))
           (when (re-search-forward "--text follows this line--" nil t)
             (forward-line 1)
             (insert template)
             (goto-char (point-min))
             (while (search-forward "" nil t)
               (replace-match (or (bbdb-record-getprop bbdb-record 'nick)
                                  (bbdb-record-name bbdb-record))
                              t t))
             (when (bbdb-record-getprop bbdb-record 'signature)
               (goto-char (point-max))
               (insert "\n")
               (let ((p (point)))
                 (insert (bbdb-record-getprop bbdb-record 'signature))
                 (fill-region-as-paragraph p (point))))
             (goto-char (point-min))
             (if (re-search-forward "^Subject: $" nil t) (end-of-line))))))

I used M-x local-set-key to bind sacha/bbdb-send-form-mail to M. * M
then applies the function to all displayed records.
../emacs/miniedit.el makes it easy to edit long strings in the
minibuffer, and that made the template much easier to write.

Emacs totally rocks. Nothing else has ever given me this much power.

Really funky Planner sorting

| emacs, planner

The following code sorts completed tasks in reverse-chronological
order and incomplete tasks in chronological order. This makes it
easier to see the next action (top of list) and the last action (first
completed task).

(defun sacha/planner-sort-tasks-by-date ()
  "Sort tasks by date, status and priority."
  (skip-chars-forward "#ABC")
  (let ((ch (char-before))
        status)
    (skip-chars-forward "0123456789 ")
    (setq status (char-after))
    (goto-char (line-end-position))
    (skip-chars-backward "]) ")
    (format "%1c%1c%10s"
            (cond
             ((= status ?o) ?1)
             ((= status ?X) ?3)
             ((= status ?C) ?4)
             (t ?2))
            ch
            (let ((date (or (planner-task-date (planner-current-task-info))
                            planner-sort-undated-tasks-equivalent)))
              (if (or (= status ?X)
                      (= status ?C))
                  (sacha/planner-invert-date date)
                date)))))

(defun sacha/planner-invert-date (date)
  "Reverse the date in the sorting order."
  (mapconcat (lambda (ch)
               (if (string= ch ".")
                   ch
                 (number-to-string
                  (- 9 (string-to-number ch)))))
             (split-string date "" t) ""))

pos://../emacs/planner-config.el#35188