The Journalist's Cage

And this gray spirit yearning in desire to follow knowledge like a sinking star...

HomeBlogTagsArticles

Calendar

January 2007
SuMoTuWeThFrSa
 123456
78910111213
14151617181920
21222324252627
28293031

Recent Bookmarks

Tags

Archives


RSS

uTorrent stats on the Logitech G15 LCD

After my needs finally exceeded the limited capacity of GNOME's default bittorrent client, I tried a few alternatives but didn't have much luck. Azureus is almost definitely the single worst program I have ever used, and most of the interesting native-Linux alternatives are still too experimental for day-to-day use. A friend had repeatedly suggested that I consider uTorrent, so I eventually decided to try running it through Wine. It is now my bittorrent client of choice, and one that I highly recommend to others. Miraculously, uTorrent works almost perfectly on Linux. Even though I am running it through Wine, it is more stable, responsive, and robust than Azureus and signficantly less resource intensive.

The latest public beta release of uTorrent includes support for a remotely-accessible web interface. The web UI itself is relatively unremarkable and of little interest to me, but the underlying API is enormously useful. In order to facilitate the construction of a web interface, the uTorrent developers have exposed much of the application's underlying infrastructure through JSON. A little bit of simple reverse engineering has made it possible for me to construct my own interface, and adapt the program in unique and innovative ways.

My first experiment was a simple Python script that connects to uTorrent, logs in using proper authentication, extracts information about active torrents, and displays that information on stdout:

#!/usr/bin/env python

import httplib, base64

def parse_data(data):
  # Generate a dict that associates torrent attributes with their names
  data = dict((name, data[column]) for column, name in enumerate([
      "hash", "state", "name", "total", "unknown", "received", "sent", "ratio",
      "upload_speed", "speed", "eta", "label", "connected_peers", "peers",
      "connected_seeds", "seeds", "available", "queue", "remaining"]))

  # Add some relevant computed values to the dict
  data.update({
    "hours_remaining": round(float(data["eta"] / 60 / 60), 2),
    "percent": round(float(data["received"]) / data["total"] * 100,2),
    "speed_kb": round(float(data["speed"]) / 1024, 2),
    })

  return data

# Create the login string using proper encoding
h = {"Authorization": "Basic " + base64.encodestring("admin:").strip()}

# Connect to the uTorrent JSON interface
con = httplib.HTTPConnection("localhost", 8080)
# Request a list of torrents
con.request("GET", "/gui/?list=1", headers = h)
response = con.getresponse()

# If the request is successful...
if response.status == 200:
  # Iterate over the list of torrents and parse all of them
  torrents = [parse_data(x) for x in eval(response.read())["torrents"]]

  # Display information about each torrent
  for t in torrents:
    print "%-15.15s | %5.2f kb/s | %3.0f hours | %5.2f%%" % (
      t["name"], t["speed_kb"], t["hours_remaining"], t["percent"])

  # Display the total download speed
  print "\nTotal download speed: %s kb/s" % sum(t["speed_kb"] for t in torrents)

This is a relatively simple task, particularly since the syntax used to describe JSON data structures is virtually identical to Python's dict syntax. I cheat and use eval to eliminate the need for a third party JSON library. In general, using eval like that is something I would avoid and discourage since it could potentially create security vulnerabilities, but for a basic experiment it isn't really a big deal. In this example, the parse_data function is used to generate a python dict that associates key names with various torrent attribute values. Data from each torrent is parsed, and then displayed on the screen. At the very end, I use the sum function to compute the total download speed.

G15 uTorrent Display

After completing my first experiment, I decided to take it to the next level. My Logitech G15 keyboard has a nifty programmable LCD that can be used to display various kinds of data. I haven't really done much with the LCD yet, so it seemed like the perfect opportunity. The G15 LCD is supported on the Linux platform by the open source G15 Tools collection, which includes a low-level hardware access library, a userspace daemon for managing the keyboard's macro buttons and LCD, a rendering library designed specifically for generating content suitable for the G15 screen, and a composer utility that enables users to paint the LCD with simple commands.

Unfortunately, there are no Python or Ruby bindings for the rendering library, so I had to either communicate directly with the daemon or use the composer as an intermediary. The Python bindings for the daemon only provide raw pixel access with no abstraction for text or anything like that. The composer provides such abstractions, but it has some weird limitations of its own. The composer binds to a named pipe on the file system, and then one communicates with it by writing commands into the named pipe. It's counterintuitive and messy, but it does work.

The composer accepts numerous commands, and allows the user to write text in fonts at several different sizes. It also supports basic drawing and graphics functionality. I used several of the composer's advanced features in my uTorrent display for the sake of experimentation. I added the following code to my initial script, and wrapped the entire thing in a while loop with a one second time.sleep invocation to make it repeat every second:

# Compute the total download speed
total_speed = sum(t["speed_kb"] for t in torrents)
# Create the basic template for the torrent and heading text
text = "%(name)-20.20s %(speed_kb)3d %(hours_remaining)3d %(percent)3d"
headings = "%-20.20s %3s %3s %3s" % ("Name", "k/s", "eta", "%")

# Open the FIFO created by the g15composer command
sys.stdout = f = open(sys.argv[1], "a")

# Clear the LCD and draw the headings
print "PC 0\nTM \"%s\"" % headings
# Extract and draw the torrent data
print "TO 0 10 1 0 \"%s\"" % '" "'.join(text % t for t in torrents)
# Draw the total speed at the bottom of the display
print "TO 0 35 2 0 \"uTorrent %0.2f Kb/s\"" % total_speed

# Render the lines
for x in [101, 121, 141]: print "DL %d 0 %d 32 1" % (x, x)
for x in [8, 32]: print "DL 0 %d 500 %d 1" % (x, x)

It's not exactly simple, but it works. My G15 LCD uTorrent display looks relatively nice. The grid layout in this particular examples uses up a lot of space, so only three active torrents will be visible. If you need to see more, you can reduce the font size of the total download speed value or remove the grid and labels. If you use the smallest font size and you don't include anything but the torrent data, you could probably fit a lot more on the screen. I usually only have two or three torrents actively downloading at any given time, so I prefer using the bigger fonts.

Since I plan on using this on a regular basis, I decided to clean up the code a bit. In the final version, I put the uTorrent stuff into its own file and threw together a few classes to make it easier to work with. You can see the complete version here. Those of you who want to use it, please remember that this implementation was designed specifically for the Linux platform and almost definitely wont work on Windows in its current state. I've already started to think about other ways to integrate uTorrent and the G15. For my next project, I want to see if I can find a way to control uTorrent's maximum download speed with the keyboard's volume dial.


Posted on 2007-01-186 comments



Global key bindings with Python and GTK

I recently purchased a Logitech G15 gaming keyboard, which features a nifty integrated LCD and an assortment of macro keys. Unfortunately, Beryl only allows users to bind arbitrary commands to 11 keys. Since the G15 has 18 separate macro keys in addition to multimedia buttons, I had to find an alternate solution. I finally decided to implement my own binding manager so that I can associate keys with commands and Python expressions.

Logitech G15

Obviously, the first step was to figure out how to set up global key bindings with Python. I started by poking around the GTK/GDK documentation in search of relevant functionality, but I didn't really find anything there. I asked in the #linux channel on the Ars Technica IRC server and got some useful suggestions, but nothing that seemed to do exactly what I wanted. It finally occurred to me to investigate the source code of a Python application that has global key bindings.

Deskbar, a really useful panel applet that I use on a daily basis, uses a global key binding for activation. The deskbar/Keybinder.py file in the Python site-packages directory reveals that Deskbar is actually using a file called _keybinder.so, which appears to have been created for the Tomboy note-taking program. The tomboy_keybinder_bind function in _keybinder.so makes it possible to associate a binding with a Python function. The binding is described as strings consisting of modifiers and single letters. The following is a simple example that shows how to associate a key binding with a function:

#!/usr/bin/env python

# Import the GTK library
import gtk

# Import the key binding function from the module
from _keybinder import tomboy_keybinder_bind as bindkey

# Define a function to execute when the binding is activated
def onBindingPress(arg):
  print "Binding initiated and parameter received: %s" % arg

# Bind to the onBindingPress function and pass "Test" as an argument
bindkey("<Ctrl><Alt>b", onBindingPress, "Test")

# Establish a binding to quit the program
bindkey("<Ctrl><Alt>q", gtk.main_quit)

# Start a GTK main loop that will run until the quit binding is used
gtk.main()

I find it a bit odd that this functionality had to be borrowed from Tomboy, and it makes me wonder how it is implemented in _keybinder.so. I'm not entirely sure why global key binding support isn't provided by the Python and Ruby GTK bindings, but I'm pleased that I found a way to do it with minimal hassle.


Posted on 2007-01-100 comments



XEmbed with Ruby and Gtk::Socket

I recently discovered a Python IDE called PIDA that provides support for embedded Vim. I'm not a big fan of Python IDEs, but being able to use Vim as the editing component is definitely a killer feature. I haven't seen Vim embedded in a GNOME program since the unfortunate demise of the GVim Bonobo component and I had no idea it was even possible with current versions of Vim. A quick inspection of PIDA's source code revealed that it launches a new instance of GVim and then uses XEmbed to incorporate it into the user interface.

XEmbed experiment I had never heard of XEmbed before, but seeing it used in PIDA compelled me to pursue further investigation. Apparently, XEmbed makes it possible to embed virtually any X11 window inside of another application. GTK provides support for this with the Plug and Socket widgets, which are available in GTK's Python and Ruby bindings. XEmbed client support is also available in Qt with the QtXEmbedContainer object.

For the sake of experimentation, I wrote a simple Ruby script that uses my window management library and the Gtk::Socket object to facilitate window embedding. It provides two combo boxes which each contain a complete list of windows. The user selects a window in each one, and then clicks the Grab button to grab those windows and reparent them in Socket objects separated by a resizeable splitter. The source code for this experiment is relatively simple:

#!/usr/bin/env ruby

require "wmlib"
require "gtk2"

win = Gtk::Window.new("Embed Test")
win.signal_connect("destroy") {|w| Gtk.main_quit}

win.add vb = Gtk::VBox.new
vb.pack_start(hb = Gtk::HBox.new, false, false)

lst = Gtk::ListStore.new String, Integer

hb.pack_start(combo1 = Gtk::ComboBox.new)
hb.pack_start(combo2 = Gtk::ComboBox.new)
hb.pack_start(btnGrab = Gtk::Button.new("Grab"), false)

combo1.model = lst
combo2.model = lst

for w in WM::Window
  i = lst.append
  i[0], i[1] = w.title[0..30], w.id if w.title
end

vb.pack_start(pane = Gtk::HPaned.new)
pane.pack1 socket = Gtk::Socket.new, true, false
pane.pack2 socket2 = Gtk::Socket.new, true, false

## Use tabs instead of panes
#vb.pack_start(tabs = Gtk::Notebook.new)
#tabs.append_page(socket = Gtk::Socket.new)
#tabs.append_page(socket2 = Gtk::Socket.new)

win.show_all()

btnGrab.signal_connect("clicked") {|*a|
  socket.add_id combo1.active_iter[1]
  socket2.add_id combo2.active_iter[1]
}

Gtk.main

While experimenting with panes and XEmbed, one is reminded of tiling window managers like Ion. The ability to place arbitrary windows into tabs or panes is very compelling, and I'm certain I'll find some uses for the feature in the future.


Posted on 2007-01-060 comments