diff --git a/VendServer/DoorClient.py b/VendServer/DoorClient.py
index d46cebe98f9e7ca055cf0c58d9279480930b1b0a..121b563892457c9460274c4a99cfc09df423206d 100755
--- a/VendServer/DoorClient.py
+++ b/VendServer/DoorClient.py
@@ -1,6 +1,6 @@
 #!/usr/bin/python
 
-from LATClient import LATClient
+from .LATClient import LATClient
 from select import select
 import signal
 import sys
@@ -14,17 +14,17 @@ def check_door_service(service, test_string="got wombles?"):
 	rr, wr, er = select([rfh], [], [], 10.0)
 	if rfh not in rr: return "open"
 	recv = rfh.read(len(test_string))
-	if recv <> test_string: return "error"
+	if recv != test_string: return "error"
 	return "closed"
 
 if __name__ == '__main__':
 	result_codes = { 'open' : 0, 'closed' : 1, 'error' : 2, 'invalid args' : 3}
 	def return_result(result):
-		print result
+		print(result)
 		sys.exit(result_codes[result])
 	def timeout(signum, frame):
 		return_result("error")
-	if len(sys.argv) <> 2: return_result('invalid args')
+	if len(sys.argv) != 2: return_result('invalid args')
 	signal.signal(signal.SIGALRM, timeout)
 	signal.alarm(15)
 	return_result(check_door_service(sys.argv[1]))
diff --git a/VendServer/HorizScroll.py b/VendServer/HorizScroll.py
index d27f7dc8e8150b72df36f6915bd55873ffca6c14..9687c8299e03b64c385fc1651724c644cef71d95 100644
--- a/VendServer/HorizScroll.py
+++ b/VendServer/HorizScroll.py
@@ -11,13 +11,12 @@ class HorizScroll:
 
 	def expand(self, padding=None, paddingchar=" ", dir=None, wraparound=False):
 		if len(self.text) <= 10:
-			return [text]
+			return [self.text]
 
 		if padding == None:
-			padding = len(self.text) / 2 + 1
+			padding = len(self.text) // 2 + 1
 
-		format = "%-" + str(padding) + "." + str(padding) + "s"
-		pad = string.replace(format % " "," ",paddingchar)
+		pad = paddingchar * padding
 		padtext = self.text + pad
 		if not wraparound:
 			numiters = len(self.text) - 10
@@ -27,7 +26,7 @@ class HorizScroll:
 		expansion = []
 
 		for x in range(0,numiters):
-			  expansion.append("%-10.10s" % (padtext[x:] + padtext[:x]))
+			expansion.append("%-10.10s" % (padtext[x:] + padtext[:x]))
 		
 		if dir == -1:
 			expansion.reverse()
@@ -40,7 +39,7 @@ if __name__ == '__main__':
 	while 1:
 		for x in eh:
 			sys.stdout.write("\r")
-			print "%-10.10s" % x,
+			print("%-10.10s" % x, end=' ')
 			sys.stdout.flush()
 			time.sleep(0.1)
 
diff --git a/VendServer/Idler.py b/VendServer/Idler.py
index c2eedee233e8c2521dbe64f229e67d43cfa19afe..d3cab072a6f70c64ab7469b7cd319dd3a88c7456 100755
--- a/VendServer/Idler.py
+++ b/VendServer/Idler.py
@@ -3,307 +3,307 @@
 import string, time, os
 from subprocess import Popen, PIPE
 from random import random
-from MessageKeeper import MessageKeeper
+from .MessageKeeper import MessageKeeper
 
 orderings = None
 
 IDLER_TEXT_SPEED=1.8
 
 class Idler:
-	def __init__(self, v, affinity=None):
-		self.v = v
-		if affinity:
-			self._affinity = affinity
-		else:
-			self._affinity = 1
-
-	def next(self):
-		"""Displays next stage of the idler. Returns time to the next step"""
-		return 1
-
-	def reset(self):
-		"""Resets the idler to a known intial state"""
-		pass
-	
-	def finished(self):
-		"""Returns True if the idler is considered finished"""
-		return False
-
-	def affinity(self):
-		"""How much we want this idler to be the next one chosen"""
-		return self._affinity
+    def __init__(self, v, affinity=None):
+        self.v = v
+        if affinity:
+            self._affinity = affinity
+        else:
+            self._affinity = 1
+
+    def __next__(self):
+        """Displays next stage of the idler. Returns time to the next step"""
+        return 1
+
+    def reset(self):
+        """Resets the idler to a known intial state"""
+        pass
+    
+    def finished(self):
+        """Returns True if the idler is considered finished"""
+        return False
+
+    def affinity(self):
+        """How much we want this idler to be the next one chosen"""
+        return self._affinity
 
 class GreetingIdler(Idler):
-	def __init__(self, v, secs_to_greeting = None):
-		affinity = 0 
-		Idler.__init__(self, v, affinity = affinity)
-		self.secs_to_greeting = secs_to_greeting
-		self.message_displayed = False
+    def __init__(self, v, secs_to_greeting = None):
+        affinity = 0 
+        Idler.__init__(self, v, affinity = affinity)
+        self.secs_to_greeting = secs_to_greeting
+        self.message_displayed = False
 
-	def next(self):
-		if not self.secs_to_greeting is None:
-			x = self.secs_to_greeting
-			self.secs_to_greeting = None
-			return x
+    def __next__(self):
+        if not self.secs_to_greeting is None:
+            x = self.secs_to_greeting
+            self.secs_to_greeting = None
+            return x
 
-		self.v.display('UCC SNACKS')
-		self.message_displayed = True
-		return 5
+        self.v.display('UCC SNACKS')
+        self.message_displayed = True
+        return 5
 
-	def reset(self):
-		self.message_displayed = False
-		self.secs_to_greeting = None
+    def reset(self):
+        self.message_displayed = False
+        self.secs_to_greeting = None
 
-	def finished(self):
-		return self.message_displayed
+    def finished(self):
+        return self.message_displayed
 
 class TrainIdler(Idler):
-	def __init__(self, v):
-		Idler.__init__(self, v)
-		self.idle_state = 0
-
-	def put_shark(self, s, l):
-		if self.s[l] == ' ':
-			self.s[l] = s
-		elif self.s[l] == 'X':
-			self.s[l] = '*'
-		else:
-			self.s[l] = 'X'
-
-	def next(self):
-		# does the next stage of a dance
-		self.s = [' ']*10
-		shark1 = self.idle_state % 18
-		if shark1 < 9:
-			self.put_shark('^', shark1)
-		else:
-			self.put_shark('^', 18-shark1)
-
-		shark2 = ((self.idle_state+4) % 36)/2
-		if shark2 < 9:
-			self.put_shark('<', shark2)
-		else:
-			self.put_shark('<', 18-shark2)
-
-		shark3 = ((self.idle_state+7) % 54)/3
-		if shark3 < 9:
-			self.put_shark('>', 9-shark3)
-		else:
-			self.put_shark('>', 9-(18-shark3))
-
-		train1 = ((self.idle_state%(18*36)))
-		train1_start = 122
-		if train1 > train1_start and train1 < train1_start+(10*2):
-			for i in range(5):
-				ptr = i+train1-train1_start-5
-				if ptr >= 0 and ptr < 10: self.s[ptr] = '#'
-
-		train2 = ((self.idle_state%(18*36)))
-		train2_start = 400
-		if train2 > train2_start and train2 < train2_start+(10*2):
-			for i in range(5):
-				ptr = i+train2-train2_start-5
-				if ptr >= 0 and ptr < 10: self.s[9-ptr] = '#'
-
-		train3 = ((self.idle_state%(18*36)))
-		train3_start = 230
-		if train3 > train3_start and train3 < train3_start+(10*2):
-			for i in range(10):
-				ptr = i+train3-train3_start-10
-				if ptr >= 0 and ptr < 10: self.s[ptr] = '-'
-
-		self.v.display(string.join(self.s, ''))
-		self.idle_state += 1
-		self.idle_state %= 18*36*54
-
-	def reset(self):
-		self.idle_state = 0
+    def __init__(self, v):
+        Idler.__init__(self, v)
+        self.idle_state = 0
+
+    def put_shark(self, s: str, l: int):
+        if self.s[l] == ' ':
+            self.s[l] = s
+        elif self.s[l] == 'X':
+            self.s[l] = '*'
+        else:
+            self.s[l] = 'X'
+
+    def __next__(self):
+        # does the next stage of a dance
+        self.s = [' ']*10
+        shark1 = self.idle_state % 18
+        if shark1 < 9:
+            self.put_shark('^', shark1)
+        else:
+            self.put_shark('^', 18-shark1)
+
+        shark2 = ((self.idle_state+4) % 36)//2
+        if shark2 < 9:
+            self.put_shark('<', shark2)
+        else:
+            self.put_shark('<', 18-shark2)
+
+        shark3 = ((self.idle_state+7) % 54)//3
+        if shark3 < 9:
+            self.put_shark('>', 9-shark3)
+        else:
+            self.put_shark('>', 9-(18-shark3))
+
+        train1 = ((self.idle_state%(18*36)))
+        train1_start = 122
+        if train1 > train1_start and train1 < train1_start+(10*2):
+            for i in range(5):
+                ptr = i+train1-train1_start-5
+                if ptr >= 0 and ptr < 10: self.s[ptr] = '#'
+
+        train2 = ((self.idle_state%(18*36)))
+        train2_start = 400
+        if train2 > train2_start and train2 < train2_start+(10*2):
+            for i in range(5):
+                ptr = i+train2-train2_start-5
+                if ptr >= 0 and ptr < 10: self.s[9-ptr] = '#'
+
+        train3 = ((self.idle_state%(18*36)))
+        train3_start = 230
+        if train3 > train3_start and train3 < train3_start+(10*2):
+            for i in range(10):
+                ptr = i+train3-train3_start-10
+                if ptr >= 0 and ptr < 10: self.s[ptr] = '-'
+
+        self.v.display(''.join(self.s))
+        self.idle_state += 1
+        self.idle_state %= 18*36*54
+
+    def reset(self):
+        self.idle_state = 0
 
 class OrderMaker:
-	def __init__(self, n=8):
-		self.n = n
-		self.make_factorials(n)
-	
-	def make_factorials(self, n):
-		self.factorial = []
-		a = 1
-		for i in range(1,n+1):
-			self.factorial.append(a)
-			a *= i
-
-	def order(self, index):
-		used = []
-		for i in range(0,self.n):
-			used.append(i)
-		i = self.n-1
-		j = 0
-		res = []
-		while i >= 0:
-			a = index/self.factorial[i]
-			index %= self.factorial[i]
-			res.append(a+1)
-			i -= 1
-			j += 1
-		for i in range(0,self.n):
-			tmp = used[res[i]-1]
-			for j in range(res[i],self.n):
-				used[j-1] = used[j]
-			res[i] = tmp
-		return res
-
-	def __getitem__(self, i):
-		return self.order(i)
+    def __init__(self, n=8):
+        self.n = n
+        self.make_factorials(n)
+    
+    def make_factorials(self, n):
+        self.factorial = []
+        a = 1
+        for i in range(1,n+1):
+            self.factorial.append(a)
+            a *= i
+
+    def order(self, index):
+        used = []
+        for i in range(0,self.n):
+            used.append(i)
+        i = self.n-1
+        j = 0
+        res = []
+        while i >= 0:
+            a = index // self.factorial[i]
+            index %= self.factorial[i]
+            res.append(a+1)
+            i -= 1
+            j += 1
+        for i in range(0,self.n):
+            tmp = used[res[i]-1]
+            for j in range(res[i],self.n):
+                used[j-1] = used[j]
+            res[i] = tmp
+        return res
+
+    def __getitem__(self, i):
+        return self.order(i)
 
 class GrayIdler(Idler):
-	def __init__(self, v, one=None, zero=None, reorder=0):
-		Idler.__init__(self, v)
-		self.bits = 8
-		self.size = 1 << self.bits
-		self.i = 0
-		self.grayCode = 0
-		self.one = one
-		self.zero = zero
-		self.reorder = reorder
-		global orderings
-		if not orderings:
-			orderings = OrderMaker()
-
-	def next(self):
-		output = self.do_next_state()
-		# does the next stage of a dance
-		if self.zero:
-			output = string.replace(output, "0", self.zero)
-		if self.one:
-			output = string.replace(output, "1", self.one)
-		if self.reorder:
-			global orderings
-			newoutput = ""
-			for i in range(0,8):
-				newoutput += output[orderings[self.reorder][i]]
-			output = newoutput
-		self.v.display(" %8.8s " % (output))
-		self.i = (self.i + 1) % self.size
-
-	def do_next_state(self):
-		self.grayCode = self.i ^ (self.i >> 1)
-		output = self.dec2bin(self.grayCode)
-
-		return "0"*(self.bits-len(output))+output
-
-
-	def dec2bin(self,num):
-	    """Convert long/integer number to binary string.
-
-	    E.g. dec2bin(12) ==> '1100'.
-	    
-	    from http://starship.python.net/~gherman/playground/decbingray/decbingray.py"""
-
-	    assert num >= 0, "Decimal number must be >= 0!"
-
-	    # Gracefully handle degenerate case.
-	    # (Not really needed, but anyway.)    
-	    if num == 0:
-		return '0'
-
-	    # Find highest value bit.
-	    val, j = 1L, 1L
-	    while val < num:
-		val, j = val*2L, j+1L
-
-	    # Convert.
-	    bin = '' 
-	    i = j - 1
-	    while i + 1L:
-		k = pow(2L, i)
-		if num >= k:
-		    bin = bin + '1'
-		    num = num - k
-		else:
-		    if len(bin) > 0:
-			bin = bin + '0'
-		i = i - 1L
-
-	    return bin
-
-	def reset(self):
-		self.i = 0
-		self.grayCode = 0
-		if self.reorder:
-			self.reorder = int(random()*40319)+1
+    def __init__(self, v, one=None, zero=None, reorder=0):
+        Idler.__init__(self, v)
+        self.bits = 8
+        self.size = 1 << self.bits
+        self.i = 0
+        self.grayCode = 0
+        self.one = one
+        self.zero = zero
+        self.reorder = reorder
+        global orderings
+        if not orderings:
+            orderings = OrderMaker()
+
+    def __next__(self):
+        output = self.do_next_state()
+        # does the next stage of a dance
+        if self.zero:
+            output = output.replace("0", self.zero)
+        if self.one:
+            output = output.replace("1", self.one)
+        if self.reorder:
+            global orderings
+            newoutput = ""
+            for i in range(0,8):
+                newoutput += output[orderings[self.reorder][i]]
+            output = newoutput
+        self.v.display(" %8.8s " % (output,))
+        self.i = (self.i + 1) % self.size
+
+    def do_next_state(self):
+        self.grayCode = self.i ^ (self.i >> 1)
+        output = self.dec2bin(self.grayCode)
+
+        return "0"*(self.bits-len(output))+output
+
+
+    def dec2bin(self,num):
+        """Convert long/integer number to binary string.
+
+        E.g. dec2bin(12) ==> '1100'.
+        
+        from http://starship.python.net/~gherman/playground/decbingray/decbingray.py"""
+
+        assert num >= 0, "Decimal number must be >= 0!"
+
+        # Gracefully handle degenerate case.
+        # (Not really needed, but anyway.)    
+        if num == 0:
+            return '0'
+
+        # Find highest value bit.
+        val, j = 1, 1
+        while val < num:
+            val, j = val*2, j+1
+
+        # Convert.
+        bin = '' 
+        i = j - 1
+        while i + 1:
+            k = pow(2, i)
+            if num >= k:
+                bin = bin + '1'
+                num = num - k
+            else:
+                if len(bin) > 0:
+                    bin = bin + '0'
+            i = i - 1
+
+        return bin
+
+    def reset(self):
+        self.i = 0
+        self.grayCode = 0
+        if self.reorder:
+            self.reorder = int(random()*40319)+1
 
 
 class StringIdler(Idler):
-	def __init__(self, v, text="Hello Cruel World!  ",repeat=True, affinity=None):
-		Idler.__init__(self, v, affinity=affinity)
-		self.mk = MessageKeeper(v)
-		self.text = "         " + self.clean_text(text) + "          "
-		
-		msg = [("",False, None),(self.text, repeat, IDLER_TEXT_SPEED)]
-		self.mk.set_messages(msg)
-
-	def clean_text(self, text):
-		# nothing like a bit of good clean text :)
-		valid = string.digits \
-			+ string.letters \
-			+ string.punctuation \
-			+ " "
-		# uppercase it
-		text = string.upper(text)
-		clean = ""
-		for char in text:
-			if char in valid:
-				clean = clean + char
-			else:
-				clean = clean + " "
-		return clean
-
-	def next(self):
-		self.mk.update_display()
-
-	def finished(self):	
-		return self.mk.done()
+    def __init__(self, v, text="Hello Cruel World!  ",repeat=True, affinity=None):
+        Idler.__init__(self, v, affinity=affinity)
+        self.mk = MessageKeeper(v)
+        self.text = "         " + self.clean_text(text) + "          "
+        
+        msg = [("",False, None),(self.text, repeat, IDLER_TEXT_SPEED)]
+        self.mk.set_messages(msg)
+
+    def clean_text(self, text: str):
+        # nothing like a bit of good clean text :)
+        valid = string.digits \
+            + string.ascii_letters \
+            + string.punctuation \
+            + " "
+        # uppercase it
+        text = text.upper()
+        clean = ""
+        for char in text:
+            if char in valid:
+                clean = clean + char
+            else:
+                clean = clean + " "
+        return clean
+
+    def __next__(self):
+        self.mk.update_display()
+
+    def finished(self): 
+        return self.mk.done()
 
 class ClockIdler(Idler):
-	def __init__(self, v):
-		affinity = 3 
-		Idler.__init__(self, v, affinity = affinity)
-		self.last = None
-
-	def next(self):
-		colonchar = ':'
-		if int(time.time()*2) & 1: colonchar = ' '
-		output = time.strftime("%%H%c%%M%c%%S"%(colonchar,colonchar))
-		if output != self.last:
-			self.v.display(" %8.8s " % (output))
-			self.last = output
+    def __init__(self, v):
+        affinity = 3 
+        Idler.__init__(self, v, affinity = affinity)
+        self.last = None
+
+    def __next__(self):
+        colonchar = ':'
+        if int(time.time()*2) & 1: colonchar = ' '
+        output = time.strftime("%%H%c%%M%c%%S"%(colonchar,colonchar))
+        if output != self.last:
+            self.v.display(" %8.8s " % (output))
+            self.last = output
 
 class FortuneIdler(StringIdler):
-	def __init__(self, v, affinity = 30):
-		fortune = "/usr/games/fortune"
-		text = "I broke my wookie...."
-		if os.access(fortune,os.F_OK|os.X_OK):
-			(lines, unused) = Popen((fortune,), close_fds=True, stdout=PIPE).communicate()
-			text = lines.replace('\n', '  ').replace('\r', '')
-		StringIdler.__init__(self, v, text,repeat=False, affinity=affinity)
+    def __init__(self, v, affinity = 30):
+        fortune = "/usr/games/fortune"
+        text = "I broke my wookie...."
+        if os.access(fortune,os.F_OK|os.X_OK):
+            (lines, unused) = Popen((fortune,), close_fds=True, stdout=PIPE).communicate()
+            text = lines.decode('utf-8').replace('\n', '  ').replace('\r', '')
+        StringIdler.__init__(self, v, text,repeat=False, affinity=affinity)
 
-	def reset(self):
-		self.__init__(self.v, affinity=self._affinity)
+    def reset(self):
+        self.__init__(self.v, affinity=self._affinity)
 
 
 class PipeIdler(StringIdler):
-	def __init__(self, v, command, args, affinity = 5):
-		text = "I ate my cookie...."
-		if os.access(command,os.F_OK|os.X_OK):
-			(lines, unused) = Popen([command,] + args.split(), close_fds=True, stdout=PIPE).communicate()
-			text = lines.replace('\n', '  ').replace('\r', '')
-		StringIdler.__init__(self, v, text,repeat=False, affinity=affinity)
+    def __init__(self, v, command, args, affinity = 5):
+        text = "I ate my cookie...."
+        if os.access(command,os.F_OK|os.X_OK):
+            (lines, unused) = Popen([command,] + args.split(), close_fds=True, stdout=PIPE).communicate()
+            text = lines.decode('utf-8').replace('\n', '  ').replace('\r', '')
+        StringIdler.__init__(self, v, text,repeat=False, affinity=affinity)
 
 class FileIdler(StringIdler):
-	def __init__(self, v, thefile=None, repeat=False, affinity=8):
-		text = "I broke my wookie...."
-
-		if file and os.access(thefile,os.F_OK|os.R_OK):
-			f = file(thefile,'r')
-			text = string.join(f.readlines())
-			f.close()
-		StringIdler.__init__(self, v, text,repeat=False, affinity=affinity)
+    def __init__(self, v, thefile=None, repeat=False, affinity=8):
+        text = "I broke my wookie...."
+
+        if os.access(thefile,os.F_OK|os.R_OK):
+            f = open(thefile,'r')
+            text = "".join(f.readlines())
+            f.close()
+        StringIdler.__init__(self, v, text,repeat=False, affinity=affinity)
diff --git a/VendServer/LATClient.py b/VendServer/LATClient.py
deleted file mode 100644
index 638d20b05bc1f3a136a93f2c5fd415f964c1048e..0000000000000000000000000000000000000000
--- a/VendServer/LATClient.py
+++ /dev/null
@@ -1,157 +0,0 @@
-from socket import *
-from select import select
-from os import popen4
-from time import sleep
-import logging
-
-LATCP_SOCKET = '/var/run/latlogin'
-
-LAT_VERSION = '1.22'
-LAT_VERSION = '1.24' # for running on Mermaid. [DAA] 20071107
-LATCP_CMD_VERSION = 8
-LATCP_CMD_TERMINALSESSION = 26
-LATCP_CMD_ERRORMSG = 99
-
-class LATClientException(Exception): pass
-
-def read_for_a_bit(rfh):
-	message = ''
-	while 1:
-		r = select([rfh], [], [], 5.0)[0]
-		if r:
-			try:
-				ch = rfh.read(1)
-			except socket.error:
-				ch = ''
-			if ch == '':
-				break
-			message = message + ch
-		else:
-			break
-	logging.debug("Received message: ", repr(message))
-	return message
-
-def write_and_get_response(rfh, wfh, message, expect_echo=True):
-	logging.debug("Writing message:", repr(message))
-	wfh.write(message+'\r\n')
-	wfh.flush()
-	logging.debug("  --> Sent")
-	response = read_for_a_bit(rfh)
-	if response.find(message) == -1 and expect_echo:
-		raise LATClientException("Talking to DEC server, expected to find original message in echo but didn't")
-	return response
-
-class LATClient:
-	def __init__(self, service = None, node = None, port = None,
-		     localport = None, password = None, is_queued = False,
-		     server_name = '', connect_password='', priv_password=''):
-
-		self.server_name = server_name
-		self.connect_password = connect_password
-		self.priv_password = priv_password
-		
-		self.sock = socket(AF_UNIX, SOCK_STREAM, 0);
-		self.sock.connect(LATCP_SOCKET)
-		self.send_msg(LATCP_CMD_VERSION, LAT_VERSION+'\000')
-		(cmd, msg) = self.read_reply()
-		if service == None: service = ''
-		if node == None: node = ''
-		if port == None: port = ''
-		if localport == None: localport = ''
-		if password == None: password = ''
-		if is_queued == True:
-			is_queued = 1
-		else:
-			is_queued = 0
-		self.send_msg(LATCP_CMD_TERMINALSESSION, '%c%c%s%c%s%c%s%c%s%c%s' % \
-			(is_queued,
-			 len(service), service,
-			 len(node), node,
-			 len(port), port,
-			 len(localport), localport,
-			 len(password), password
-			 ))
-		(cmd, msg) = self.read_reply()
-		if ord(cmd) == LATCP_CMD_ERRORMSG:
-			raise LATClientException(msg)
-
-		self.rfh = self.sock.makefile('r')
-		self.wfh = self.sock.makefile('w')
-
-		r = select([self.rfh], [], [], 2.0)[0]
-		if r:
-			l = self.rfh.readline()
-			if l.find('Service in use') >= 0:
-				logging.warning("Service in use, apparently: restarting DEC server")
-				self.reboot_server()
-
-	def __del__(self):
-		try:
-			self.sock.close()
-			self.sock.shutdown(2)
-		except:
-			pass
-		del self.sock
-
-	def send_msg(self, cmd, msg):
-		self.sock.send('%c%c%c%s'%(cmd, len(msg)/256, len(msg)%256, msg))
-
-	def reboot_server(self):
-		self.sock.shutdown(2)
-		self.sock.close()
-		
-		logging.info('Logging into DEC server')
-		mopw, mopr = popen4('/usr/sbin/moprc '+self.server_name)
-		write_and_get_response(mopr, mopw, '')
-
-		logging.info('Sending password')
-		r = write_and_get_response(mopr, mopw, self.connect_password, False)
-		if r.find('Enter username> ') == -1:
-			logging.warning("Expected username prompt, got " + repr(r))
-			raise LATClientException('failed to reboot server')
-
-		logging.info('Sending username')
-		r = write_and_get_response(mopr, mopw, 'grim reaper')
-		if r.find('Local> ') == -1:
-			logging.warning("Expected DEC server prompt, got " + repr(r))
-			raise LATClientException('failed to reboot server')
-
-		logging.info('Requesting privileges')
-		r = write_and_get_response(mopr, mopw, 'set priv')
-		if r.find('Password> ') == -1:
-			logging.warning("Expected priv password prompt, got " + repr(r))
-			raise LATClientException('failed to reboot server')
-
-		logging.info('Sending password')
-		r = write_and_get_response(mopr, mopw, self.priv_password, False)
-		if r.find('Local> ') == -1:
-			logging.warning("Expected DEC server prompt, got " + repr(r))
-			raise LATClientException('failed to reboot server')
-
-		logging.info('Sending reboot request')
-		r = write_and_get_response(mopr, mopw, 'init del 0')
-		if r.find('Target does not respond') == -1:
-			logging.warning("Expected DEC server to die, got " + repr(r))
-			raise LATClientException('failed to reboot server')
-
-		logging.info('Closed connection to server')
-		mopr.close()
-		mopw.close()
-		logging.info("Waiting 10 seconds for DEC server to come back to life...")
-		sleep(10)
-		logging.info("Rightyo, back to vending!")
-		raise LATClientException('needed to reboot server')
-
-	def read_reply(self):
-		head = self.sock.recv(3)
-		if len(head) != 3:
-			raise LATClientException('Short LAT packet')
-		cmd = head[0]
-		length = ord(head[1])*256 + ord(head[2])
-		msg = self.sock.recv(length)
-		if cmd == LATCP_CMD_ERRORMSG:
-			raise LATClientException('Received LAT error: %s'%msg)
-		return (cmd, msg)
-	
-	def get_fh(self):
-		return (self.rfh, self.wfh)
diff --git a/VendServer/LDAPConnector.py b/VendServer/LDAPConnector.py
deleted file mode 100644
index 8699fd2ba6f536d39efd434ca534c685bdad87e7..0000000000000000000000000000000000000000
--- a/VendServer/LDAPConnector.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python2.4
-
-import ldap
-import ldap.filter
-
-LDAP_TIMEOUT = 10
-
-def get_ldap_connection():
-        ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, '/etc/ssl/UCC-CA.crt')
-        ldap.set_option(ldap.OPT_X_TLS,1)
-        ldap.set_option(ldap.OPT_X_TLS_ALLOW,1)
-        #ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
-        conn = ldap.initialize('ldaps://mussel.ucc.gu.uwa.edu.au/')
-        
-        binddn = 'cn=mifareagent,ou=profile,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
-        passfile = open('/etc/dispense2/ldap.passwd')
-        password = passfile.readline().strip()
-        passfile.close()
-        
-        conn.simple_bind_s(binddn, password)
-        return conn
-
-def get_uid(card_id):
-        ldapconn = get_ldap_connection()
-        
-        basedn = 'ou=People,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
-        filter = ldap.filter.filter_format('(uccDispenseMIFARE=%s)', (card_id, ))
-        attrs = ('uidNumber',)
-        
-        results = ldapconn.search_st(basedn, ldap.SCOPE_SUBTREE, filter, attrs, timeout=LDAP_TIMEOUT)
-        
-        ldapconn.unbind()
-        
-        if len(results) != 1:
-                raise ValueError, "no UID found for card ID"
-        
-        return results[0][1]['uidNumber'][0]
-
-def get_uname(uid):
-        ldapconn = get_ldap_connection()
-        
-        basedn = 'ou=People,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
-        filter = ldap.filter.filter_format('(uidNumber=%s)', (uid, ))
-        attrs = ('uid',)
-        
-        results = ldapconn.search_st(basedn, ldap.SCOPE_SUBTREE, filter, attrs, timeout=LDAP_TIMEOUT)
-        
-        ldapconn.unbind()
-        
-        if len(results) != 1:
-                raise ValueError, "no username found for user id"
-        
-        return results[0][1]['uid'][0]
-
-def set_card_id(uidNumber, card_id):
-        ldapconn = get_ldap_connection()
-        
-	# fix uidNumber for three/four digit uids
-	uidNumber = str(int(uidNumber))
-        basedn = 'ou=People,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au'
-        filter = ldap.filter.filter_format('(uidNumber=%s)', (uidNumber, ))
-        attrs = ('objectClass', )
-        
-        results = ldapconn.search_st(basedn, ldap.SCOPE_SUBTREE, filter, attrs, timeout=LDAP_TIMEOUT)
-        
-        if len(results) != 1:
-                raise "ValueError", 'error in uidNumber'
-        
-        user_dn = results[0][0]
-        
-        mod_attrs = []
-        
-        # Does it have the correct object class?
-        if 'uccDispenseAccount' not in results[0][1]['objectClass']:
-                # Add uccDispenseAccount objectclass
-                mod_attrs.append((ldap.MOD_ADD, 'objectClass', 'uccDispenseAccount'))
-        
-        # Add MIFARE Card ID
-        mod_attrs.append((ldap.MOD_ADD, 'uccDispenseMIFARE', card_id))
-        
-        # Use a double-try here to work around something that's fixed in Python 2.5
-        try:
-            try:
-                ldapconn.modify_s(user_dn, mod_attrs)
-            except ldap.TYPE_OR_VALUE_EXISTS, e:
-                pass
-        finally:
-            ldapconn.unbind()
-
-if __name__ == '__main__':
-        set_card_id('11126', '\x01\x02\x03\x04\x05\x06')
-        print get_uid('\x01\x02\x03\x04\x05\x06')
diff --git a/VendServer/MIFAREClient.py b/VendServer/MIFAREClient.py
index e0b6a6c83eed31f73bdbc6b1194b60adac73baee..8adf2336cc699bd8ec369d692e00af7a8da79ea3 100644
--- a/VendServer/MIFAREClient.py
+++ b/VendServer/MIFAREClient.py
@@ -1,44 +1,21 @@
-from MIFAREDriver import MIFAREReader, MIFAREException
-from serial import Serial
-from LDAPConnector import get_uid, set_card_id
-
-class MIFAREClient:
-    def __init__(self):
-        self.port = Serial('/dev/ttyS2', baudrate = 19200)
-        self.reader = MIFAREReader(self.port)
-        self.reader.set_led(red = False, green = True)
-        self.reader.beep(100)
-    
-    def get_card_id(self):
-        self.reader.set_led(red = True, green = False)
-        try:
-            card_id, capacity = self.reader.select_card()
-        except MIFAREException:
-            self.reader.set_led(red = False, green = True)
-            return None
-        else:
-            self.reader.set_led(red = False, green = True)
-            self.reader.beep(100)
-            return card_id
-    
-    def get_card_uid(self):
-		card_id = self.get_card_id()
-		if card_id == None:
-			return None
-		else:
-			return get_uid(card_id)
-    
-    def add_card(self, uid):
-        self.reader.set_led(red = True, green = False)
-        for attempt in range(5):
-            self.reader.beep(50)
-            try:
-                card_id, capacity = self.reader.select_card()
-            except MIFAREException:
-                pass
-            else:
-                set_card_id(uid, card_id)
-                self.reader.set_led(red = False, green = True)
-                return True
-        self.reader.set_led(red = False, green = True)
-        return False
+from .MIFAREDriver import MIFAREReader, MIFAREException
+from serial import Serial
+
+class MIFAREClient:
+    def __init__(self):
+        self.port = Serial('/dev/ttyS2', baudrate = 19200)
+        self.reader = MIFAREReader(self.port)
+        self.reader.set_led(red = False, green = True)
+        self.reader.beep(100)
+    
+    def get_card_id(self):
+        self.reader.set_led(red = True, green = False)
+        try:
+            card_id, capacity = self.reader.select_card()
+        except MIFAREException:
+            self.reader.set_led(red = False, green = True)
+            return None
+        else:
+            self.reader.set_led(red = False, green = True)
+            self.reader.beep(100)
+            return card_id
diff --git a/VendServer/MIFAREDriver.py b/VendServer/MIFAREDriver.py
index a52a89115883b8d18f6574f805625ee3d8515995..852a9056f9119c1e73953d6ae0b5d26d7af04d37 100644
--- a/VendServer/MIFAREDriver.py
+++ b/VendServer/MIFAREDriver.py
@@ -8,10 +8,11 @@ Licensed under an MIT-style license: see LICENSE file for details.
 '''
 
 import serial, logging
+from functools import reduce
 
 xor = lambda x, y: x ^ y
-def checksum(string):
-    return chr(reduce(xor, [ord(i) for i in string]))
+def checksum(data: bytes):
+    return bytes([reduce(xor, data)])
 
 
 class MIFAREException(Exception):
@@ -35,7 +36,7 @@ class MIFAREReader:
         self.io = io
         if isinstance(self.io, serial.Serial):
             self.io.setTimeout = 2
-        self.address = '\x00\x00'
+        self.address = b'\x00\x00'
 
     def get_absolute_block(self, vector):
         if vector[0] < 32:
@@ -46,7 +47,7 @@ class MIFAREReader:
             # Thus, sector 32 starts at block 128, 33 at 144, and so on
             return 128 + (vector[0] - 32) * 16 + vector[1]
 
-    def send_packet(self, data):
+    def send_packet(self, data: bytes):
         '''Constructs a packet for the supplied data string, sends it to the
         MIFARE reader, then returns the response (if any) to the commmand.'''
 
@@ -57,29 +58,30 @@ class MIFAREReader:
             self.io.flushOutput()
 
         # XXX - Needs more error checking.
-        data = '\x00' + self.address + data
-        packet = '\xAA\xBB' + chr(len(data)) + data + checksum(data)
+        data = b'\x00' + self.address + data
+        packet = bytes([0xAA, 0xBB, len(data)]) + data + checksum(data)
+        #print("send_packet: {!r}".format(packet))
         self.io.write(packet)
         response = ''
         header = self.io.read(2)
-        if header == '\xaa\xbb':
-            length = ord(self.io.read(1))
+        if header == b'\xaa\xbb':
+            length = self.io.read(1)[0]
             data = self.io.read(length)
             packet_xsum = self.io.read(1)
             if checksum(data) == packet_xsum and len(data) == length:
                 # Strip off separator and address header
                 return data[3:]
             else:
-                raise MIFARECommunicationException, "Invalid response received"
+                raise MIFARECommunicationException("Invalid response received")
 
     def set_antenna(self, state = True):
         """Turn the card reader's antenna on or off (no return value)"""
-        command = '\x0C\x01' + chr(int(state))
+        command = bytes([0xc, 0x1, int(state)])
         response = self.send_packet(command)
-        if response == '\x0c\x01\x00':
+        if response == b'\x0c\x01\x00':
             return None
         else:
-            raise MIFAREException, 'command failed: set_antenna (%s)' % state
+            raise MIFAREException('command failed: set_antenna (%s)' % state)
 
     def select_card(self, include_halted = False):
         """Selects a card and returns a tuple of  (serial number, capacity).
@@ -88,59 +90,59 @@ class MIFAREReader:
         been called on."""
 
         # Request type of card available
-        command = command = '\x01\x02'
+        command = command = b'\x01\x02'
         if include_halted:
-            command += '\x52'
+            command += b'\x52'
         else:
-            command += '\x26'
+            command += b'\x26'
 
         card_type_response = self.send_packet(command)
 
-        if card_type_response == None or card_type_response[2] == '\x14':
-            raise MIFAREException, "select_card: no card available"
+        if card_type_response == None or card_type_response[2] == 0x14:
+            raise MIFAREException("select_card: no card available")
         card_type = card_type_response[3:5]
 
-        if card_type == '\x44\x00': # MIFARE UltraLight
+        if card_type == b'\x44\x00': # MIFARE UltraLight
             #raise NotImplementedError, "UltraLight card selected - no functions available"
-            # HACK by JAH: The response format isn't fully known yet (official driver reads 7 bytes from offset +9 in the raw response)
+            # HACK by TPG: The response format isn't fully known yet (official driver reads 7 bytes from offset +9 in the raw response)
             # - This code effectively reads all bytes after +9 (AA, BB, len, and three skilled by `send_packet`, then three skipped here)
             # - Official driver has `AA` bytes followed by `00` (encoded/decoded)
-            command = '\x12\x02'
+            command = b'\x12\x02'
             serial = self.send_packet(command)[3:]
             capacity = 0
             return (serial, capacity)
         else:
         # Otherwise, must be a standard MIFARE card.
             # Anticollision
-            command = '\x02\x02\x04'
+            command = b'\x02\x02\x04'
             # No error handling on this command
             serial = self.send_packet(command)[3:]
 
             # Select the card for use
             try:
-                select_response = self.send_packet('\x03\x02' + serial)
-                capacity = ord(select_response[3])
+                select_response = self.send_packet(b'\x03\x02' + serial)
+                capacity = select_response[3]
             except IndexError:
                 logging.warning('Tried to select card but failed: card_type %s, serial %s, select_response %s' % (card_type.__repr__(), serial.__repr__(), select_response.__repr__()))
                 capacity = 0
             return (serial, capacity)
 
-    def sector_login(self, blockvect, key, keytype=0):
+    def sector_login(self, blockvect, key: bytes, keytype=0):
         """Log in to a block using the six-byte key.
 
         Use a keytype of 1 to use key B."""
         sector = self.get_absolute_block((blockvect[0], 0))
 
         if len(key) != 6:
-            raise ValueError, 'key must be a six-byte string'
+            raise ValueError('key must be a six-byte string')
 
         keytype = 96 + keytype
 
-        data = chr(keytype) + chr(sector) + key
+        data = bytes([keytype, sector]) + key
 
-        result = self.send_packet('\x07\x02' + data)
-        if ord(result[2]) == 22:
-            raise MIFAREAuthenticationException, "incorrect key provided"
+        result = self.send_packet(b'\x07\x02' + data)
+        if result[2] == 22:
+            raise MIFAREAuthenticationException("incorrect key provided")
 
         return
 
@@ -148,17 +150,17 @@ class MIFAREReader:
         "Read the 16-byte block at vector (sector, block)."
         block = self.get_absolute_block(blockvect)
 
-        result = self.send_packet('\x08\x02' + chr(block))
+        result = self.send_packet( bytes([0x08,0x02, block]) )
         return result[3:19]
 
-    def write_block(self, blockvect, data):
+    def write_block(self, blockvect, data: bytes):
         """Write the 16 bytes in data to the block at vector (sector, block)."""
         block = self.get_absolute_block(blockvect)
         if len(data) != 16:
-            raise ValueError, "invalid data length - must be 16 bytes"
+            raise ValueError("invalid data length - must be 16 bytes")
 
-        result = self.send_packet('\x09\x02' + chr(block) + data)
-        return
+        result = self.send_packet( bytes([0x09,0x02,block]) + data )
+        return #result[3:19]
 
     def write_key(self, key):
         pass
@@ -174,7 +176,7 @@ class MIFAREReader:
 
     def halt(self):
         """Halt the current card - no further transactions will be performed with it."""
-        self.send_packet('\x04\x02')
+        self.send_packet(b'\x04\x02')
 
     def set_led(self, red = False, green = False):
         led_state = 0
@@ -182,14 +184,14 @@ class MIFAREReader:
             led_state += 1
         if green:
             led_state += 2
-        self.send_packet('\x07\x01' + chr(led_state))
+        self.send_packet(bytes([0x07,0x01, led_state]))
 
     def beep(self, length):
         '''Beep for a specified length of milliseconds.'''
         length = int(round(length / 10.))
         if length > 255:
             length = 255
-        self.send_packet('\x06\x01' + chr(length))
+        self.send_packet(bytes([0x06,0x01, length]))
 
     def reset(self):
         pass
diff --git a/VendServer/MessageKeeper.py b/VendServer/MessageKeeper.py
index 60d2b9e2b331bee93bffe25f0edecedf3ddddb97..f14e05ac08f1c3cd51cba6a0c684ed80007c559a 100755
--- a/VendServer/MessageKeeper.py
+++ b/VendServer/MessageKeeper.py
@@ -2,7 +2,7 @@
 # vim:ts=4
 
 import sys, os, string, re, pwd, signal
-from HorizScroll import HorizScroll
+from .HorizScroll import HorizScroll
 from random import random, seed
 from time import time, sleep
 
diff --git a/VendServer/OpenDispense.py b/VendServer/OpenDispense.py
index 6b93058b19e0dac3bf1b9a921f8e5229ed736f05..c42f235d0db5d30b27de13109d07b065cebba510 100644
--- a/VendServer/OpenDispense.py
+++ b/VendServer/OpenDispense.py
@@ -7,18 +7,15 @@ This is so VendServer can easily operate regardless of the current accounting ba
 Documentation for this code can be found inder Dispence.DispenceInterface
 """
 
-from DispenseInterface import DispenseInterface
+from .DispenseInterface import DispenseInterface
 import os
 import logging
 import re
 import pwd
 import base64
 import socket
-from subprocess import Popen, PIPE
-from LDAPConnector import get_uid,get_uname, set_card_id
 
 DISPENSE_ENDPOINT = ("localhost", 11020)
-DISPSRV_MIFARE = True
 
 # A list of cards that should never be registered, and should never log in
 # - Some of these might have been registered before we knew they were duplicates
@@ -36,32 +33,52 @@ class OpenDispense(DispenseInterface):
 	def __init__(self, username=None, secret=False):
 		pass
 
-	def authUserIdPin(self, userId, pin):
+	def authUserIdPin(self, userId: str, pin: str):
 		return self.authUserIdPin_db(userId, pin)
 		#return self.authUserIdPin_file(userId, pin)
 	
-	def authUserIdPin_db(self, userId, pin):
+	def _connect(self, authenticate:bool=True, set_euid:bool=False):
+
+		sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+		try:
+			sock.connect(DISPENSE_ENDPOINT)
+		except ConnectionRefusedError:
+			logging.error("Cannot connect to dispsrv on {}".format(DISPENSE_ENDPOINT,))
+			return None
+		logging.debug('connected to dispsrv')
+		conn = Connection( sock.makefile('rw', encoding='utf-8') )
+		if authenticate:
+			rsp = conn.send_command("AUTHIDENT")
+			if not "200" in rsp:
+				logging.info('Server said no to AUTHIDENT! - %r' % (rsp,))
+				return None
+			logging.debug('authenticated')
+		if set_euid:
+			rsp = conn.send_command("SETEUSER %s" % (self._username,))
+			if not "200" in rsp:
+				logging.info('Server said no to SETEUSER! - %r' % (rsp,))
+				return None
+		
+		return conn
+
+	def authUserIdPin_db(self, userId: str, pin: str):
 		userId = int(userId)
 
 		try:
-			# Get username (TODO: Store the user ID in the dispense database too)
+			# Get username (TODO: Store the user ID in the dispense database too, so the vending machine
+			# doesn't need LDAP/AD working)
 			info = pwd.getpwuid(userId)
 		except KeyError:
 			logging.info('getting pin for uid %d: user not in password file'%userId)
 			return False
 		
-		sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-		sock.connect(DISPENSE_ENDPOINT)
-		logging.debug('connected to dispsrv')
-		sockf = sock.makefile()
-		sockf.write("AUTHIDENT\n"); sockf.flush()
-		rsp = sockf.readline()
-		assert "200" in rsp
-		logging.debug('authenticated')
-		sockf.write("PIN_CHECK %s %s\n" % (info.pw_name, pin)); sockf.flush()
-		rsp = sockf.readline()
+		conn = self._connect()
+		if conn is None:
+			logging.error("getting pin for uid {}: Unable to open connection".format(userId))
+			return False
+		rsp = conn.send_command("PIN_CHECK %s %s" % (info.pw_name, pin,))
 		if not "200" in rsp:
-			logging.info('checking pin for uid %d: Server said no - %r' % (userId, rsp))
+			logging.info('checking pin for uid %d: Server said no (PIN_CHECK) - %r' % (userId, rsp))
 			return False
 		#Login Successful
 		logging.info('accepted pin for uid %d \'%s\'' % (userId, info.pw_name))
@@ -71,7 +88,7 @@ class OpenDispense(DispenseInterface):
 		self._username = info.pw_name
 		return True
 
-	def authUserIdPin_file(self, userId, pin):
+	def authUserIdPin_file(self, userId: str, pin: str):
 		userId = int(userId)
 
 		try:
@@ -88,11 +105,11 @@ class OpenDispense(DispenseInterface):
 		except OSError:
 			logging.info('getting pin for uid %d: .pin not found in home directory'%userId)
 			return False
-		if s.st_mode & 077:
+		if s.st_mode & 0o77:
 			logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%userId)
-			os.chmod(pinfile, 0600)
+			os.chmod(pinfile, 0o600)
 		try:
-			f = file(pinfile)
+			f = open(pinfile)
 		except IOError:
 			logging.info('getting pin for uid %d: I cannot read pin file'%userId)
 			return False
@@ -116,94 +133,66 @@ class OpenDispense(DispenseInterface):
 	def authMifareCard(self, cardId):
 		self._loggedIn = False
 		self._username = None
-		if DISPSRV_MIFARE:
-			card_base64 = base64.b64encode(cardId)
+	
+		card_base64 = base64.b64encode(cardId).decode('utf-8')
 
-			if card_base64 in CARD_BLACKLIST:
-				logging.info("Blacklisted card base64:%s" % (card_base64,))
-				return False
-			
-			sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-			sock.connect(DISPENSE_ENDPOINT)
-			logging.debug('connected to dispsrv')
-			sockf = sock.makefile()
-			sockf.write("AUTHIDENT\n"); sockf.flush()
-			rsp = sockf.readline()
-			assert "200" in rsp
-			logging.debug('authenticated')
-			sockf.write("AUTHCARD %s\n" % (card_base64,)); sockf.flush()
-			rsp = sockf.readline()
-			if not "200" in rsp:
-				logging.info("Rejected card base64:%s" % (card_base64,))
-				return False
-			username = rsp.split('=')[1].strip()
-			logging.info("Accepted card base64:%s for %s" % (card_base64,username,))
-
-			## Check for thier username
-			#try:
-			#	# Get info from the system (by username)
-			#	info = pwd.getpwnam(username)
-			#except KeyError:
-			#	logging.info('getting info for user \'%s\': user not in password file' % (username,))
-			#	return False
-			#self._userid = info.pw_uid
-			self._userid = None
-			self._username = username
-		else:
-			# Get the users ID
-			self._userid = get_uid(cardId)
-
-			# Check for thier username
-			try:
-				# Get info from the system (by UID)
-				info = pwd.getpwuid(self._userid)
-			except KeyError:
-				logging.info('getting info for uid %d: user not in password file' % (self._userid,))
-				return False
-			self._username = info.pw_name
+		if card_base64 in CARD_BLACKLIST:
+			logging.info("Blacklisted card base64:%s" % (card_base64,))
+			return False
+		
+		conn = self._connect()
+		if conn is None:
+			logging.error("getting username for card {}: Unable to open connection".format(card_base64))
+			return False
+		cmd = "AUTHCARD %s" % (card_base64,)
+		rsp = conn.send_command(cmd)
+		if not rsp.startswith("200 "):
+			logging.info("%s failed: Rejected card base64:%s: rsp %r" % (cmd, card_base64, rsp))
+			return False
+		username = rsp.split('=')[1].strip()
+		logging.info("Accepted card base64:%s for %s" % (card_base64,username,))
+
+		## Get UID for the username (not needed?)
+		#try:
+		#	# Get info from the system (by username)
+		#	info = pwd.getpwnam(username)
+		#except KeyError:
+		#	logging.info('getting info for user \'%s\': user not in password file' % (username,))
+		#	return False
+		#self._userid = info.pw_uid
+		self._userid = None
+		self._username = username
 
 		# If we get this far all is good
 		self._loggedIn = True
 		self._disabled = False
 		return True
 
-        def logOut(self):
-            self._loggedIn = False
-            self._disabled = False
-            self._userId = None
-            self._username = None
+	def logOut(self):
+		self._loggedIn = False
+		self._disabled = False
+		self._userId = None
+		self._username = None
 
 	def addCard(self, cardId):
 		if not self.isLoggedIn():
 			return False
-		if DISPSRV_MIFARE:
-			card_base64 = base64.b64encode(cardId)
-			if card_base64 in CARD_BLACKLIST:
-				logging.info("Blacklisted card base64:%s" % (card_base64,))
-				return False
-			logging.info('Enrolling card base64:%s to uid %s (%s)' % (card_base64, self._userId, self._username))
-			sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
-			sock.connect(DISPENSE_ENDPOINT)
-			sockf = sock.makefile()
-			sockf.write("AUTHIDENT\n")
-			sockf.flush(); rsp = sockf.readline()
-			assert "200" in rsp
-			sockf.write("SETEUSER %s\n" % (self._username,))
-			sockf.flush(); rsp = sockf.readline()
-			assert "200" in rsp
-			sockf.write("CARD_ADD %s\n" % (card_base64,))
-			sockf.flush(); rsp = sockf.readline()
-			if "200" in rsp:
-				return True
-			else:
-				return False
+		
+		card_base64 = base64.b64encode(cardId)
+		if card_base64 in CARD_BLACKLIST:
+			logging.info("Blacklisted card base64:%s" % (card_base64,))
+			return False
+		logging.info('Enrolling card base64:%s to uid %s (%s)' % (card_base64, self._userId, self._username))
+		conn = self._connect(set_euid=True)
+		if conn is None:
+			logging.warn("Enrolling card failed: Unable to connect".format(rsp))
+			return False
+		rsp = conn.send_command("CARD_ADD %s" % (card_base64,))
+		if "200" in rsp:
+			return True
 		else:
-			if get_uid(cardId) != None:
-				return False
-			else:
-				logging.info('Enrolling card %s to uid %s (%s)' % (cardId, self._userId, self._username))
-				set_card_id(self._userId, cardId)
-				return True
+			logging.warn("Enrolling card failed: Response = {}".format(rsp))
+			return False
 
 	def isLoggedIn(self):
 		return self._loggedIn
@@ -213,41 +202,92 @@ class OpenDispense(DispenseInterface):
 
 	def getBalance(self):
 		# Balance checking
-		if self.isLoggedIn():
-			acct, unused = Popen(['dispense', 'acct', self._username], close_fds=True, stdout=PIPE).communicate()
-		else:
-			return None
-		balance = acct[acct.find("$")+1:acct.find("(")].strip()
-		return balance
+		if not self.isLoggedIn():
+			return "?"
+		
+		conn = self._connect(authenticate=False)
+		if conn is None:
+			return "?"
+		cmd = "USER_INFO {}".format(self._username)
+		rsp = conn.send_command(cmd)
+		try:
+			code,rest = rsp.split(" ", 1)
+			if code != "202":
+				raise ValueError("Code not 202")
+			_user,_name,balance,flags = rest.split(" ")
+			return "{:.2f}".format(int(balance) / 100)
+		except ValueError as e:
+			logging.warn("OpenDispense: {!r} response malformed ({!r}) - exception {}".format(cmd, rsp, e))
+			return "?"
 
-	def getItemInfo(self, itemId):
+	def getItemInfo(self, itemId: str):
 		logging.debug("getItemInfo(%s)" % (itemId,))
 		itemId = OpenDispenseMapping.vendingMachineToOpenDispense(itemId)
-		args = ('dispense', 'iteminfo', itemId)
-		info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
-		m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
-		if m == None:
-			return("dead", 0)
-		cents = int(m.group(1))*100 + int(m.group(2))
-		# return (name, price in cents)
-		return (m.group(3), cents)
+
+		conn = self._connect(authenticate=False)
+		if conn is None:
+			return ("dead", 0,)
+		cmd = "ITEM_INFO {}".format(itemId)
+		rsp = conn.send_command(cmd)
+		try:
+			code,rest = rsp.split(" ", 1)
+			if code != "202":
+				raise ValueError("Code not 202")
+			_item,itemid,status,price_cents,name = rest.split(" ", 4)
+			return (name, int(price_cents),)
+		except ValueError as e:
+			logging.warn("OpenDispense: {!r} response malformed ({!r}) - exception {}".format(cmd, rsp, e))
+			return ("dead", 0,)
 
 	def isDisabled(self):
 		return self._disabled
 
 	def dispenseItem(self, itemId):
+		logging.debug("getItemInfo(%s)" % (itemId,))
 		if not self.isLoggedIn() or self.getItemInfo(itemId)[0] == "dead":
+			return "999"
+
+		conn = self._connect(set_euid=True)
+		if conn is None:
+			return "999"
+
+		cmd = "DISPENSE {}".format( OpenDispenseMapping.vendingMachineToOpenDispense(itemId) )
+		rsp = conn.send_command(cmd)
+		try:
+			code,rest = rsp.split(" ", 1)
+			return code
+		except ValueError as e:
+			logging.warn("OpenDispense: {!r} response malformed ({!r}) - exception {}".format(cmd, rsp, e))
+			return 999
+	def openDoor(self):
+		if not self.isLoggedIn():
 			return False
-		else:
-			print('dispense -u "%s" %s'%(self._username, itemId))
-			#os.system('dispense -u "%s" %s'%(self._username, itemId))
-			return True
 
-	def logOut(self):
-		self._username = ""
-		self._disabled = True
-		self._loggedIn = False
-		self._userId = None
+		conn = self._connect(set_euid=True)
+		if conn is None:
+			return False
+
+		cmd = "DISPENSE door:0".format()
+		rsp = conn.send_command(cmd)
+		try:
+			code,rest = rsp.split(" ", 1)
+			if code == "200":
+				return True
+			if code == "402":	# "Poor You"
+				return False
+			raise ValueError("Unknown code")
+		except ValueError as e:
+			logging.warn("OpenDispense: {!r} response malformed ({!r}) - exception {}".format(cmd, rsp, e))
+			return False
+
+class Connection(object):
+	def __init__(self, sockf):
+		self.sockf = sockf
+	def send_command(self, command):
+		self.sockf.write(command)
+		self.sockf.write("\n")
+		self.sockf.flush()
+		return self.sockf.readline()
 
 """
 This class abstracts the idea of item numbers.
diff --git a/VendServer/SerialClient.py b/VendServer/SerialClient.py
index e0a9a7e965e95fcd0b86c2eb24917b8a8f49c1e8..280109b8838f5628fd3ad4a4c088fc4de0a34d37 100644
--- a/VendServer/SerialClient.py
+++ b/VendServer/SerialClient.py
@@ -19,8 +19,8 @@ class SerialClient:
 		)
 	
 
-		self.rfh = self.ser
-		self.wfh = self.ser
+		self.rfh = ReadWrapper(self.ser)
+		self.wfh = WriteWrapper(self.ser)
 		self.wfh.write('B\n')
 
 	def get_fh(self):
@@ -28,6 +28,22 @@ class SerialClient:
 
 	def __del__(self):
 	    pass
+class WriteWrapper:
+    def __init__(self, fh):
+        self.fh = fh
+    def write(self, s: str):
+        return self.fh.write(s.encode('utf-8'))
+    def flush(self):
+        return self.fh.flush()
+class ReadWrapper:
+    def __init__(self, fh):
+        self.fh = fh
+    def fileno(self):
+        return self.fh.fileno()
+    def read(self, count) -> str:
+        return self.fh.read(count).decode('utf-8')
+    def readline(self) -> str:
+        return self.fh.readline().decode('utf-8')
 
 
 if __name__ == '__main__':
@@ -36,6 +52,6 @@ if __name__ == '__main__':
 	(rfh, wfh) = s.get_fh()
 
 	wfh.write('B\n')
-	print rfh.read()
+	print(rfh.read())
 
 
diff --git a/VendServer/SnackConfig.py b/VendServer/SnackConfig.py
deleted file mode 100755
index 231407480171c5078e55d3aaedc1e361e9a24bb5..0000000000000000000000000000000000000000
--- a/VendServer/SnackConfig.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python
-
-class VendingException( Exception ): pass
-
-import subprocess
-import os, re
-
-def get_snack( slot ):
-	
-	if slot == "--":
-		return (0, 'nothing', 'Nothing')
-	cmd = 'dispense iteminfo snack:%s' % slot
-#	print 'cmd = %s' % cmd
-	try:
-#		info = subprocess.check_output(["dispense","iteminfo",'snack:%s'%slot])
-		raw = os.popen(cmd)
-		info = raw.read()
-		raw.close()
-#		print 'cmd (2) = %s' % cmd
-#		print 'info = "%s"' % info
-		m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
-		val = ( int(m.group(1))*100 + int(m.group(2)), m.group(3), m.group(3) )
-#		print 'Price: %i, Name: %s' % (val[0], val[1])
-	except BaseException as e:
-		print "BaseException"
-		print e
-		val = (0, 'error', 'Error')
-	except:
-		print "Unknown exception"
-		val = (0, 'error', 'Error')
-	return val
-
-def get_price( slot ):
-		p, sn, n = get_snacks( slot )
-		return p
-
-def get_name( slot ):
-		p, sn, n = get_snacks( slot )
-		return n
-
-def get_short_name( slot ):
-		p, sn, n = get_snacks( slot )
-		return sn
-
-if __name__ == '__main__':
-	print "Don't run this"
diff --git a/VendServer/VendServer.py b/VendServer/VendServer.py
index cf9c8bbdbb689476b1c8fe584a16049691175b2f..f26c0e3c489c9a170b412e2ce32b8b7a88b35d40 100755
--- a/VendServer/VendServer.py
+++ b/VendServer/VendServer.py
@@ -3,23 +3,21 @@
 
 USE_MIFARE = 1
 
-import ConfigParser
+import configparser
 import sys, os, string, re, pwd, signal, math, syslog
 import logging, logging.handlers
 from traceback import format_tb
 from time import time, sleep, mktime, localtime
-from subprocess import Popen, PIPE
-from LATClient import LATClient, LATClientException
-from SerialClient import SerialClient, SerialClientException
-from VendingMachine import VendingMachine, VendingException
-from MessageKeeper import MessageKeeper
-from HorizScroll import HorizScroll
+#from .LATClient import LATClient, LATClientException
+from .SerialClient import SerialClient, SerialClientException
+from .VendingMachine import VendingMachine, VendingException
+from .MessageKeeper import MessageKeeper
+from .HorizScroll import HorizScroll
 from random import random, seed
-from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
-from SnackConfig import get_snack#, get_snacks
+from .Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
 import socket
 from posix import geteuid
-from OpenDispense import OpenDispense as Dispense
+from .OpenDispense import OpenDispense as Dispense
 import TracebackPrinter
 
 CREDITS="""
@@ -28,6 +26,7 @@ Bernard Blackham
 Mark Tearle
 Nick Bannon
 Cameron Patrick
+John Hodge
 and a collective of hungry alpacas.
 
 The MIFARE card reader bought to you by:
@@ -59,7 +58,7 @@ STATE_GETTING_UID,
 STATE_GETTING_PIN,
 STATE_GET_SELECTION,
 STATE_GRANDFATHER_CLOCK,
-) = range(1,8)
+) = list(range(1,8))
 
 TEXT_SPEED = 0.8
 IDLE_SPEED = 0.05
@@ -82,7 +81,7 @@ config_options = {
 class VendConfigFile:
 	def __init__(self, config_file, options):
 		try:
-			cp = ConfigParser.ConfigParser()
+			cp = configparser.ConfigParser()
 			cp.read(config_file)
 
 			for option in options:
@@ -90,7 +89,7 @@ class VendConfigFile:
 				value = cp.get(section, name)
 				self.__dict__[option] = value
 		
-		except ConfigParser.Error, e:
+		except configparser.Error as e:
 			raise SystemExit("Error reading config file "+config_file+": " + str(e))
 
 """
@@ -176,7 +175,7 @@ class VendServer():
 		messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
 		choice = int(random()*len(messages))
 		msg = messages[choice]
-		left = range(len(msg))
+		left = list(range(len(msg)))
 		for i in range(len(msg)):
 			if msg[i] == ' ': left.remove(i)
 		reveal = 1
@@ -198,9 +197,9 @@ class VendServer():
 	"""
 	Format text so it will appear centered on the screen.
 	"""
-	def center(self, str):
+	def center(self, str: str):
 		LEN = 10
-		return ' '*((LEN-len(str))/2)+str
+		return ' '*((LEN-len(str))//2)+str
 
 	"""
 	Configure the things that will appear on screen whil the machine is idling.
@@ -244,7 +243,7 @@ class VendServer():
 	"""
 	def reset_idler(self, t = None):
 		self.idler = GreetingIdler(self.v, t)
-		self.vstatus.time_of_next_idlestep = time()+self.idler.next()
+		self.vstatus.time_of_next_idlestep = time()+next(self.idler)
 		self.vstatus.time_of_next_idler = None
 		self.vstatus.time_to_autologout = None
 		self.vstatus.change_state(STATE_IDLE, 1)
@@ -287,7 +286,7 @@ class VendServer():
 		if self.idler.finished():
 			self.choose_idler()
 			self.vstatus.time_of_next_idler = time() + 30
-		nextidle = self.idler.next()
+		nextidle = next(self.idler)
 		if nextidle is None:
 			nextidle = IDLE_SPEED
 		self.vstatus.time_of_next_idlestep = time()+nextidle
@@ -307,7 +306,7 @@ class VendServer():
 	Don't do anything for this event.
 	"""
 	def do_nothing(self, event, params):
-		print "doing nothing (s,e,p)", state, " ", event, " ", params
+		print("doing nothing (s,e,p)", self.state, " ", event, " ", params)
 		pass
 
 	"""
@@ -412,11 +411,7 @@ class VendServer():
 		if self.vstatus.cur_selection == '55':
 			self.vstatus.mk.set_message('OPENSESAME')
 			logging.info('dispensing a door for %s'%self.vstatus.username)
-			if geteuid() == 0:
-				ret = os.system('dispense -u "%s" door'%self.vstatus.username)
-			else:
-				ret = os.system('dispense door')
-			if ret == 0:
+			if self.dispense.openDoor():
 				logging.info('door opened')
 				self.vstatus.mk.set_message(self.center('DOOR OPEN'))
 			else:
@@ -430,38 +425,36 @@ class VendServer():
 			self.vstatus.cur_selection = ''
 			return
 		elif self.vstatus.cur_selection[1] == '8':
+			# Drinks: No need to vend, just print a funny message and ask the server to vend from coke
+			logging.info('dispensing drink {} for {}'.format(self.vstatus.cur_selection, self.vstatus.username))
 			self.v.display('GOT DRINK?')
-			if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
+			if self.dispense.dispenseItem(self.vstatus.cur_selection) == "200":
 				self.v.display('SEEMS NOT')
 			else:
 				self.v.display('GOT DRINK!')
 		else:
-			# first see if it's a named slot
-			try:
-				price, shortname, name = get_snack( self.vstatus.cur_selection )
-			except:
-				price, shortname, name = get_snack( '--' )
+			logging.info('dispensing snack {} for {}'.format(self.vstatus.cur_selection, self.vstatus.username))
+			# Snacks: Show the name/price then dispense it
+			name, price = self.dispense.getItemInfo( self.vstatus.cur_selection )
 			dollarprice = "$%.2f" % ( price / 100.0 )
 			self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
-			exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
-			if (exitcode == 0):
+			status = self.dispense.dispenseItem(self.vstatus.cur_selection)
+			if status == "200":
 				# magic dispense syslog service
 				(worked, code, string) = self.v.vend(self.vstatus.cur_selection)
 				if worked:
 					self.v.display('THANK YOU')
 					syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
 				else:
-					print "Vend Failed:", code, string
+					print("Vend Failed:", code, string)
 					syslog.syslog(syslog.LOG_WARNING | syslog.LOG_LOCAL4, "vending %s (slot %s) for %s FAILED %r %r" % (name, self.vstatus.cur_selection, self.vstatus.username, code, string))
 					self.v.display('VEND FAIL')
-			elif (exitcode == 5):	# RV_BALANCE
+			elif status == "402":	# RV_BALANCE
 				self.v.display('NO MONEY?')
-			elif (exitcode == 4):	# RV_ARGUMENTS (zero give causes arguments)
-				self.v.display('EMPTY SLOT')
-			elif (exitcode == 1):	# RV_BADITEM (Dead slot)
+			elif status == "406":	# RV_BADITEM (Dead slot)
 				self.v.display('EMPTY SLOT')
 			else:
-				syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, self.vstatus.cur_selection, self.vstatus.username, exitcode))
+				syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %s)" % (name, self.vstatus.cur_selection, self.vstatus.username, status))
 				self.v.display('UNK ERROR')
 		sleep(1)
 
@@ -645,16 +638,16 @@ class VendServer():
 		### check for interesting times
 		now = localtime()
 
-		quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
-		halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
-		threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
-		fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
+		quarterhour      = mktime((now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]))
+		halfhour         = mktime((now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]))
+		threequarterhour = mktime((now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]))
+		fivetothehour    = mktime((now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]))
 
 		hourfromnow = localtime(time() + 3600)
 		
 		#onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
-		onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
-			0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
+		onthehour = mktime((hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
+			0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]))
 
 		## check for X seconds to the hour
 		## if case, update counter to 2
@@ -675,16 +668,16 @@ class VendServer():
 		### we live in interesting times
 		now = localtime()
 
-		quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
-		halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
-		threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
-		fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
+		quarterhour      = mktime((now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]))
+		halfhour         = mktime((now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]))
+		threequarterhour = mktime((now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]))
+		fivetothehour    = mktime((now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]))
 
 		hourfromnow = localtime(time() + 3600)
 		
 	#	onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
-		onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
-			0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
+		onthehour = mktime((hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
+			0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]))
 
 
 		#print "when it fashionable to wear a onion on your hip"
@@ -923,7 +916,7 @@ Connect to the machine.
 """
 def connect_to_vend(options, cf):
 
-	if options.use_lat:
+	if False and options.use_lat:
 		logging.info('Connecting to vending machine using LAT')
 		latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
 		rfh, wfh = latclient.get_fh()
@@ -964,7 +957,8 @@ def parse_args():
 	op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
 	op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
 	op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
-        op.add_option('--traceback-file', dest='traceback_file', default='', help='destination to print tracebacks when receiving SIGUSR1')
+	op.add_option('--traceback-file', dest='traceback_file', default='', help='destination to print tracebacks when receiving SIGUSR1')
+	op.add_option('--crash', action='store_true', help="Crash immediately instead of looping")
 	options, args = op.parse_args()
 
 	if len(args) != 0:
@@ -974,10 +968,10 @@ def parse_args():
 
 def create_pid_file(name):
 	try:
-		pid_file = file(name, 'w')
+		pid_file = open(name, 'w')
 		pid_file.write('%d\n'%os.getpid())
 		pid_file.close()
-	except IOError, e:
+	except IOError as e:
 		logging.warning('unable to write to pid file '+name+': '+str(e))
 
 def set_stuff_up():
@@ -1016,7 +1010,7 @@ def set_up_logging(options):
 			file_logger = logging.FileHandler(options.log_file)
 			file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
 			logger.addHandler(file_logger)
-		except IOError, e:
+		except IOError as e:
 			logger.warning('unable to write to log file '+options.log_file+': '+str(e))
 
 	if options.syslog != None:
@@ -1032,7 +1026,7 @@ def set_up_logging(options):
 		logger.setLevel(logging.INFO)
 
 def become_daemon():
-	dev_null = file('/dev/null')
+	dev_null = open('/dev/null')
 	fd = dev_null.fileno()
 	os.dup2(fd, 0)
 	os.dup2(fd, 1)
@@ -1041,14 +1035,14 @@ def become_daemon():
 		if os.fork() != 0:
 			sys.exit(0)
 		os.setsid()
-	except OSError, e:
+	except OSError as e:
 		raise SystemExit('failed to fork: '+str(e))
 
 def do_vend_server(options, config_opts):
 	while True:
 		try:
 			rfh, wfh = connect_to_vend(options, config_opts)
-		except (SerialClientException, socket.error), e:
+		except (SerialClientException, socket.error) as e:
 			(exc_type, exc_value, exc_traceback) = sys.exc_info()
 			del exc_traceback
 			logging.error("Connection error: "+str(exc_type)+" "+str(e))
@@ -1076,12 +1070,23 @@ def main(argv=None):
 			logging.error('Vend Server finished unexpectedly, restarting')
 		except KeyboardInterrupt:
 			logging.info("Killed by signal, cleaning up")
+			# NOTE: When debugging deadlocks, enable this
+			if options.crash:
+				(exc_type, exc_value, exc_traceback) = sys.exc_info()
+				tb = format_tb(exc_traceback, 20)
+				del exc_traceback
+				logging.info("Traceback:")
+				for event in tb:
+					for line in event.strip().split('\n'):
+						logging.critical('    '+line)
 			clean_up_nicely(options, config_opts)
 			logging.warning("Vend Server stopped")
 			break
 		except SystemExit:
 			break
 		except:
+			if options.crash:
+				raise
 			(exc_type, exc_value, exc_traceback) = sys.exc_info()
 			tb = format_tb(exc_traceback, 20)
 			del exc_traceback
@@ -1090,7 +1095,7 @@ def main(argv=None):
 			logging.critical("Message: " + str(exc_value))
 			logging.critical("Traceback:")
 			for event in tb:
-				for line in event.split('\n'):
+				for line in event.strip().split('\n'):
 					logging.critical('    '+line)
 			logging.critical("This message should be considered a bug in the Vend Server.")
 			logging.critical("Please report this to someone who can fix it.")
diff --git a/VendServer/VendingMachine.py b/VendServer/VendingMachine.py
index 72cec3dce2c63ba5be54f2a0f880a7507fa4f2c7..4a5026836b3b17861e3a2f7cc6968a2bb43ddafa 100644
--- a/VendServer/VendingMachine.py
+++ b/VendServer/VendingMachine.py
@@ -1,10 +1,10 @@
 # vim:ts=4
 import re
-from CRC import do_crc
+from .CRC import do_crc
 from select import select
 import socket, logging
 from time import time, sleep
-from MIFAREClient import MIFAREClient
+from .MIFAREClient import MIFAREClient
 
 asynchronous_responses = [	'400', '401', # door open/closed
 				'610',        # switches changed
@@ -72,7 +72,7 @@ class VendingMachine:
 					self.challenge = int(prefix, 16)
 					return
 
-	def get_response(self, async = False):
+	def get_response(self, is_async = False):
 		self.wfh.flush()
 		while True:
 			s = ''
@@ -85,7 +85,7 @@ class VendingMachine:
 			text = s[4:]
 			if code in asynchronous_responses:
 				self.handle_event(code, text)
-				if async: return None
+				if is_async: return None
 			else:
 				self.await_prompt()
 				return (code, text)
@@ -115,14 +115,15 @@ class VendingMachine:
 		else:
 			logging.warning('Unhandled event! (%s %s)\n'%(code,text))
 
-	def authed_message(self, message):
-		print 'self.challenge = %04x' % self.challenge
+	def authed_message(self, message: str):
 		if self.challenge == None:
+			print('self.challenge = None')
 			return message
+		print('self.challenge = %04x' % self.challenge)
 		crc = do_crc('%c%c'%(self.challenge >> 8, self.challenge & 0xff))
 		crc = do_crc(self.secret, crc)
 		crc = do_crc(message, crc)
-		print 'output = "%s|%04x"' % (message, crc)
+		print('output = "%s|%04x"' % (message, crc))
 		return message+'|'+('%04x'%crc)
 
 	def ping(self):
@@ -182,7 +183,7 @@ class VendingMachine:
 
 			(r, _, _) = select([self.rfh], [], [], this_timeout)
 			if r:
-				self.get_response(async = True)
+				self.get_response(is_async = True)
 				timeout = 0
 
 			if self.mifare: