Blog

Blogging with Kirby is as easy as it’s fun. This simple blog shows just a basic implementation of a blogging engine, realized with only a few lines of code. It can easily be extended with tags, categories or whatever feature you need.


TP-Link TD-W8961N

  • Product Link
  • Don't buy one
  • If you're too lazy to replace it, and you want to monitor your ADSL stats when it rains...

tp-w8961n-status.py

import telnetlib
import re
import time

class TPW8961N(object):

    def __init__(self, host):
        self.host = host

    def __enter__(self):
        self.con = telnetlib.Telnet(self.host, 23, 5)
        self.con.read_until('Password: ')
        self.con.write('admin\n')
        self.con.read_until('TP-LINK> ')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.con.write('exit\n')
        self.con.read_all()

    def send_cmd(self, cmd):
        self.con.write(cmd.get_cmd() + '\n')
        response = self.con.read_until('TP-LINK> ')
        return cmd.parse(response)

class Command(object):
    def __init__(self):
        pass

    def get_cmd(self):
        pass

    def parse(self, string):
        pass

class StatusCommand(Command):
    def get_cmd(self):
        return 'wan adsl status'

    def parse(self, response):
        match = re.search('current modem status: (.*?)\r\n', response)
        if match is not None and match.group(1) == 'up':
            return 'up'
        else:
            return 'down'

class RateCommand(Command):
    def get_cmd(self):
        return 'wan adsl c'

    def parse(self, response):
        dl_match = re.search('near-end interleaved channel bit rate: (.*?) kbps\r\n', response)
        ul_match = re.search('far-end interleaved channel bit rate: (.*?) kbps\r\n', response)

        if dl_match is None or ul_match is None:
            return {'dl_rate': 0, 'ul_rate': 0}

        return {'dl_rate': int(dl_match.group(1)), 'ul_rate': int(ul_match.group(1))}

class QualityCommand(Command):
    def __init__(self, direction):
        if direction not in ['downstream', 'upstream']:
            raise Exception('Unknown direction, must be "downstream" or "upstream"')
        self.direction = direction
        super(Command, self).__init__()

    def get_cmd(self):
        if self.direction == 'downstream':
            return 'wan adsl l n'
        return 'wan adsl l f'

    def parse(self, response):
        noise_match = re.search('noise margin ' + self.direction + ': (.*?) db\r\n', response)
        attenuation_match = re.search('attenuation ' + self.direction + ': (.*?) db\r\n', response)

        if noise_match is None or attenuation_match is None:
            return {'noise': 0.0, 'attenuation': 0.0}

        return {'noise': float(noise_match.group(1)), 'attenuation': float(attenuation_match.group(1))}

if __name__ == '__main__':

    while True:
        try:
            print time.strftime('%Y-%m-%d %H:%M-%S')
            with TPW8961N('192.168.1.1') as modem:
                print modem.send_cmd(StatusCommand())
                print modem.send_cmd(RateCommand())
                print modem.send_cmd(QualityCommand('downstream'))
                print modem.send_cmd(QualityCommand('upstream'))
            time.sleep(60)
        except KeyboardInterrupt:
            raise
        except e:
            print e
Link


Picking a Practice Lock

I bought a transparent practice lock (5 pin with spools) from Massdrop a while ago, and thanks to some procrastinating had a proper go at it today. While it is just a practice lock, I struggled judging the correct tension when dealing with false sets, and the whole process felt more delicate than a basic lock. I feel like I might have given up on a standard lock before understanding how the lock behaved and why. Still, I need much, much more practice.

(Massdrop is running another drop for it now)

Link


KBT Pure Pro 60% Keyboard Layout

There’s not that much information about the (Vortex) KBT Pure Pro keyboard on the internet so I thought I’d add my notes here.

Layout:
  1x15
  1.5 1x12 1.5
  1.75 1x11 2.25
  2 1x13
  1.25 1x2 1.25 4.5 1x6

(1 unit = 0.75" or about 19mm)
Notable weird key sizes:
  \          1
  Backspace  1
  Del        1.5
  L Shift    2
  R Shift    1
  R Ctrl     1
  System     1
  Space      4.5
  R Alt      1

I’ve been playing with using Lua to generate parts of my wiki pages, below is the code to generate a simple SVG of the Pure Pro’s layout.

keyboard_svg_layout.lua

--noescape--

local tag = doku.xml_tag

local unit = 19*2  -- Unit size, mapped straight to pixels
local space = 4    -- Spacing between keys (as pixels again)

local halfspace = space/2
local doublespace = space*2

local fontsize = 10

local layout = {
  {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
  {1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5},
  {1.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.25},
  {2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
  {1.25, 1, 1, 1.25, 4.5, 1, 1, 1, 1, 1, 1}
}

local keys = {
  {'esc', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\\', 'BS'},
  {'tab', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 'del'},
  {'caps', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k',' l', ';', '\'', 'enter'},
  {'shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'shift', 'up', 'rctrl'},
  {'lctrl', 'fn', 'sys', 'lalt', 'space', 'ralt', 'fn', 'pn', 'left', 'down', 'right'},
}

local scene = {}

local background = tag('rect', {width="100%", height="100%", fill="#ddd", stroke="black"})
table.insert(scene, background)

for row=1, #layout do
  x = unit
  for col=1, #layout[row] do

    local rect = tag('rect', {x=x+halfspace,
                             y=(row*unit)+halfspace,
                             rx=halfspace,
                             ry=halfspace,
                             width=(layout[row][col]*unit)-space,
                             height=unit-space,
                             fill="#fff",
                             stroke="black"})

    table.insert(scene, rect)

    local sizetext = tag('text', {x=x+doublespace,
                                  y=((row)*unit)+space+fontsize,
                                  fill="#d11",
                                  style='font-size: ' .. fontsize .. 'px'},
                                  tostring(layout[row][col]))

    table.insert(scene, sizetext)

    if keys[row] and keys[row][col] then
      local keytext = tag('text', {x=x+doublespace,
                                  y=((row)*unit)+(2*fontsize)+(3*space),
                                  fill="#111",
                                  style='font-size: ' .. fontsize .. 'px'},
                                  keys[row][col])

      table.insert(scene, keytext)
    end

    x = x + (layout[row][col]*unit)
  end
end

-- Keyboard is 15 units by 5 units (+1 each side for padding)
print(tag('svg', {width=(unit*17), height=(unit*7)}, scene))

SVG Output

1esc111213141516171819101-1=1\\1BS1.5tab1q1w1e1r1t1y1u1i1o1p1[1]1.5del1.75caps1a1s1d1f1g1h1j1k1 l1;12.25enter2shift1z1x1c1v1b1n1m1,1.1/1shift1up1rctrl1.25lctrl1fn1sys1.25lalt4.5space1ralt1fn1pn1left1down1right Link


LuaJIT Benchmarks

I’ve been playing with Lua a little bit recently and was interested in getting an idea of the performance difference between Lua 5.3 and LuaJIT. (The LuaJIT website has very good page covering benchmarks. I just wanted to play around myself). Like the LuaJIT page I picked a few example programs from The Computer Language Benchmarks Game. Programs were run on a AMD-6300FX running Windows 8.

nbody.lua-4.lua 50000000

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 469.36 0.05 467.05 2,356
luajit203 26.28 0.00 26.20 2,504
luajit21 26.31 0.00 26.25 2,512

fannkuchredux.lua 12

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 1717.53 0.14 1710.78 2,284
luajit203 105.33 0.00 105.16 2,528
luajit21 106.06 0.00 105.91 2,536

spectralnorm.lua 5500

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 199.65 0.03 199.38 2,864
luajit203 4.89 0.00 4.88 2,724
luajit21 4.89 0.00 4.88 2,728

binarytrees.lua-2.lua 2

Version Elapsed time (s) Kernel time (s) User time (s) Working set (KB)
lua53 439.98 13.95 423.66 1,072,400
luajit203 78.34 2.22 75.86 928,076
luajit21 77.92 1.75 75.91 811,984

Summary Relative increase in speed over Lua 5.3.

Benchmark lua53 luajit203 luajit2
nbody.lua-4.lua 50000000 1.00 17.83 17.79
fannkuchredux.lua 12 1.00 16.27 16.15
spectralnorm.lua 5500 1.00 40.86 40.86
binarytrees.lua-2.lua 20 1.00 5.58 5.58
Link


Needless Abuse of Lua

So, this is pretty awful. Cool too. Bust mostly awful.

bestoptionsna.lua

local function setoptions(f)
    local options = {}
    local currentname = nil
    local env = setmetatable({},
    {
        __index = function(self, method)
            if method == 'set' then
                return function(name)
                    if currentname ~= nil then
                        error('need to call "to" first')
                    end
                    currentname = name
                end
            elseif method == 'to' then
                return function(value)
                    if currentname == nil then
                        error('need to call "set" before "to"')
                    end
                    options[currentname] = value
                    currentname = nil
                end
            elseif method == 'enable' then
                return function(name)
                    options[name] = true
                end
            end
        end
    })

    -- close enough to fsetenv
    load(string.dump(f), nil, nil, env)()

    return options
end

local function pptable(t)
    for k,v in pairs(t) do
        print(k, '=', v)
    end
end

-- Cool way
local options = setoptions(function()
    set "port" to "8080"
    set "listen" to "127.0.0.1"
    enable "debug"
end)

pptable(options)

-- Boring way
options = {port='8080', listen='127.0.0.1', debug=true}

pptable(options)

Output

debug   =   true
port    =   8080
listen  =   127.0.0.1

debug   =   true
port    =   8080
listen  =   127.0.0.1
Link


Useless Lua Code

Got a hex dump of some firmware which was in kinda an odd format (offsets, data, checksum(maybe?) in hexadecimal ascii text). Wrote a small script to dump the printable characters. Not really interesting.

dump-text.lua

local function readsection(f)
    local colon = f:read(1)
    if colon == nil then return nil end
    if colon ~= ':' then error('section did not start with colon') end
    local data = ''
    while true do
        local char = f:read(1)
        if(char:byte() == 13) then
            f:read(1) -- eat cr
            return data
        end
        data = data .. char
    end
end

local function decodecharacter(c)
    if c:len() ~= 1 then error('must be single character') end
    local map = {['0']=0, ['1']=1, ['2']=2, ['3']=3, ['4']=4, ['5']=5, ['6']=6,
                 ['7']=7, ['8']=8, ['9']=9, A=10, B=11, C=12, D=13, E=14, F=15}
    return map[c]
end

local function decodepair(s)
    if s:len() ~= 2 then error('must be two characters in length') end
    return (decodecharacter(s:sub(1,1)) * 16) + decodecharacter(s:sub(2,2))
end

local function decodesection(s)
    local offset = {}
    local data = {}

    -- todo: last two chars = checksum(offset+data)?
    for i=1, s:len(), 2 do
        local pair = s:sub(i, i+1)
        local value = decodepair(pair)

        if i <= 8 then
            table.insert(offset, value)
        else
            table.insert(data, value)
        end
    end
    return offset, data
end

local function safeprintdata(data)
    local chars = {}
    for index, byte in ipairs(data) do
        if byte >= 32 and byte <= 126 then
            table.insert(chars, string.char(byte))
        else
            table.insert(chars, '.')
        end
    end
    return table.concat(chars)
end

local f = io.open('P2008-V-02.08.02.hex', 'rb')

while true do
    line = readsection(f)
    if line == nil then break end
    local offset, data = decodesection(line)
    print(string.format('%-42s  %s', line, safeprintdata(data)))
end

Output

102170005B204D317878205D2020000000000000B9  [ M1xx ]  .......
102180005B204D313133205D202000000000000035  [ M113 ]  ......5
10219000562D30322E30382E3032202000000000F4  V-02.08.02  .....
1021A00028632920616C6973746172207379737478  (c) alistar systx
1021B000656D732C20323030312D32303132202099  ems, 2001-2012  .
Link


Crimsonland Pak Format Again

Have been spending a little bit of time playing with Lua and an early project was to rewrite the Python script for reading a Crimsonland Pak file. So far I’m enjoying Lua and its language design, even with the very limited standard libraries.

clpak.lua

BinaryFile = {}

BinaryFile.new = function(file)
    return setmetatable({file=file}, {__index=BinaryFile})
end

BinaryFile.readint32 = function(self)
    local data = self.file:read(4)
    return (data:byte(4) * (256^3)) +
           (data:byte(3) * (256^2)) +
           (data:byte(2) * (256^1)) +
           (data:byte(1) * (256^0))
end

BinaryFile.readnulstring = function(self)
    local buf = ''
    while true do
        local ch = self.file:read(1)
        if ch:byte() == 0 then break end
        buf = buf .. ch
    end
    return buf
end

BinaryFile.read = function(self, ...)
    return self.file:read(...)
end

BinaryFile.skip = BinaryFile.read

BinaryFile.seek = function(self, ...)
    return self.file:seek(...)
end

local readindex = function(filename)
    local f  = io.open(filename, 'rb')
    local bf = BinaryFile.new(f)

    -- Header
    assert(bf:readnulstring() == 'PAK')
    assert(bf:readnulstring() == 'V11')

    -- index offsets
    local indexstartoffset = bf:readint32()
    local indexendoffset   = bf:readint32()

    -- just to index
    bf:seek('set', indexstartoffset)

    -- number of entries
    local indexsize = bf:readint32()

    local index = {}

    for i=1, indexsize do
        table.insert(index, {
            name   = bf:readnulstring(),
            offset = bf:readint32(),
            length = bf:readint32()
        })

        -- Unknown (junk?)
        bf:skip(8)
    end

    f:close()

    return index
end

local index = readindex('D:\\Steam\\steamapps\\common\\Crimsonland\\data.pak')

for _,v in ipairs(index) do
    print(string.format('name:%s offset:%d length:%d', v.name, v.offset, v.length))
end
Link


CloudFlare FlexSSL

Thanks to CloudFlare this website now sortof has a SSL cert. Lots of posts needed to be cleaned up with links and embedded content that forced http (rather than being agnostic). Was interesting looking to look back at the old posts (first was in Jan 2006!). So much awful code; but I’m happy that I recorded it somewhere.

Link


Who Hung up an Asterisk Call

Little snippet to work out which party hung up on a call in Asterisk.

extensions.conf

; Detect which party hungup an Asterisk call
; If we (the caller) hangup after Dial() we jump directly to h
; Because of the 'g' option if the other party hangs up we continue on
exten => s,1,Set(whoHungUp=CALLER)
exten => s,n,Dial(IAX2/provider/${number},,g)
exten => s,n,Set(whoHungUp=OTHERPARTY)
exten => s,n,Hangup
exten => h,1,NoOp(whoHungUp ${whoHungUp})
Link


Crimsonland (Steam) Pak Format

A remake of Crimsonland was recently released on Steam (and soon PS4/Vita) and I thought I’d take a look at the pak data file. Didn’t turn out to be anything really interesting, just art assets, sounds, music, and some frontend lua. But was good fun (even if it was a simple format).

clpak.py

import os
import struct

"""
Crimsonland (Steam) pak file format
Header format:
50 40 4B 00 56 31 31 00   ("PAK" NUL "V11" NUL)
int32 index offset     (eg: 28679361)
int32 end index offset (eg: 28885444)
Index Format:
int32 number of indexes?
Index_File format:
null terminated string
int32 absolute offset of file
int32 file length
unknown maybe always (?) equal to: FF 26 E2 50 20 00 00 00
"""

class ClPak(object):
    def __init__(self, filename):
        self.file = open(filename, 'rb')

    def read_header(self):
        self.file.seek(0)
        bytes = self.file.read(16)

        (magic_1, magic_2, index_start_offset, index_end_offset) = 
            struct.unpack('3sx3sxii', bytes)

        if magic_1 != 'PAK' or magic_2 != 'V11':
            raise Exception('Unknown file format')

        return index_start_offset, index_end_offset

    def read_index(self):
        start_offset, end_offset = self.read_header()

        self.file.seek(start_offset)
        bytes = self.file.read(end_offset - start_offset)

        # index_size = struct.unpack('i', bytes[:4])

        return self.read_index_file_details(bytes[4:])

    def read_index_file_details(self, bytes):

        details = {'name': '', 'offset': 0, 'length': 0, 'unknown': ''}

        sequence = iter(bytes)

        for char in sequence:
            if char != chr(0):
                details['name'] += char
            else:
                offset_bytes = sequence.next() + sequence.next() +
                               sequence.next() + sequence.next()
                length_bytes = sequence.next() + sequence.next() +
                               sequence.next() + sequence.next()

                (details['offset'],) = struct.unpack('i', offset_bytes)
                (details['length'],) = struct.unpack('i', length_bytes)

                for x in range(0, 8):
                    details['unknown'] += sequence.next()

                yield details

                details = {'name': '', 'offset': 0, 'length': 0, 'unknown': ''}

    def dump_file(self, details, base_directory):
        dest_filename = base_directory + '/' + details['name']

        if not os.path.exists(os.path.dirname(dest_filename)):
            os.makedirs(os.path.dirname(dest_filename))

        with open(dest_filename, 'wb') as dest:
            self.file.seek(details['offset'])
            dest.write(self.file.read(details['length']))

if __name__ == '__main__':
    cl_pak = ClPak(r'D:\Steam\steamapps\common\Crimsonland\data.pak')
    for file_details in cl_pak.read_index():
        print file_details
        cl_pak.dump_file(file_details, 'd:/clpak_files')
Link