diff --git a/exec/load/ax25defs.js b/exec/load/ax25defs.js
index ea413f21a26157a8a1a8b0e736ae6198e9d1e970..3f073afc2e69b75a748956e19b4b3b7b4a51d87f 100644
--- a/exec/load/ax25defs.js
+++ b/exec/load/ax25defs.js
@@ -4,58 +4,63 @@
 const AX25_FLAG = (1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6);	// Unused, but included for non-KISS implementations.
 
 // Address field - SSID subfield bitmasks
-const A_CRH = (1<<7);				// Command/Response or Has-Been-Repeated bit of an SSID octet
-const A_RR = (1<<5)|(1<<6);			// The "R" (reserved) bits of an SSID octet
-const A_SSID = (1<<1)|(1<<2)|(1<<3)|(1<<4);	// The SSID portion of an SSID octet
+const A_CRH		= (1<<7);						// Command/Response or Has-Been-Repeated bit of an SSID octet
+const A_RR		= (1<<5)|(1<<6);				// The "R" (reserved) bits of an SSID octet
+const A_SSID	= (1<<1)|(1<<2)|(1<<3)|(1<<4);	// The SSID portion of an SSID octet
 
 // Control field bitmasks
-const PF = (1<<4);			// Poll/Final
+const PF = (1<<4);					// Poll/Final
 const NR = (1<<5)|(1<<6)|(1<<7);	// N(R) - receive sequence number
 const NS = (1<<1)|(1<<2)|(1<<3);	// N(S) - send sequence number
 // 	Information frame
-const I_FRAME = 0;	// Derp
+const I_FRAME		= 0;
+const I_FRAME_MASK	= 1;
 // 	Supervisory frame and subtypes
-const S_FRAME = (1<<0);
-const S_FRAME_RR = S_FRAME;		// Receive Ready
-const S_FRAME_RNR = S_FRAME|(1<<2);	// Receive Not Ready
-const S_FRAME_REJ = S_FRAME|(1<<3);	// Reject
+const S_FRAME		= 1;
+const S_FRAME_RR	= S_FRAME;			// Receive Ready
+const S_FRAME_RNR	= S_FRAME|(1<<2);	// Receive Not Ready
+const S_FRAME_REJ	= S_FRAME|(1<<3);	// Reject
+const S_FRAME_MASK	= S_FRAME|S_FRAME_RR|S_FRAME_RNR|S_FRAME_REJ
 // 	Unnumbered frame and subtypes
-const U_FRAME = (1<<0)|(1<<1);
-const U_FRAME_SABM = U_FRAME|(1<<2)|(1<<3)|(1<<5);	// Set Asynchronous Balanced Mode
-const U_FRAME_DISC = U_FRAME|(1<<6);			// Disconnect
-const U_FRAME_DM = U_FRAME|(1<<2)|(1<<3);		// Disconnected Mode
-const U_FRAME_UA = U_FRAME|(1<<5)|(1<<6);		// Acknowledge
-const U_FRAME_FRMR = U_FRAME|(1<<2)|(1<<7);		// Frame Reject
-const U_FRAME_UI =  U_FRAME;				// Information
+const U_FRAME		= 3;
+const U_FRAME_SABM	= U_FRAME|(1<<2)|(1<<3)|(1<<5);	// Set Asynchronous Balanced Mode
+const U_FRAME_DISC	= U_FRAME|(1<<6);				// Disconnect
+const U_FRAME_DM	= U_FRAME|(1<<2)|(1<<3);		// Disconnected Mode
+const U_FRAME_UA	= U_FRAME|(1<<5)|(1<<6);		// Acknowledge
+const U_FRAME_FRMR	= U_FRAME|(1<<2)|(1<<7);		// Frame Reject
+const U_FRAME_UI	= U_FRAME;						// Information
+const U_FRAME_MASK	= U_FRAME|U_FRAME_SABM|U_FRAME_DISC|U_FRAME_DM|U_FRAME_UA|U_FRAME_FRMR;
 
 // Protocol ID field bitmasks (most are unlikely to be used, but are here for the sake of completeness.)
-const PID_X25 = (1<<0);								// ISO 8208/CCITT X.25 PLP
-const PID_CTCPIP = (1<<1)|(1<<2);						// Compressed TCP/IP packet. Van Jacobson (RFC 1144)
-const PID_UCTCPIP = (1<<0)|(1<<1)|(1<<2);					// Uncompressed TCP/IP packet. Van Jacobson (RFC 1144)
-const PID_SEGF = (1<<4);							// Segmentation fragment
-const PID_TEXNET = (1<<0)|(1<<1)|(1<<6)|(1<<7);					// TEXNET datagram protocol
-const PID_LQP = (1<<2)|(1<<6)|(1<<7);						// Link Quality Protocol
-const PID_ATALK = (1<<1)|(1<<3)|(1<<6)|(1<<7);					// Appletalk
-const PID_ATALKARP = (1<<0)|(1<<1)|(1<<3)|(1<<6)|(1<<7);			// Appletalk ARP
-const PID_ARPAIP = (1<<2)|(1<<3)|(1<<6)|(1<<7);					// ARPA Internet Protocol
-const PID_ARPAAR = (1<<0)|(1<<2)|(1<<3)|(1<<6)|(1<<7);				// ARPA Address Resolution
-const PID_FLEXNET = (1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7);				// FlexNet
-const PID_NETROM = (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7);			// Net/ROM
-const PID_NONE = (1<<4)|(1<<5)|(1<<6)|(1<<7);					// No layer 3 protocol implemented
-const PID_ESC	= (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6)|(1<<7);	// Escape character. Next octet contains more Level 3 protocol information.
+const PID_X25		= 1;											// ISO 8208/CCITT X.25 PLP
+const PID_CTCPIP	= (1<<1)|(1<<2);								// Compressed TCP/IP packet. Van Jacobson (RFC 1144)
+const PID_UCTCPIP	= (1<<0)|(1<<1)|(1<<2);							// Uncompressed TCP/IP packet. Van Jacobson (RFC 1144)
+const PID_SEGF		= (1<<4);										// Segmentation fragment
+const PID_TEXNET	= (1<<0)|(1<<1)|(1<<6)|(1<<7);					// TEXNET datagram protocol
+const PID_LQP		= (1<<2)|(1<<6)|(1<<7);							// Link Quality Protocol
+const PID_ATALK		= (1<<1)|(1<<3)|(1<<6)|(1<<7);					// Appletalk
+const PID_ATALKARP	= (1<<0)|(1<<1)|(1<<3)|(1<<6)|(1<<7);			// Appletalk ARP
+const PID_ARPAIP	= (1<<2)|(1<<3)|(1<<6)|(1<<7);					// ARPA Internet Protocol
+const PID_ARPAAR	= (1<<0)|(1<<2)|(1<<3)|(1<<6)|(1<<7);			// ARPA Address Resolution
+const PID_FLEXNET	= (1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7);			// FlexNet
+const PID_NETROM	= (1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<6)|(1<<7);	// Net/ROM
+const PID_NONE		= (1<<4)|(1<<5)|(1<<6)|(1<<7);					// No layer 3 protocol implemented
+const PID_ESC		= 255;											// Escape character. Next octet contains more Level 3 protocol information.
 
 // KISS protocol-related constants
 
 // 	FEND and transpositions
-const KISS_FEND = (1<<6)|(1<<7);				// Frame end
-const KISS_FESC = (1<<0)|(1<<1)|(1<<3)|(1<<4)|(1<<6)|(1<<7);	// Frame escape
-const KISS_TFEND = (1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7);		// Transposed frame end
-const KISS_TFESC = (1<<0)|(1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7);	// Transposed frame escape
+const KISS_FEND		= (1<<6)|(1<<7);								// Frame end
+const KISS_FESC		= (1<<0)|(1<<1)|(1<<3)|(1<<4)|(1<<6)|(1<<7);	// Frame escape
+const KISS_TFEND	= (1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7);			// Transposed frame end
+const KISS_TFESC	= (1<<0)|(1<<2)|(1<<3)|(1<<4)|(1<<6)|(1<<7);	// Transposed frame escape
 
-// 	Commands (SetHardware (0x06) excluded intentionally.)
-const KISS_DF = 0;		// Data frame
-const KISS_TXD = (1<<0);	// TX delay
-const KISS_P = (1<<1);		// Persistence
-const KISS_ST = (1<<0)|(1<<1);	// Slot time
-const KISS_TXT = (1<<2);	// TX tail
-const KISS_FD = (1<<0)|(1<<2);	// Full Duplex
+// 	Commands
+const KISS_DATAFRAME	= 0;	// Data frame
+const KISS_TXDELAY	 	= 1;	// TX delay
+const KISS_PERSISTENCE	= 2;	// Persistence
+const KISS_SLOTTIME		= 3;	// Slot time
+const KISS_TXTAIL		= 4;	// TX tail
+const KISS_FULLDUPLEX	= 5;	// Full Duplex
+const KISS_SETHARDWARE	= 6;	// Set Hardware
+const KISS_RETURN		= 255;	// Exit KISS mode
\ No newline at end of file
diff --git a/exec/load/kissAX25lib.js b/exec/load/kissAX25lib.js
index f5506be09e6869104c4a6eb69813707a63232e5f..cc972f5495d5af86acf827a6b116f0efabd57f8f 100644
--- a/exec/load/kissAX25lib.js
+++ b/exec/load/kissAX25lib.js
@@ -1,674 +1,1826 @@
-/*	kissAX25lib.js for Synchronet 3.15+
-	echicken -at- bbs.electronicchicken.com (VE3XEC)
+/*	kissAX25lib.js for Synchronet 3.16+
+	VE3XEC (echicken -at- bbs.electronicchicken.com)
 	
-	This library provides support for the KISS and AX.25 protocols.  AX.25
-	support is partial and needs much improvement.
+This library adds support for the KISS and AX.25 protocols to Synchronet BBS.
+You can find the latest copy on the Synchronet CVS at cvs.synchro.net.
 	
-	The following functions are available:
+Dependencies:
 	
-	loadKISSInterface(section)
-		- 'section' is the name of a section from ctrl/kiss.ini
-		- Returns a kissTNC object
-		
-	loadKISSInterfaces()
-		- Takes no arguments and loads all interfaces defined in ctrl/kiss.ini
-		- Returns an array of kissTNC objects
-		
-	logbyte(b)
-		- Adds a representation of an eight-bit byte to the log (eg. 00111100)
-		- Occasionally useful for debugging
-		
-	stringToByteArray(s)
-		- Converts string 's' into an array of ASCII values
-		- Returns an array with one element per character in 's'
-		
-	byteArrayToString(s)
-		- Converts byte array 's' (as above) into a string and returns it
-		
-	The following objects are available:
+	exec/load/ax25defs.js	- Masks for bitwise operations
+	ctrl/kiss.ini			- This is where your TNCs are defined and configured
 	
-	kissTNC(name, callsign, ssid, serialPort, baudRate)
-
-		Normally you'll use loadKISSInterface(section) to instantiate these,
-		however the arguments it accepts are obvious if you want to create
-		instances manually and bypass kiss.ini.
-		
-		Properties:
-		
-		'name'
-			A textual name for the TNC (a section heading from kiss.ini)
-		
-		'callsign'
-			The callsign assigned to this TNC
-		
-		'ssid'
-			The SSID assigned to this TNC
-			
-		'handle'
-			A COM object representing the serial connection to the TNC.  This
-			has its own methods for reading and writing data to and from the
-			TNC.
-		
-		Methods:
-		
-		getKISSFrame()
-			A bit of a misnomer.  What it actually does is read a KISS frame
-			from the TNC and then returns an array representing the bytes of
-			the AX.25 packet contained therein.
-			
-		sendKISSFrame(p)
-			Where 'p' is array containing the bytes of an AX.25 frame, this
-			will encapsulate that packet in a KISS frame and then send it to
-			the TNC for transmission.
-			
-		beacon()
-			Causes the TNC to transmit a UI frame containing the value of
-			system.name (the name of your BBS.)  I'll probably make the beacon
-			text configurable in kiss.ini later on.
-			
-	ax25packet()
+Objects, Properties, and Methods:
 	
-		Represents a single AX.25 frame, but is mostly devoid of properties
-		until either the assemble() or disassemble() methods are called.
-		
-		assemble(destination, destinationSSID, source, sourceSSID, repeaters, control, pid, information)
-			- 'repeaters' is the digipeater path, an array of 'callsign-ssid'
-			- 'control' is a control field octet from ax25defs.js, eg. U_FRAME
-			- 'pid' is a protocol ID octet from ax25defs.js (usually PID_NONE)
-			- 'information' is the payload of an I or UI frame, eg. the return
-			  value of stringToByteArray(s)
-		
-		disassemble(p)
-			'p' is an array containing the bytes of an AX.25 frame, eg. the
-			return value of kissTNC.getKISSFrame().
-		
-		logPacket()
-			Writes some information about this packet to the log.
-			
-		After assemble() or disassemble() has been called, your ax25packet
-		object will have the following properties:
-		
-			'destination'
-				The remote callsign
-			
-			'destinationSSID'
-				The remote SSID
-				
-			'source'
-				The local callsign
-			
-			'sourceSSID'
-				The local SSID
-			
-			'repeaters'
-				An array of 'callsign-ssid' representing the digipeater path
-			
-			'control'
-				The control octet, which can be compared bitwise against masks
-				defined in ax25defs.js
-				
-			'pid'
-				The protocol ID octet, which can be compared bitwise against
-				masks defined in ax25defs.js
-				
-			'information'
-				The payload if applicable (I or UI frames only,) good for use
-				with byteArrayToString(s)
-
-			'clientID'
-				A unique ID for the sender or recipient, for internal use
-				
-			'raw'
-				An array representing the bytes of the frame, good for use
-				with kissTNC.sendKISSFrame(p)
-				
-	ax25Client(destination, destinationSSID, source, sourceSSID, k)
-	
-		Can represent a client that has called you, but can also be used to
-		represent an outgoing session that you have initiated.
-		
-		All arguments but 'k' are self-explanatory. 'k' is a reference to a
-		kissTNC object (see above) and will be the interface through which all
-		traffic for this client shall pass.
-		
-		Properties:
-		
-		'kissTNC'
-			A reference to the kissTNC object representing the TNC through
-			which all traffic to and from this client passes
-		
-		'callsign'
-			The local callsign
-		
-		'ssid'
-			The local SSID
-			
-		'clientID'
-			A unique ID for this client, for internal use, same as
-			ax25packet.clientID
-			
-		'ssv'
-			The send-sequence variable
-			
-		'rsv'
-			The receive-sequence variable
-		
-		'ns'
-			Client's last reported N(S)
-			
-		'nr'
-			Client's last reported N(R)
-		
-		't1'
-			Timer T1 (Placeholder; not yet implemented)
+AX25 (Object)
+	
+	Contains settings that should be global to your entire application.  Is also
+	the parent of all other KISS & AX.25 related objects.
+	
+	Properties:
+	
+	logging	- Set to 'true' to log all packets in and out of all TNCs
+			  (This logging will be done at the DEBUG log level.)
+	tncs	- Object containing references to every loaded AX25.KISSTNC object
+	clients	- Object containing references to every loaded AX25.Client object
+	
+	Methods:
+	
+	loadTNC(tncName)
+	
+		Where 'tncName' is a section name from ctrl/kiss.ini, returns an
+		AX25.KISSTNC object, and adds that object to AX25.tncs
+	
+	AX25.loadAllTNCs()
 
-		't2'
-			Timer T2 (Placeholder; not yet implemented)
+		Populates AX25.tncs with an object for each section in ctrl/kiss.ini.	
+	
+AX25.KISSTNC(serialPort, baudRate, callsign, ssid)
+	
+	Creates a new AX25.KISSTNC object, eg:
+	var k = new AX25.KISSTNC("COM1", 9600, "MYCALL", 1);
+	A reference to this TNC will be placed in AX25.tncs.
+	
+	Arguments:
+	
+	serialPort	- Device name; "COM1", "/dev/ttyUSB0", etc.
+	baudRate	- The speed of the serial connection between your computer
+				  and your TNC.  1200, 9600, etc.
+	callsign	- Alphanumerics only
+	ssid		- A number between 0 and 15
+	name		- Whatver you want; usually a section name from kiss.ini
+	
+	Settings:
+	
+	serialPort	- As above
+	baudRate	- As above
+	callsign	- As above
+	ssid		- As above
+	timeout		- Timeout when reading a packet from the TNC, in milliseconds
+	txDelay		- The delay between the time when the TNC keys down the
+				  transmitter and when it sends audio, in milliseconds
+	txTail		- The delay between the time when the TNC stops sending
+				  audio and when it unkeys the transmitter, in milliseconds
+	persistence	- CSMA persistence, between 0 and 1 (default: 0.25)
+	slotTime	- CSMA slot time, in milliseconds
+	fullDuplex	- true/false (default: false)
+	
+	Properties:
+	
+	dataWaiting	- true if a packet is waiting to be read
+	dataPending	- true if a packet is waiting to be sent
+	
+	Methods:
+	
+	setHardware(command)	- Set a TNC-specific configuration option, where
+							  'command' is a number from 0 to 255.  Refer to
+							  your TNC documentation for valid 'command' values
+	
+	exitKISS()				- Take the TNC out of KISS mode
+	
+	receive()				- Returns an AX25.Packet object, or false if there
+							  are no packets waiting in the receive buffer
+	
+	send(packet)			- Adds AX25.Packet object 'packet' to the outgoing
+							  packet queue
+							  
+	cycle()					- Appends to the receive buffer any packets waiting
+							  to be read from the interface.  Sends to the
+							  interface any packets waiting in the outgoing
+							  queue.
+							  
+AX25.Packet(frame)
 
-		't3'
-			Timer T3 (Placeholder; not yet implemented)
+	Creates a new AX25.Packet object.
 
-		'resend'
-			true/false, for internal use
-			
-		'reject'
-			true/false, for internal use
-			
-		'wait'
-			If true, do not send any additional I frames to this client. (Your
-			scripts should check this value before deciding to send data to
-			the client.)
-			
-		'connected'
-			true/false, whether or not this client is connected
+	Arguments:
+	
+	frame	- Optional, and normally only supplied by kissTNC.getPacket(),
+			  which returns an AX25.Packet object to you.
+			  
+	Properties:
+	
+	destinationCallsign	- The callsign of the destination station (<= 6 chars)
+	destinationSSID		- The SSID of the destination station (0 - 15)
+	sourceCallsign		- The callsign of the source station (<= 6 chars)
+	sourceSSID			- The SSID of the source station (0 - 15)
+	repeaterPath		- An array of { callsign, ssid } objects
+	pollFinal			- True if the poll/final bit is set
+	command				- True if this is a command
+	response			- True if this is a response
+	control				- The control field (frame type with N(R), N(S), P/F
+						  set as applicable.) This property is read only. If
+						  you want to modify it, modify its constituent parts:
+						  .type, .nr, .ns, .pollFinal
+	type				- The control field with N(R), N(S), P/F left unset
+						  (Will match one of the frame types defined in
+						  ax25defs.js, eg. U_FRAME.)
+	nr					- The receive sequence number of an I or S frame
+	ns					- The send sequence number of an I frame
+	pid					- The PID field of an I or UI frame
+	info				- The info field of an I or UI frame
+	clientID			- If there was a client associated with this packet,
+						  this is what its ID would be.
+	
+	Methods:
+	
+	These methods are normally called indirectly by the kissTNC object's
+	getPacket() and sendPacket() methods.
+	
+	disassemble(frame)	- Where frame is an array of the bytes of a raw AX.25
+						  frame (less the start/stop flags and FCS), populates
+						  the above properties of this instance of the object
+						  with data from that frame. (If 'frame' is supplied
+						  when creating an AX25.Packet object, this method will
+						  be called automatically.)
+						  
+	assemble()			- Opposite of disassemble; returns an array of bytes
+						  of a raw AX.25 frame (less the start/stop flags and
+						  FCS) based on the properties of this instance of the
+						  object. 
+						  
+AX25.Client(tnc, packet)
 
-		'sentIFrames'
-			An array of the last seven I frames that we have sent to this
-			client.
+	Where 'tnc' is the AX25.KISSTNC object associated with this client, and the
+	optional 'packet' argument is a *received* AX25.Packet object.
 
-		'lastPacket'
-			An ax25packet object representing the last packet that we sent to
-			this client.
-			
-		Methods:
-		
-		receive(p)
-			'p' is an optional ax25packet object; if omitted, an attempt will
-			be made to read an AX.25 frame from the KISS TNC associated with
-			this client.  Returns false if there is no AX.25 frame waiting to
-			be received.
-		
-		sendPacket(a)
-			'a' is an ax25packet object that will be sent to the client via
-			its associated KISS TNC.
-		
-		send(p)
-			Sends an I frame to the client, where 'p' is the payload (eg. a
-			return value from stringToByteArray(s).
-		
-		connect()
-			Makes five attempts to connect to the client, at three second
-			intervals.  (Attempts and intervals to be made configurable at
-			some point.)  To verify the connection state afterward, check
-			the value of ax25client.connected.
-		
-		disconnect()
-			Makes five attempts to disconnect the client, at three second
-			intervals.  (Attempts and intervals to be made configurable at
-			some point.  To verify the connection state afterward, check the
-			value of ax25client.connected.
-		
-*/
+	Public Properties:
+	
+	callsign				- The callsign of the remote station
+	ssid					- The SSID of the remote station
+	id						- Unique ID for this client (can be compared against
+							  the 'clientID' property of an inbound packet to
+							  match an AX25.Packet with an existing AX25.Client.
+	connected				- true if the link is active
+	dataWaiting				- true if data is waiting in the receive buffer
+	
+	Public Methods:
+	
+	connect(callsign, ssid)	- Connect to station <callsign>-<ssid>, SSID will
+							  default to 0 if omitted.
+	disconnect()			- Terminate the link.
+	receivePacket(packet)	- Add 'packet' (ie. a packet object read from an
+							  AX25.KISSTNC object) to the incoming packet buffer
+							  to be processed (and responded to) the next time
+							  .cycle() is called.
+	send(array)				- Add 'array' to the send buffer.  Data from this
+							  buffer will be sent out in sequence when .cycle()
+							  is called.
+	sendString(string)		- Converts 'string' to an array of ASCII codes, then
+							  places that array in the send buffer to be sent
+							  out the next time .cycle() is called.
+	receive()				- Returns an array of eight bit binary integers, or
+							  false if no data is waiting to be read.
+	receiveString()			- Treats the next array of bytes in the receive
+							  buffer as an array of ASCII codes, converts that
+							  array into a string and returns it.
+	cycle()					- Sends out any pending data up to the maximum
+							  amount possible.  Processes any packets in the
+							  incoming packet buffer, populating the receive
+							  buffer with data as applicable.	*/
 
 load("ax25defs.js");
 
-// Return a kissTNC object (see below) based on 'section' of ctrl/kiss.ini
-function loadKISSInterface(section) {
+function logByte(b) {
+	log(
+		format(
+			"%d%d%d%d%d%d%d%d",
+			(b & (1<<7)) ? 1 : 0,
+			(b & (1<<6)) ? 1 : 0,
+			(b & (1<<5)) ? 1 : 0,
+			(b & (1<<4)) ? 1 : 0,
+			(b & (1<<3)) ? 1 : 0,
+			(b & (1<<2)) ? 1 : 0,
+			(b & (1<<1)) ? 1 : 0,
+			(b & (1<<0)) ? 1 : 0
+		)
+	);
+}
+
+/*	distanceBetween(leader, follower, modulus)
+	Find the difference between 'leader' and 'follower' modulo 'modulus'. */
+var distanceBetween = function(l, f, m) {
+	return (l < f) ? l + (m - f) : l - f;
+}
+
+/*	wrapAround(number, modulus)
+	If 'number' happens to be negative, it will be wrapped back around 'modulus'
+	eg. wrapAround(-1, 8) == 7. */
+var wrapAround = function(n, m) {
+	return (n < 0) ? m - Math.abs(n) : n;
+}
+
+/*	testCallsign(callsign) - boolean
+	Returns true if 'callsign' is a valid AX.25 callsign (a string
+	containing up to six letters and numbers only.) */
+var testCallsign = function(callsign) {
+	if(typeof callsign == "undefined" || callsign.length > 6)
+		return false;
+	callsign = callsign.toUpperCase().replace(/\s*$/g, "");
+	for(var c = 0; c < callsign.length; c++) {
+		var a = ascii(callsign[c]);
+		if(
+			(a >= 48 && a <= 57)
+			||
+			(a >=65 && a <=90)
+		) {
+			continue;
+		}
+		return false;
+	}
+	return true;
+}
+
+// Turns a string into an array of ASCII character codes
+function stringToByteArray(s) {
+	s = s.split("");
+	var r = new Array();
+	for(var i = 0; i < s.length; i++)
+		r.push(ascii(s[i]));
+	return r;
+}
+
+// Turns an array of ASCII character codes into a string
+function byteArrayToString(s) {
+	var r = "";
+	for(var i = 0; i < s.length; i++)
+		r += ascii(s[i]);
+	return r;
+}
+
+var AX25 = {
+
+	'logging'		: false,// Set true to log packets in and out of TNCs
+	'tncs'			: {}, 	// Indexed by AX25.KISSTNC.id
+	'clients'		: {}, 	// Indexed by AX25.Client.id/AX25.Packet.clientID
+
+	// I plan to make this setting more configurable at some point.
+	// Flag + Destination + Source + Repeaters + Control + PID + Info + Flag
+	'maximumPacketSize' : 8 + 56 + 56 + 448 + 8 + 8 + 2048 + 8 // 2640 Bits
+	
+};
+
+AX25.loadTNC = function(tncName) {
 	var f = new File(system.ctrl_dir + "kiss.ini");
 	f.open("r");
-	if(!f.exists || !f.is_open)
-		return false;
-	var kissINI = f.iniGetObject(section);
+	var tncIni = f.iniGetObject(tncName);
 	f.close();
-	var tnc = new kissTNC(section, kissINI.callsign, kissINI.ssid, kissINI.serialPort, Number(kissINI.baudRate));
+	if(tncIni == null) {
+		throw format(
+			"AX25: KISS TNC %s not found in %skiss.ini",
+			tncName, system.ctrl_dir
+		);
+	}
+	var tnc = new AX25.KISSTNC(
+		tncIni.serialPort,
+		tncIni.baudRate,
+		tncIni.callsign,
+		tncIni.ssid
+	);
+	for(var property in tncIni) {
+		if(
+			property == "serialPort"
+			||
+			property == "baudRate"
+			||
+			property == "callsign"
+			||
+			property == "ssid"
+		) {
+			continue;
+		}
+		if(tnc.hasOwnProperty(property))
+			tnc[property] = tncIni[property];
+		else
+			throw "AX25: Unknown TNC property " + property;
+	}
 	return tnc;
 }
 
-// Load and configure all KISS TNCs, return an array of kissTNC objects (see below)
-function loadKISSInterfaces() {
+AX25.loadAllTNCs = function() {
 	var f = new File(system.ctrl_dir + "kiss.ini");
 	f.open("r");
-	if(!f.exists || !f.is_open)
-		return false;
-	var kissINI = f.iniGetAllObjects();
+	var sections = f.iniGetSections();
 	f.close();
-	var kissTNCs = new Array();
-	for(var i = 0; i < kissINI.length; i++) {
-		kissTNCs.push(new kissTNC(kissINI[i].name, kissINI[i].callsign, kissINI[i].ssid, kissINI[i].serialPort, Number(kissINI[i].baudRate)));
+	if(typeof sections == "undefined" || sections == null) {
+		throw format(
+			"AX.25: Unable to read TNC configuration from %skiss.ini",
+			system.ctrl_dir
+		);
 	}
-	return kissTNCs;
+	for(var s = 0; s < sections.length; s++)
+		AX25.loadTNC(sections[s]);
 }
 
-// Create an object representing a KISS TNC, where object.handle is a COM object
-function kissTNC(name, callsign, ssid, serialPort, baudRate) {
-	this.name = name;
-	this.callsign = callsign;
-	this.ssid = ssid;
-	this.handle = new COM(serialPort);
-	this.handle.baud_rate = parseInt(baudRate);
-	this.handle.open();
-	if(!this.handle.is_open)
-		return false;
+AX25.KISSTNC = function(serialPort, baudRate, callsign, ssid) {
+	
+	var settings = {
+		'serialPort'	: "",
+		'baudRate'		: 0,
+		'callsign'		: "",
+		'ssid'			: 0,
+		'port'			: 0,
+		'timeout'		: 10000,
+		'txDelay'		: 500,
+		'persistence'	: 63,
+		'slotTime'		: 100,
+		'txTail'		: 0,
+		'fullDuplex'	: false
+	};
+	
+	var buffers = {
+		'send' 		: [],
+		'receive'	: []
+	};
+	
+	var com = new COM("");
+	
+	var sendCommand = function(command, value) {
+		com.sendBin(KISS_FEND, 1);
+		com.sendBin((((settings.port<<4)|command)<<8)|value, 2);
+		com.sendBin(KISS_FEND, 1);
+	}
+	
+	this.__defineGetter__(
+		"serialPort",
+		function() {
+			return settings.serialPort;
+		}
+	);
+	
+	this.__defineSetter__(
+		"serialPort",
+		function(serialPort) {
+			if(com.is_open)
+				com.close();
+			if(typeof serialPort == "undefined")
+				throw "Unable to open nonexistent serial port.";
+			com = new COM(serialPort);
+			com.open();
+			if(!com.is_open) {
+				throw format(
+					"Error opening serial port '%s': %s.",
+					serialPort,
+					com.last_error
+				);
+			}
+		}
+	);
+	
+	this.__defineGetter__(
+		"baudRate",
+		function() {
+			return settings.baudRate;
+		}
+	);
+	
+	this.__defineSetter__(
+		"baudRate",
+		function(baudRate) {
+			if(typeof baudRate == "undefined" || isNaN(Math.abs(baudRate))) {
+				throw format(
+					"Invalid baud rate: %s.",
+					baudRate
+				);
+			} else {
+				settings.baudRate = Math.abs(baudRate);
+				if(com.is_open)
+					com.close();
+				com.baud_rate = settings.baudRate;
+				com.open();
+				if(!com.is_open) {
+					throw format(
+						"Error setting baud rate '%s': %s",
+						baudRate,
+						com.last_error
+					);
+				}
+			}
+		}
+	);
+	
+	this.__defineGetter__(
+		"callsign",
+		function() {
+			return callsign;
+		}
+	);
+	
+	this.__defineSetter__(
+		"callsign",
+		function(callsign) {
+			if(typeof callsign == "undefined" || !testCallsign(callsign)) {
+				throw format(
+					"Invalid callsign: %s.",
+					callsign
+				);
+			} else {
+				settings.callsign = callsign;
+			}
+		}
+	);
+	
+	this.__defineGetter__(
+		"ssid",
+		function() {
+			return settings.ssid;
+		}
+	);
+	
+	this.__defineSetter__(
+		"ssid",
+		function(ssid) {
+			if(typeof ssid == "undefined" || isNaN(ssid))
+				throw "Invalid SSID.";
+			var ssid = Math.abs(ssid);
+			if(ssid > 15) {
+				throw format(
+					"Invalid SSID: %s",
+					ssid
+				);
+			}
+			settings.ssid = ssid;
+		}
+	);
+	
+	this.__defineGetter__(
+		"port",
+		function() {
+			return settings.port;
+		}
+	);
+	
+	this.__defineSetter__(
+		"port",
+		function(port) {
+			if(typeof port == "undefined" || isNaN(port) || port < 0 || port > 15)
+				throw "AX25.KISSTNC: Invalid TNC radio port assignment";
+			settings.port = port;
+		}
+	);
+	
+	this.__defineGetter__(
+		"id",
+		function() {
+			return md5_calc(
+				format(
+					"%s%s%s",
+					this.callsign,
+					this.ssid,
+					this.serialPort,
+					this.port
+				),
+				true
+			);
+		}
+	);
+
+	this.__defineGetter__(
+		"timeout",
+		function() {
+			return settings.timeout;
+		}
+	);
+	
+	this.__defineSetter__(
+		"timeout",
+		function(timeout) {
+			if(typeof timeout == "undefined" || isNaN(timeout))
+				throw "Invalid timeout argument.";
+			settings.timeout = timeout;
+		}
+	);
+	
+	this.__defineGetter__(
+		"txDelay",
+		function() {
+			return settings.txDelay;
+		}
+	);
+	
+	this.__defineSetter__(
+		"txDelay",
+		function(txDelay) {
+			if(typeof txDelay == "undefined" || isNaN(txDelay))
+				throw "Invalid txDelay argument.";
+			settings.txDelay = Math.abs(txDelay);
+			sendCommand(KISS_TXDELAY, this.txDelay / 10);
+		}
+	);
+	
+	this.__defineGetter__(
+		"persistence",
+		function() {
+			return settings.persistence / 255;
+		}
+	);
+	
+	this.__defineSetter__(
+		"persistence",
+		function(persistence) {
+			if(
+				typeof persistence == "undefined"
+				||
+				isNaN(persistence)
+				||
+				persistence > 1
+			) {
+				throw "Invalid persistence argument.";
+			}
+			settings.persistence = (persistence * 256) - 1;
+			sendCommand(KISS_PERSISTENCE, settings.persistence);
+		}
+	);
+	
+	this.__defineGetter__(
+		"slotTime",
+		function() {
+			return settings.slotTime;
+		}
+	);
+	
+	this.__defineSetter__(
+		"slotTime",
+		function(slotTime) {
+			if(typeof slotTime == "undefined" || isNaN(slotTime))
+				throw "Invalid slotTime argument.";
+			settings.slotTime = Math.abs(slotTime);
+			sendCommand(KISS_SLOTTIME, this.slotTime / 10);
+		}
+	);
+	
+	this.__defineGetter__(
+		"txTail",
+		function() {
+			return settings.txTail;
+		}
+	);
+	
+	this.__defineSetter__(
+		"txTail",
+		function(txTail) {
+			if(typeof txTail == "undefined" || isNaN(txTail))
+				throw "Invalid txTail argument.";
+			settings.txTail = Math.abs(txTail);
+			sendCommand(KISS_TXTAIL, this.txTail / 10);
+		}
+	);
+
+	this.__defineGetter__(
+		"fullDuplex",
+		function() {
+			return (settings.fullDuplex > 0) ? true : false;
+		}
+	);
+	
+	this.__defineSetter__(
+		"fullDuplex",
+		function(fullDuplex) {
+			if(typeof fullDuplex != "boolean")
+				throw "Invalid fullDuplex argument.";
+			settings.fullDuplex = fullDuplex;
+			sendCommand(KISS_FULLDUPLEX, (fullDuplex) ? 1 : 0);
+		}
+	);
+	
+	this.__defineGetter__(
+		"dataWaiting",
+		function() {
+			return (buffers.receive.length > 0) ? true : false;
+		}
+	);
+	
+	this.__defineGetter__(
+		"dataPending",
+		function() {
+			return (buffers.send.length > 0) ? true : false;
+		}
+	);
+	
+	this.setHardware = function(setting) {
+		if(typeof setting == "undefined" || isNaN(setting) || setting > 255)
+			throw "Invalid hardware setting.";
+		sendCommand(KISS_SETHARDWARE, setting);
+	}
+	
+	this.exitKISS = function() {
+		com.writeBin(KISS_FEND<<8|KISS_RETURN, 2);
+		com.writeBin(KISS_FEND, 1);
+	}
 
-	// Read a KISS frame from a TNC, return an AX.25 packet (array of bytes) less the flags and FCS
-	this.getKISSFrame = function() {
+	var getPacket = function() {
 		var escaped = false;
-		var kissByte = this.handle.readBin(2);
-		if(kissByte != (KISS_FEND<<8))
+		var kissByte = com.readBin(2);
+		if(kissByte != KISS_FEND<<8)
 			return false;
 		var kissFrame = new Array();
-		// To do: add a timeout to this loop
+		var startTime = time();
 		while(kissByte != KISS_FEND) {
-			kissByte = this.handle.readBin(1);
+			if(time() - startTime > settings.timeout) {
+				throw format(
+					"Timeout reading packet from KISS interface %s, %s-%s",
+					settings.name,
+					settings.callsign,
+					settings.ssid
+				);
+			}
+			kissByte = com.readBin(1);
 			if(kissByte == -1)
 				continue;
-			if((kissByte & KISS_FESC) == KISS_FESC) {
+			if((kissByte&KISS_FESC) == KISS_FESC) {
 				escaped = true;
 				continue;
 			}
-			if(escaped && (kissByte & KISS_TFESC) == KISS_TFESC) {
+			if(escaped && kissByte&KISS_TFESC == KISS_TFESC)
 				kissFrame.push(KISS_FESC);
-			} else if(escaped && (kissByte & KISS_TFEND) == KISS_TFEND) {
+			else if(escaped && kissByte&KISS_TFEND == KISS_TFEND)
 				kissFrame.push(KISS_FEND);
-			} else if(kissByte != KISS_FEND) {
+			else if(kissByte != KISS_FEND)
 				kissFrame.push(kissByte);
-			}
 			escaped = false;
 		}
-		return kissFrame;
-	}
+		
+		var packet = new AX25.Packet(kissFrame);
+		buffers.receive.push(packet);
 
-	// Write a KISS frame to a TNC where p is an AX.25 packet (array of bytes) less the flags and FCS
-	this.sendKISSFrame = function(p) {
-		if(p == undefined) return false;
+		if(AX25.logging)
+			packet.log();
+			
+		return true;
+	}
+	
+	var sendPacket = function() {
+		if(buffers.send.length == 0)
+			return false;
+		var packet = buffers.send.shift();
+		var kissFrame = packet.assemble();
 		var kissByte;
-		this.handle.writeBin((KISS_FEND<<8), 2);
-		for(var i = 0; i < p.length; i++) {
-			kissByte = p[i];
-			if(kissByte == KISS_FEND) {
-				this.handle.writeBin((KISS_FESC<<8)|KISS_TFEND, 2);
-			} else if(kissByte == KISS_FESC) {
-				this.handle.writeBin((KISS_FESC<<8)|KISS_TFESC, 2);
-			} else {
-				this.handle.writeBin(kissByte, 1);
-			}
+		com.writeBin((KISS_FEND<<8)|(settings.port<<4), 2);
+		for(var i = 0; i < kissFrame.length; i++) {
+			kissByte = kissFrame[i];
+			if(kissByte == KISS_FEND)
+				com.writeBin(KISS_FESC<<8|KISS_TFEND, 2);
+			else if(kissByte == KISS_FESC)
+				com.writeBin(KISS_FESC<<8|KISS_TFESC, 2);
+			else
+				com.writeBin(kissByte, 1);
 		}
-		this.handle.writeBin(KISS_FEND, 1);
+		com.writeBin(KISS_FEND, 1);
+		if(AX25.logging)
+			packet.log();
+		return true;
 	}
 	
-	this.beacon = function() {
-		var a = new ax25packet();
-		a.assemble("BEACON", 0, this.callsign, this.ssid, false, U_FRAME_UI, PID_NONE, stringToByteArray(system.name));
-		this.sendKISSFrame(a.raw);
+	this.receive = function() {
+		return (this.dataWaiting) ? buffers.receive.shift() : undefined;
 	}
-}
 	
-function logByte(b) {
-	log(format("%d%d%d%d%d%d%d%d\r\n", (b & (1<<7)) ? 1 : 0, (b & (1<<6)) ? 1 : 0, (b & (1<<5)) ? 1 : 0, (b & (1<<4)) ? 1 : 0, (b & (1<<3)) ? 1 : 0, (b & (1<<2)) ? 1 : 0, (b & (1<<1)) ? 1 : 0, (b & (1<<0)) ? 1 : 0 ));
+	this.send = function(packet) {
+		if(typeof packet == "undefined" || !(packet instanceof AX25.Packet))
+			throw "AX25.KISSTNC: Unable to send invalid packets";
+		buffers.send.push(packet);
+	}
+	
+	this.cycle = function() {
+		while(getPacket()) {
+		}
+		while(sendPacket()) {
+		}
+	}
+	
+	this.serialPort = serialPort;
+	this.baudRate = baudRate;
+	this.callsign = callsign;
+	this.ssid = ssid;
+	
+	AX25.tncs[this.id] = this;
 }
 
-function ax25packet() {
-
-	this.assemble = function(destination, destinationSSID, source, sourceSSID, repeaters, control, pid, information) {
-		this.destination = destination;
-		this.destinationSSID = destinationSSID;
-		this.source = source;
-		this.sourceSSID = sourceSSID;
-		this.repeaters = repeaters;
-		this.control = control;
-		this.pid = pid;
-		this.information = information;
-		this.clientID = this.destination.replace(/\s/, "") + this.destinationSSID + this.source.replace(/\s/, "") + this.sourceSSID;
-		this.raw = new Array();
-		var dest = stringToByteArray(this.destination);
-		for(var i = 0; i < dest.length; i++) {
-			this.raw.push((dest[i]<<1));
-		}
-		this.raw.push((parseInt(this.destinationSSID)<<1));
-		var src = stringToByteArray(this.source);
-		for(var i = 0; i < src.length; i++) {
-			this.raw.push((src[i]<<1));
-		}
-		if(!repeaters) {
-			this.raw.push((parseInt(this.sourceSSID)<<1)|(1<<0));
-		} else {
-			this.raw.push((parseInt(this.sourceSSID)<<1));
-			for(var i = 0; i < this.repeaters.length; i++) {
-				var repeater = this.repeaters[i].split("-");
-				var repeaterCall = stringToByteArray(repeater[0]);
-				for(var j = 0; j < repeaterCall.length; j++) {
-					this.raw.push((repeaterCall[j]<<1));
+AX25.Packet = function(frame) {
+
+	var properties = {
+		'destinationCallsign'	: "",
+		'destinationSSID'		: 0,
+		'sourceCallsign'		: "",
+		'sourceSSID'			: 0,
+		'repeaterPath'			: [ ],
+		'pollFinal'				: false,
+		'command'				: 0,
+		'response'				: 0,
+		'type'					: 0,
+		'nr'					: 0,
+		'ns'					: 0,
+		'pid'					: PID_NONE,
+		'info'					: [ ]
+	};
+	
+	this.__defineGetter__(
+		"destinationCallsign",
+		function() {
+			if(!testCallsign(properties.destinationCallsign))
+				throw "AX25.Packet: Invalid destination callsign in received packet.";
+			return properties.destinationCallsign;
+		}
+	);
+	
+	this.__defineSetter__(
+		"destinationCallsign",
+		function(callsign) {
+			if(typeof callsign == "undefined" || !testCallsign(callsign))
+				throw "AX25.Packet: Invalid destination callsign assignment.";
+			properties.destinationCallsign = callsign;
+		}
+	);
+	
+	this.__defineGetter__(
+		"destinationSSID",
+		function() {
+			if(properties.destinationSSID < 0 || properties.destinationSSID > 15)
+				throw "AX25.Packet: Invalid SSID in received packet.";
+			return properties.destinationSSID;
+		}
+	);
+	
+	this.__defineSetter__(
+		"destinationSSID",
+		function(ssid) {
+			if(typeof ssid == "undefined" || isNaN(ssid) || ssid < 0 || ssid > 15)
+				throw "AX25.Packet: Invalid destination SSID assignment.";
+			properties.destinationSSID = ssid;
+		}
+	);
+
+	this.__defineGetter__(
+		"sourceCallsign",
+		function() {
+			if(!testCallsign(properties.sourceCallsign))
+				throw "AX25.Packet: Invalid source callsign in received packet.";
+			return properties.sourceCallsign;
+		}
+	);
+	
+	this.__defineSetter__(
+		"sourceCallsign",
+		function(callsign) {
+			if(typeof callsign == "undefined" || !testCallsign(callsign))
+				throw "AX25.Packet: Invalid source callsign assignment.";
+			properties.sourceCallsign = callsign;
+		}
+	);
+	
+	this.__defineGetter__(
+		"sourceSSID",
+		function() {
+			if(properties.sourceSSID < 0 || properties.sourceSSID > 15)
+				throw "AX25.Packet: Invalid SSID in received packet.";
+			return properties.sourceSSID;
+		}
+	);
+	
+	this.__defineSetter__(
+		"sourceSSID",
+		function(ssid) {
+			if(typeof ssid == "undefined" || isNaN(ssid) || ssid < 0 || ssid > 15)
+				throw "AX25.Packet: Invalid source SSID assignment.";
+			properties.sourceSSID = ssid;
+		}
+	);
+	
+	this.__defineGetter__(
+		"repeaterPath",
+		function() {
+			return properties.repeaterPath;
+		}
+	);
+	
+	this.__defineSetter__(
+		"repeaterPath",
+		function(repeaters) {
+			if(typeof repeaters == "undefined" || !Array.isArray(repeaters))
+				throw "AX25.Packet: Repeater path must be an array of { callsign, ssid } objects.";
+			for(var r = 0; r < repeaters.length; r++) {
+				if(
+					!repeaters[r].hasOwnProperty('callsign')
+					||
+					!testCallsign(repeaters[r].callsign)
+				) {
+					throw "AX25.Packet: Repeater path must be an array of valid { callsign, ssid } objects.";
+				}
+				if(
+					!repeaters[r].hasOwnProperty('ssid')
+					||
+					repeaters[r].ssid < 0
+					||
+					repeaters[r].ssid > 15
+				) {
+					throw "AX25.Packet: Repeater path must be an array of valid { callsign, ssid } objects.";
 				}
-				var repeaterSSID = (parseInt(repeater[1])<<1);
-				if(i == this.repeaters.length - 1)
-					repeaterSSID |= (1<<0);
-				this.raw.push(repeaterSSID);
 			}
+			properties.repeaterPath = repeaters;
 		}
-		this.raw.push(this.control);
-		if(this.pid !== undefined)
-			this.raw.push(this.pid);
-		if(this.information !== undefined) {
-			for(var i = 0; i < this.information.length; i++) {
-				this.raw.push(this.information[i]);
-			}
-			this.ns = ((this.control & NS)>>>1);
+	);
+
+	this.__defineGetter__(
+		"pollFinal",
+		function() {
+			return (properties.pollFinal == 1) ? true : false;
 		}
-		if((this.control & I_FRAME) == I_FRAME || (this.control & S_FRAME) == S_FRAME)
-			this.nr = ((this.control & NR)>>>5);
-	}
+	);
+	
+	this.__defineSetter__(
+		"pollFinal",
+		function(pollFinal) {
+			if(typeof pollFinal != "boolean")
+				throw "AX25.Packet: Invalid poll/final bit assignment (should be boolean.)";
+			properties.pollFinal = (pollFinal) ? 1 : 0;
+		}
+	);
 
-	this.disassemble = function(p) {
-		this.raw = p;
-		this.destination = "";
-		for(var i = 0; i < 6; i++) {
-			this.destination += ascii((p[i]>>1));
-		}
-		this.destinationSSID = ((p[6] & A_SSID)>>1);
-		this.source = "";
-		for(var i = 7; i < 13; i++) {
-			this.source += ascii(p[i]>>1);
-		}
-		var i = 13;
-		this.sourceSSID = ((p[i] & A_SSID)>>1);
-		this.clientID = this.destination.replace(/\s/, "") + this.destinationSSID + this.source.replace(/\s/, "") + this.sourceSSID;
-		// Either the source callsign & SSID pair was the end of the address field, or we need to tack on a repeater path
-		if(p[i] & (1<<0)) {
-			this.repeaters = [0];
-		} else {
-			var repeater = "";
-			this.repeaters = new Array();
-			for(var i = 14; i <= 78; i++) {
-				if(repeater.length == 6) {
-					repeater += "-" + ((p[i] & A_SSID)>>1);
-					this.repeaters.push(repeater);
-					repeater = "";
-				} else {
-					repeater += ascii((p[i]>>1));
-				}
-				if(p[i] & (1<<0))
-					break;
+	this.__defineGetter__(
+		"command",
+		function() {
+			return (properties.command == 1) ? true : false;
+		}
+	);
+	
+	this.__defineSetter__(
+		"command",
+		function(command) {
+			if(typeof command != "boolean")
+				throw "AX25.Packet: Invalid command bit assignment (should be boolean.)";
+			properties.command = (command) ? 1 : 0;
+			properties.response = (command) ? 0 : 1;
+		}
+	);
+	
+	this.__defineGetter__(
+		"response",
+		function() {
+			return (properties.response == 1) ? true : false;
+		}
+	);
+	
+	this.__defineSetter__(
+		"response",
+		function(response) {
+			if(typeof response != "boolean")
+				throw "AX25.Packet: Invalid response bit assignment (should be boolean.)";
+			properties.response = (response) ? 1 : 0;
+			properties.command = (response) ? 0 : 1;
+		}
+	);
+	
+	/*	Assemble and return a control octet based on the properties of this
+		packet.  (Note that there is no corresponding setter - the control
+		field is always generated based on packet type, poll/final, and the
+		N(S) and N(R) values if applicable, and must always be fetched from
+		this getter. */
+	this.__defineGetter__(
+		"control",
+		function() {
+			var control = properties.type;
+			if(properties.type == I_FRAME || (properties.type&U_FRAME) == S_FRAME)
+				control|=(properties.nr<<5);
+			if(properties.type == I_FRAME)
+				control|=(properties.ns<<1);
+			if(this.pollFinal)
+				control|=(properties.pollFinal<<4);
+			return control;
+		}
+	);
+
+	this.__defineGetter__(
+		"type",
+		function() {
+			return properties.type;
+		}
+	);
+	
+	this.__defineSetter__(
+		"type",
+		function(type) {
+			if(typeof type == undefined || isNaN(type))
+				throw "AX25.Packet: Invalid frame type assignment.";
+			properties.type = type;
+		}
+	);
+	
+	this.__defineGetter__(
+		"nr",
+		function() {
+			return properties.nr;
+		}
+	);
+	
+	this.__defineSetter__(
+		"nr",
+		function(nr) {
+			if(typeof nr == "undefined" || isNaN(nr) || nr < 0 || nr > 7)
+				throw "AX25.Packet: Invalid N(R) assignment.";
+			properties.nr = nr;
+		}
+	);
+	
+	this.__defineGetter__(
+		"ns",
+		function() {
+			return properties.ns;
+		}
+	);
+	
+	this.__defineSetter__(
+		"ns",
+		function(ns) {
+			if(typeof ns == "undefined" || isNaN(ns) || ns < 0 || ns > 7)
+				throw "AX25.Packet: Invalid N(S) assignment.";
+			properties.ns = ns;
+		}
+	);
+	
+	this.__defineGetter__(
+		"pid",
+		function() {
+			if(properties.pid == 0)
+				return undefined;
+			else
+				return properties.pid;
+		}
+	);
+	
+	this.__defineSetter__(
+		"pid",
+		function(pid) {
+			if(typeof pid == undefined || isNaN(pid))
+				throw "AX25.Packet: Invalid PID field assignment.";
+			if(properties.type == I_FRAME || properties.type == U_FRAME_UI)
+				properties.pid = pid;
+			else
+				throw "AX25.Packet: PID can only be set on I and UI frames (set the frame type first, accordingly.)";
+		}
+	);
+	
+	this.__defineGetter__(
+		"info",
+		function() {
+			if(properties.info.length < 1)
+				return undefined;
+			else
+				return properties.info;
+		}
+	);
+	
+	this.__defineSetter__(
+		"info",
+		function(info) {
+			if(typeof info == "undefined")
+				throw "AX25.Packet: Invalid information field assignment.";
+			if(properties.type == I_FRAME || properties.type == U_FRAME)
+				properties.info = info;
+			else
+				throw "AX25.Packet: Info field can only be set on I and UI frames (set the frame type first, accordingly.)";
+		}
+	);
+	
+	/*	You can compare this value against the 'ID' properties of your
+		AX25.Client objects to route an inbound packet to the appropriate
+		client (or create a new one.)
+		
+		Example:
+		
+		if(AX25.clients.hasOwnProperty(packet.clientID))
+			AX25.clients[packet.clientID].receivePacket(packet);
+		else
+			var a = new AX25.client(tnc, packet); 						*/
+	this.__defineGetter__(
+		'clientID',
+		function() {
+			return md5_calc(
+				format(
+					"%s%s%s%s",
+					properties.sourceCallsign,
+					properties.sourceSSID,
+					properties.destinationCallsign,
+					properties.destinationSSID
+				),
+				true
+			);
+		}
+	);
+	
+	this.disassemble = function(frame) {
+	
+		// Address field
+		
+		// Address - destination subfield
+		var field = frame.splice(0, 6);
+		for(var f = 0; f < field.length; f++)
+			properties.destinationCallsign += ascii(field[f]>>1);
+		field = frame.shift();
+		properties.destinationSSID = (field&A_SSID)>>1;
+		properties.command = (field&A_CRH)>>7;
+		
+		// Address - source subfield
+		field = frame.splice(0, 6);
+		for(var f = 0; f < field.length; f++)
+			properties.sourceCallsign += ascii(field[f]>>1);
+		field = frame.shift();
+		properties.sourceSSID = (field&A_SSID)>>1;
+		properties.response = (field&A_CRH)>>7;
+		
+		// Address - repeater path
+		while(field&1 == 0) {
+			field = frame.splice(0, 6);
+			var repeater = {
+				'callsign' : "",
+				'ssid' : 0
+			};
+			for(var f = 0; f < field.length; f++)
+				repeater.callsign += ascii(field[f]>>1);
+			field = frame.shift();
+			repeater.ssid = (field&A_SSID)>>1;
+			properties.repeaterPath.push(repeater);
+		}
+		
+		// Control field
+		var control = frame.shift();
+		properties.pollFinal = (control&PF)>>4;
+		if((control&U_FRAME) == U_FRAME) {
+			properties.type = control&U_FRAME_MASK;
+			if(properties.type == U_FRAME_UI) {
+				properties.pid = frame.shift();
+				properties.info = frame;
 			}
+		} else if((control&U_FRAME) == S_FRAME) {
+			properties.type = control&S_FRAME_MASK;
+			properties.nr = (control&NR)>>5;
+		} else if((control&1) == I_FRAME) {
+			properties.type = I_FRAME;
+			properties.nr = (control&NR)>>5;
+			properties.ns = (control&NS)>>1;
+			properties.pid = frame.shift();
+			properties.info = frame;
+		} else {
+			// This shouldn't be possible
+			throw "Invalid packet.";
+		}
+		
+	};
+	
+	this.assemble = function() {
+	
+		// Try to catch a few obvious derps
+		if(properties.destinationCallsign.length == 0)
+			throw "AX25.Packet: Destination callsign not set.";
+		if(properties.sourceCallsign.length == 0)
+			throw "AX25.Packet: Source callsign not set.";
+		if(
+			properties.type == I_FRAME
+			&&
+			(
+				!properties.hasOwnProperty('pid')
+				||
+				!properties.hasOwnProperty('info')
+				|| properties.info.length == 0
+			)
+		) {
+			throw "I or UI frame with no payload.";
+		}
+		
+		var frame = [];
+		
+		// Address field
+
+		// Address - destination subfield - encode callsign and SSID
+		for(var c = 0; c < 6; c++) {
+			frame.push(
+				(
+					(properties.destinationCallsign.length - 1 >= c)
+					?
+					ascii(properties.destinationCallsign[c])
+					:
+					32
+				)<<1
+			);
+		}
+		frame.push((properties.command<<7)|(properties.destinationSSID<<1));
+
+		// Address - source subfield - encode callsign and SSID
+		for(var c = 0; c < 6; c++) {
+			frame.push(
+				(
+					(properties.sourceCallsign.length - 1 >= c)
+					?
+					ascii(properties.sourceCallsign[c])
+					:
+					32
+				)<<1
+			);
 		}
-		this.control = p[i + 1]; // Implementation can compare this against bitmasks from ax25defs.js to determine frame type.
-		// A U frame or an I frame will have a PID octet and an information field
-		if((this.control & U_FRAME_UI) == U_FRAME_UI || (this.control & U_FRAME_FRMR) == U_FRAME_FRMR || (this.control & I_FRAME) == I_FRAME) {
-			this.pid = p[i + 2];
-			this.information = new Array();
-			for(var x = i + 3; x < p.length; x++) {
-				this.information.push(p[x]);
+		frame.push(
+			(properties.response<<7)
+			|
+			(properties.sourceSSID<<1)
+			|
+			((properties.repeaterPath.length < 1) ? 1 : 0)
+		);
+
+		// Address - tack on a repeater path if one was specified
+		for(var r = 0; r < properties.repeaterPath.length; r++) {
+			for(var c = 0; c < 6; c++) {
+				frame.push(
+					(
+						(properties.repeaterPath[r].callsign.length - 1 >= c)
+						?
+						ascii(properties.repeaterPath[r].callsign[c])
+						:
+						32
+					)<<1
+				);
 			}
+			frame.push(
+				(properties.repeaterPath[r].ssid<<1)
+				|
+				((r == properties.repeaterPath.length - 1) ? 1 : 0)
+			);
+		}
+
+		// Control field
+		frame.push(this.control);
+
+		// PID field (I and UI frames only)
+		if(
+			properties.pid
+			&&
+			(properties.type == I_FRAME || properties.type == U_FRAME_UI)
+		) {
+			frame.push(properties.pid);
 		}
-		// An I frame will have N(S) (send-sequence) bits in the control octet
-		if((this.control & I_FRAME) == I_FRAME)
-			this.ns = ((this.control & NS)>>>1);
-		// An I frame or an S frame will have N(R) (receive sequence) bits in the control octet
-		if((this.control & I_FRAME) == I_FRAME || (this.control & S_FRAME) == S_FRAME)
-			this.nr = ((this.control & NR)>>>5);
+
+		// Info field (I and UI frames only)
+		if(
+			properties.info.length > 0
+			&&
+			(properties.type == I_FRAME || properties.type == U_FRAME_UI)
+		) {
+			for(var i = 0; i < properties.info.length; i++)
+				frame.push(properties.info[i]);
+		}
+		
+		return frame;
 	}
 	
-	this.logPacket = function() {
-		var x = "Unknown or unhandled frame type";
-		if((this.control & U_FRAME_SABM) == U_FRAME_SABM) {
-			x = "U_FRAME_SABM";
-		} else if((this.control & U_FRAME_UA) == U_FRAME_UA) {
-			x = "U_FRAME_UA";
-		} else if((this.control & U_FRAME_FRMR) == U_FRAME_FRMR) {
-			x = "U_FRAME_FRMR";
-		} else if((this.control & U_FRAME_DISC) == U_FRAME_DISC) {
-			x = "U_FRAME_DISC";
-		} else if((this.control & U_FRAME) == U_FRAME) {
-			x = "U_FRAME";
-		} else if((this.control & S_FRAME_REJ) == S_FRAME_REJ) {
-			x = "S_FRAME_REJ, N(R): " + this.nr;
-		} else if((this.control & S_FRAME_RNR) == S_FRAME_RNR) {
-			x = "S_FRAME_RNR, N(R): " + this.nr;
-		} else if((this.control & S_FRAME) == S_FRAME) {
-			x = "S_FRAME, N(R): " + this.nr;
-		} else if((this.control & I_FRAME) == I_FRAME) {
-			x = "I_FRAME, N(R): " + this.nr + ", N(S): " + this.ns;
-		}
-		log(LOG_DEBUG, this.source + "-" + this.sourceSSID + "->" + this.destination + "-" + this.destinationSSID + ": " + x);
+	this.log = function() {
+		var out = format(
+			"%s-%s -> %s-%s ",
+			this.sourceCallsign,
+			this.sourceSSID,
+			this.destinationCallsign,
+			this.destinationSSID
+		);
+		if(this.repeaterPath.length > 0) {
+			out += "via ";
+			for(var r = 0; r < this.repeaterPath.length; r++) {
+				out += format(
+					"%s-%s, ",
+					this.repeaterPath[r].callsign,
+					this.repeaterPath[r].ssid
+				);
+			}
+		}
+		switch(this.type) {
+			case U_FRAME_UI:
+				out += "UI";
+				break;
+			case U_FRAME_SABM:
+				out += "SABM";
+				break;
+			case U_FRAME_DISC:
+				out += "DISC";
+				break;
+			case U_FRAME_DM:
+				out += "DM";
+				break;
+			case U_FRAME_UA:
+				out += "UA";
+				break;
+			case U_FRAME_FRMR:
+				out += "FRMR";
+				break;
+			case S_FRAME_RR:
+				out += "RR";
+				break;
+			case S_FRAME_RNR:
+				out += "RNR";
+				break;
+			case S_FRAME_REJ:
+				out += "REJ";
+				break;
+			case I_FRAME:
+				out += "I";
+				break;
+			default:
+				// Unlikely
+				out += "Unknown frame type";
+				break;
+		}
+		
+		out += format(
+			", C: %s, R: %s, ",
+			(this.command) ? 1 : 0,
+			(this.response) ? 1 : 0
+		);
+		
+		out += format("PF: %s", (this.pollFinal) ? 1 : 0);
+		
+		if((this.control&U_FRAME) == S_FRAME || (this.control&1) == I_FRAME)
+			out += format(", NR: %s", this.nr);
+		
+		if((this.control&1) == I_FRAME)
+			out += format(", NS: %s,", this.ns);
+		
+		if(
+			(this.control&1) == I_FRAME
+			||
+			(this.control&U_FRAME_MASK) == U_FRAME_UI
+		) {
+			out += format(" PID: %s, Info: ", this.pid);
+			for(var i = 0; i < this.info.length; i++)
+				out += ascii(this.info[i]);
+		}
+		
+		log(7, out);
 	}
+	
+	if(typeof frame != "undefined")
+		this.disassemble(frame);
+	
 }
 
-// Turns a string into an array of ASCII character codes
-function stringToByteArray(s) {
-	s = s.split("");
-	var r = new Array();
-	for(var i = 0; i < s.length; i++) {
-		r.push(ascii(s[i]));
+AX25.Client = function(tnc, packet) {
+
+	if(!(tnc instanceof AX25.KISSTNC))
+		throw "AX25.Client: invalid 'tnc' argument";
+
+	var settings = {
+		'maximumRetries'	:	5,	// Maximum failed T1 or T3 polls (per timer)
+		'maximumErrors'		:	5,	// Maximum (FRMR) errors before disconnect
+		'timer3Interval'	:	60	// Period of inactivity before T3 poll
+	};
+	
+	var properties = {
+		'tnc'					: tnc,		// Reference to an AX25.KISSTNC obj.
+		'callsign'				: "",		// Callsign of the remote side
+		'ssid'					: 0,		// SSID of the remote side
+		'sendState'				: 0,		// N(S) of next I frame we'll send
+		'receiveState'			: 0,		// N(S) of next expected I frame
+		'remoteReceiveState'	: 0,		// Remote side's last reported N(R)
+		'timer1'				: 0,		// Waiting acknowledgement if not 0
+		'timer1Retries'			: 0,		// Count of unacknowledged T1 polls
+		'timer3Retries'			: 0,		// Count of failed keepalive polls
+		'errors'				: 0,		// Persistent across link resets
+		'repeaterPath'			: [],		// Same format as in AX25.Packet
+		'timer3'				: time(),	// Updated on receipt of any packet
+		'connected'				: false,	// True if link established
+		'connecting'			: false,	// True if we sent SABM
+		'disconnecting'			: false,	// True if we sent DISC
+		'remoteBusy'			: false,	// True if remote sent RNR
+		'rejecting'				: false,	// True if we sent FRMR
+		'sentReject'			: false		// True if we sent REJ
+	};
+	
+	var buffers = {
+		'send'		: [],	// Data to be sent in I frames
+		'receive'	: [],	// Data received in I frames
+		'outgoing'	: [],	// Actual packets to be sent
+		'incoming'	: [],	// Actual received packets
+		'sent'		: []	// I frames sent but not yet acknowledged
+	};
+	
+	/*	For internal use only.  Scripts can use AX25.Client.send() without
+		having to consult this property; data will only actually be sent when
+		AX25.Client.cycle() is called, and when flow control permits. */
+	this.__defineGetter__(
+		"canSend",
+		function() {
+			return (
+				properties.connected
+				&&
+				buffers.send.length > 0
+				&&
+				!properties.remoteBusy
+				&&
+				distanceBetween(
+					properties.sendState,
+					properties.remoteReceiveState,
+					8
+				) < 7
+			) ? true : false;
+		}
+	);
+	
+	this.__defineGetter__(
+		"dataWaiting",
+		function() {
+			return (buffers.receive.length > 0) ? true : false;
+		}
+	);
+	
+	this.__defineGetter__(
+		"connected",
+		function() {
+			return properties.connected;
+		}
+	);
+	
+	/*	When waiting for acknowledgement of (certain) sent packets, wait this
+		many seconds before polling the remote side again.
+		
+		You may wonder how I arrived at this formula (particularly the '* 20'.)
+		The AX.25 specification is deliberately vague about how long this should
+		be.  Their suggested duration is far too short, resulting in our side
+		pinging the remote station every couple of seconds, not leaving it
+		enough time to prepare and send a response, and causing us to reach our
+		retry limit in short order.  I find that ten times the round-trip time
+		of the largest possible frame seems to work without being excessively
+		long. ((Bits / baud rate) * hops to client) * 20. */
+	this.__defineGetter__(
+		"timer1Timeout",
+		function() {
+			return (
+				(
+					(AX25.maximumPacketSize / properties.tnc.baudRate)
+					*
+					(properties.repeaterPath.length + 1)
+				)
+				* 20
+			);
+		}
+	);
+	
+	this.__defineGetter__(
+		"callsign",
+		function() {
+			return properties.callsign;
+		}
+	);
+	
+	this.__defineSetter__(
+		"callsign",
+		function(callsign) {
+			if(typeof callsign == "undefined" || !testCallsign(callsign))
+				throw "AX25.Client: Invalid destination callsign assignment";
+			properties.callsign = callsign;
+		}
+	);
+	
+	this.__defineGetter__(
+		"ssid",
+		function() {
+			return properties.ssid;
+		}
+	);
+	
+	this.__defineSetter__(
+		"ssid",
+		function(ssid) {
+			if(typeof ssid == "undefined" || isNaN(ssid) || ssid < 0 || ssid > 15)
+				throw "AX25.Client: Invalid destination SSID assignment";
+			properties.ssid = ssid;
+		}
+	);
+	
+	/*	You can compare the 'clientID' property of an *inbound* packet against
+		this value to match it up with an existing AX25.Client object.
+		
+		Example:
+		
+		if(AX25.clients.hasOwnProperty(packet.clientID))
+			AX25.clients[packet.clientID].receivePacket(packet);
+		else
+			var a = new AX25.Client(tnc, packet); */
+	this.__defineGetter__(
+		"id",
+		function() {
+			return md5_calc(
+				format(
+					"%s%s%s%s",
+					properties.callsign,
+					properties.ssid,
+					properties.tnc.callsign,
+					properties.tnc.ssid
+				),
+				true
+			);
+		}
+	);
+	
+	// Resets properties that shouldn't persist after a link reset or disconnect
+	var resetVariables = function () {
+		properties.sendState			= 0;
+		properties.receiveState			= 0;
+		properties.remoteReceiveState	= 0;
+		properties.timer1				= 0;
+		properties.timer1Retries		= 0;
+		properties.timer3Retries		= 0;
+		properties.timer3				= time();
+		properties.remoteBusy			= false;
+		properties.sentReject			= false;
+		properties.connected			= false;
+		properties.connecting			= false;
+		properties.disconnecting		= false;
+	}
+	
+	// Returns a new AX25.Packet with the Address field pre-populated
+	var makePacket = function() {
+		var packet = new AX25.Packet();
+		packet.destinationCallsign = properties.callsign;
+		packet.destinationSSID = properties.ssid;
+		packet.sourceCallsign = properties.tnc.callsign;
+		packet.sourceSSID = properties.tnc.ssid;
+		if(properties.repeaterPath.length > 0)
+			packet.repeaterPath = properties.repeaterPath;
+		return packet;
+	}
+	
+	/*	Cancels or resets timer T1 as necessary, discards sent I frames that
+		have been acknowledged by the remote side. */
+	var receiveAcknowledgement = function(nr) {
+		if(nr == properties.remoteReceiveState)
+			return;
+		properties.timer1 = 0;
+		properties.timer1Retries = 0;
+		properties.remoteReceiveState = nr;
+		for(var i = 0; i < buffers.sent.length; i++) {
+			if(buffers.sent[i].ns != wrapAround((nr - 1), 8))
+				continue;
+			buffers.sent.splice(0, i + 1);
+		}
+		if(buffers.sent.length > 0)
+			properties.timer1 = time();
 	}
-	return r;
-}
 
-// Turns an array of ASCII character codes into a string
-function byteArrayToString(s) {
-	var r = "";
-	for(var i = 0; i < s.length; i++) {
-		r += ascii(s[i]);
+	/*	Sends 'packet' to the TNC.  If 'packet is an I frame, stuffs it into
+		buffers.sent so that it can be resent later if necessary, or discarded
+		once it's acknowledged. */
+	var sendPacket = function(packet) {
+		properties.tnc.send(packet);
+		if(packet.type == I_FRAME) {
+			buffers.sent.push(packet);
+			properties.timer1 = time();
+		}
 	}
-	return r;
-}
 
-// Create an AX.25 client object from an ax25packet() object
-function ax25Client(destination, destinationSSID, source, sourceSSID, k) {
-	this.kissTNC = k;
-	if(source == k.callsign && sourceSSID == k.ssid) {
-		this.callsign = destination;
-		this.ssid = destinationSSID;
-	} else {
-		this.callsign = source;
-		this.ssid = sourceSSID;	
+	/*	Shifts the next payload off of buffers.send, creates an I frame around
+		it, pushes that I frame into buffers.outgoing. 'command' and 'pollFinal'
+		are boolean toggles for the C, R, and P/F bits of the I frame. */
+	var sendIFrame = function(command, pollFinal) {
+		if(buffers.send.length == 0)
+			return false;
+		var info = buffers.send.shift();
+		var packet = makePacket();
+		packet.command = command;
+		packet.type = I_FRAME;
+		packet.nr = properties.receiveState;
+		packet.ns = properties.sendState;
+		packet.pollFinal = pollFinal;
+		packet.pid = PID_NONE;
+		packet.info = info;
+		properties.sendState = (properties.sendState + 1) % 8;
+		buffers.outgoing.push(packet);
+		return true;
 	}
-	this.clientID = destination.replace(/\s/, "") + destinationSSID + source.replace(/\s/, "") + sourceSSID;
-
-	this.init = function() {
-		this.ssv = 0; // Send Sequence Variable
-		this.rsv = 0; // Receive Sequence Variable
-		this.ns = 0; // Client's last reported N(S)
-		this.nr = 0; // Client's last reported N(R)
-		this.t1 = 0; // Timer T1
-		this.t2 = 0; // Timer T2
-		this.t3 = 0; // Timer T3
-		this.resend = false;
-		this.reject = false;
-		this.wait = false;
-		this.connected = false;
-		this.sentIFrames = [];
-		this.lastPacket = false;
-		this.expectUA = false;
+	
+	/*	Resends I frames (called when the remote side sends a REJ frame.)
+		receiveAcknowledgement() should be called on the receipt of any S or I
+		frame and (on receipt of a REJ frame) prior to calling this function.
+		That way, buffers.sent[0] will assuredly be the oldest of our sent I
+		frames that we're still waiting for acknowledgement on.	*/
+	var resendIFrames = function(nr) {
+		if(buffers.sent.length < 1 || buffers.sent[0].ns != nr)
+			return false;
+		for(var i = 0; i < buffers.sent.length; i++) {
+			buffers.sent[i].nr = properties.receiveState;
+			buffers.outgoing.push(buffers.sent[i]);
+		}
+		return true;
 	}
 	
-	this.init();
+	this.connect = function(callsign, ssid) {
+		resetVariables();
+		this.callsign = callsign;
+		this.ssid = (typeof ssid == "undefined") ? 0 : ssid;
+		var packet = makePacket();
+		packet.type = U_FRAME_SABM;
+		packet.command = true;
+		packet.pollFinal = true;
+		buffers.outgoing.push(packet);
+		properties.connecting = true;
+		properties.timer1 = time();
+	}
+	
+	this.disconnect = function() {
+		resetVariables();
+		for(var b in buffers)
+			buffers[b] = [];
+		var packet = makePacket();
+		packet.type = U_FRAME_DISC;
+		packet.command = true;
+		packet.pollFinal = true;
+		buffers.outgoing.push(packet);
+		properties.disconnecting = true;
+		properties.timer1 = time();
+	}
 
-	/*	Process and respond to ax25packet object 'p', returning false unless
-		'p' is an I frame, in which case the I frame payload will be returned
-		as an array of bytes.
-		
-		Argument 'p' is optional; if it is not supplied, this function will
-		try to read an AX.25 frame from the KISS interface associated with
-		this client. */
-	this.receive = function(p) {
-		if(p == undefined) {
-			var kissFrame = this.kissTNC.getKISSFrame();
-			if(!kissFrame)
-				return false;
-			var p = new ax25packet();
-			p.disassemble(kissFrame);
-			if(p.destination != this.kissTNC.callsign || p.destinationSSID != this.kissTNC.ssid)
-				return false;
-		}
-		p.logPacket();
-		var retval = false;
-		var a = new ax25packet;
-		if((p.control & U_FRAME_SABM) == U_FRAME_SABM) {
-			this.connected = true;
-			if(this.reject) {
-				this.reject = false;
-			} else {
-				this.init();
-				this.connected = true;
-			}
-			a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_UA);
-			log(LOG_INFO, this.kissTNC.callsign + "-" + this.kissTNC.ssid + ": Connection from " + this.callsign + "-" + this.ssid);
-		} else if((p.control & U_FRAME_DM) == U_FRAME_DM) {
-			this.connected = false;
-		} else if((p.control & U_FRAME_UA) == U_FRAME_UA) {
-			if(this.expectUA) {
-				this.expectUA = false;
-				if((this.lastPacket.control & U_FRAME_SABM) == U_FRAME_SABM)
-					this.connected = true;
-				if((this.lastPacket.control & U_FRAME_DISC) == U_FRAME_DISC)
-					this.connected = false;
-				return retval;
-			} else {
-				a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_SABM);
-				this.init();
-				this.expectUA = true;
-			}
-		} else if((p.control & U_FRAME_FRMR) == U_FRAME_FRMR && this.connected) {
-			a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_SABM);
-			this.init();
-		} else if((p.control & U_FRAME_DISC) == U_FRAME_DISC) {
-			a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_UA);
-			this.connected = false;
-			this.reject = false;
-		} else if((p.control & U_FRAME) == U_FRAME) {
-			if(p.hasOwnProperty("information") && p.information.length > 0)
-				retval = p.information;
-			return retval;
-		} else if(!this.connected) {
-			a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_DM);
-		} else if((p.control & S_FRAME_REJ) == S_FRAME_REJ) {
-			this.resend = true;
-			a = this.sentIFrames[p.nr - 1];
-			this.nr = p.nr;
-		} else if((p.control & S_FRAME_RNR) == S_FRAME_RNR) {
-			this.nr = p.nr - 1;
-		} else if((p.control & S_FRAME) == S_FRAME) {
-			// This is a Receive-Ready and an acknowledgement of all frames in the sequence up to client's N(R)
-			this.nr = p.nr;
-			if(this.ssv >= this.nr)
-				// We haven't exceeded the flow control window, so no need to wait before sending more I frames
-				this.wait = false;
-			if(p.nr == 7 && this.sentIFrames.length >= 7) {
-				// Client acknowledges the entire sequence, we can ditch our stored sent packets
-				this.sentIFrames = this.sentIFrames.slice(7);
-				return retval;
-			} else if(this.resend && p.nr < this.sentIFrames.length) {
-				a = this.sentIFrames[p.nr - 1];
-			} else if(this.resend && p.nr >= this.sentIFrames.length) {
-				this.resend = false;
-				return retval;
-			} else {
-				return retval;
-			}
-		} else if((p.control & I_FRAME) == I_FRAME) {
-			this.ns = p.ns;
-			this.nr = p.nr;
-			if(this.ssv >= this.nr)
-				this.wait = false;
-			if(p.ns != this.rsv) {
-				if(this.reject)
-					return retval;
-				// Send a REJ, requesting retransmission of the frame whose N(S) value matches our current RSV
-				a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (S_FRAME_REJ|(this.rsv<<5)));
-				this.reject = true;
-			} else if(p.information.length <= 256) {
-				// This is an actual, good and expected I frame
-				this.rsv++;
-				this.rsv = this.rsv % 8;
-				a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (S_FRAME_RR|(this.rsv<<5)));
-				if(p.hasOwnProperty("information") && p.information.length > 0)
-					retval = p.information;
-				this.reject = false;
-			} else {
-				// Send a FRMR with the offending control field, our RSV and SSV, and the "Z" flag to indicate an invalid N(R)
-				var i = [p.control, (this.rsv<<5)|(this.ssv<<1), 0];
-				if(p.information.length > 256)
-					i[2] = (1<<2);
-				if(p.nr != this.ssv)
-					i[2]|=(1<<3);
-				a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (U_FRAME_FRMR), PID_NONE, i);
-				this.reject = true;
+	this.cycleTimers = function() {
+
+		var packet = makePacket();
+		packet.pollFinal = true;
+		packet.command = true;
+	
+		if(
+			properties.connected
+			&&
+			(
+				properties.timer1Retries > settings.maximumRetries
+				||
+				properties.timer3Retries > settings.maximumRetries
+			)
+		) {
+			packet.type = U_FRAME_DM;
+			resetVariables();
+		} else if(
+			properties.timer1 > 0
+			&&
+			time() - properties.timer1 > this.timer1Timeout
+		) {
+			if(properties.connecting) {
+				packet.type = U_FRAME_SABM;
+			} else if(properties.disconnecting) {
+				packet.type = U_FRAME_DISC;
+			} else if(properties.connected) {
+				packet.type = S_FRAME_RR;
+				packet.nr = properties.receiveState;
 			}
+			properties.timer1Retries++;
+			properties.timer1 = time();
+		} else if(
+			properties.connected
+			&&
+			time() - properties.timer3 > settings.timer3Interval
+		) {
+			packet.type = S_FRAME_RR;
+			packet.nr = properties.receiveState;
+			properties.timer3Retries++;
+			properties.timer3 = time();
 		} else {
-			return retval;
+			packet = false;
 		}
-		this.sendPacket(a);
-		return retval;
+		
+		if(!packet || packet.type == I_FRAME)
+			return;
+		
+		buffers.outgoing.push(packet);
 	}
-
-	// Send ax25Packet object 'a' to an ax25Client
-	this.sendPacket = function(a) {
-		this.lastPacket = a;
-		this.kissTNC.sendKISSFrame(a.raw);
-		a.logPacket();
+	
+	this.send = function(info) {
+		if(typeof info == "undefined")
+			throw "AX25.Client: Tried to send an empty packet";
+		buffers.send.push(info);
+	}
+	
+	this.sendString = function(str) {
+		if(typeof str != "string")
+			throw "AX25.Client.sendString: Invalid string";
+		buffers.send.push(stringToByteArray(str));
 	}
 
-	// Send an I Frame to an ax25Client, with payload 'p'
-	this.send = function(p) {
-		var a = new ax25packet();
-		a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, (I_FRAME|(this.rsv<<5)|(this.ssv<<1)), PID_NONE, p);
-		this.sendPacket(a);
-		this.ssv++;
-		this.ssv = this.ssv % 8;
-		if(this.ssv < this.nr)
-			/*	If we send again, we will exceed the flow control window. We
-				should wait for the client to catch up before sending more. */
-			this.wait = true;
-		this.sentIFrames.push(a);
+	this.receive = function() {
+		if(!this.dataWaiting)
+			return undefined;
+		return buffers.receive.shift();
 	}
+	
+	this.receiveString = function() {
+		if(!this.dataWaiting)
+			return undefined;
+		return byteArrayToString(buffers.receive.shift());
+	}
+	
+	this.receivePacket = function(packet) {
+		if(typeof packet == "undefined" || !(packet instanceof AX25.Packet))
+			throw "AX25.Client: Tried to handle an invalid packet";
 
-	// Connect this client to an AX.25 host, 5 attempts at 3 second intervals
-	// (Attempts and intervals should probably be made configurable)
-	this.connect = function() {
-		var a = new ax25packet();
-		a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_SABM);
-		var i = 0;
-		while(!this.connected && i < 5){
-			this.sendPacket(a);
-			this.expectUA = true;
-			mswait(3000);
-			this.receive();
-			i++;
-		}
-		if(this.connected) {
-			log(LOG_INFO, this.kissTNC.callsign + "-" + this.kissTNC.ssid + " connected to " + this.callsign + "-" + this.ssid);
-		} else {
-			log(LOG_INFO, this.kissTNC.callsign + "-" + this.kissTNC.ssid + " failed to connect to " + this.callsign + "-" + this.ssid);
+		if(properties.callsign == "") {
+			properties.callsign = packet.sourceCallsign;
+			properties.ssid = packet.sourceSSID;
+			AX25.clients[this.id] = this;
 		}
+
+		buffers.incoming.push(packet);
+		properties.timer3 = time();
+		properties.timer3Retries = 0;
 	}
+	
+	this.handlePacket = function(packet) {
+		
+		// Update the repeater path, unsetting the H bit as we go
+		properties.repeaterPath = [];
+		for(var r = packet.repeaterPath.length - 1; r >= 0; r--) {
+			// Drop any packet that was meant for a repeater and not us
+			if(packet.repeaterPath[r].ssid&A_CRH == 0)
+				return false;
+			packet.repeaterPath[r].ssid|=(0<<7);
+			properties.repeaterPath.push(packet.repeaterPath[r]);
+		}
 
-	// Disconnect this client, 5 attempts at 3 second intervals
-	// (Attempts and intervals should probably be made configurable)
-	this.disconnect = function() {
-		var a = new ax25packet();
-		a.assemble(this.callsign, this.ssid, this.kissTNC.callsign, this.kissTNC.ssid, false, U_FRAME_DISC);
-		var i = 0;
-		while(this.connected && i < 5){
-			this.sendPacket(a);
-			this.expectUA = true;
-			mswait(3000);
-			this.receive();
-			i++;
-		}
-		if(this.connected) {
-			this.connected = false;
-			log(LOG_INFO, this.callsign + "-" + this.ssid + " failed to acknowledge U_FRAME_DISC from " + this.kissTNC.callsign + "-" + this.kissTNC.ssid);
+		var response = makePacket();
+		response.pollFinal = packet.pollFinal;
+		response.response = packet.command;
+		
+		if(!properties.connected) {
+		
+			switch(packet.type) {
+			
+				case U_FRAME_SABM:
+					resetVariables();
+					properties.connected = true;
+					response.type = U_FRAME_UA;
+					break;
+					
+				case U_FRAME_UA:
+					if(properties.connecting) {
+						resetVariables();
+						properties.connected = true;
+						response = false;
+						break;
+					} // Else, fall through to default
+					
+				case U_FRAME_UI:
+					buffers.receive.push(packet.info);
+					if(!packet.pollFinal) {
+						response = false;
+						break;
+					} // Else, fall through to default
+			
+				default:
+					response.type = U_FRAME_DM;
+					response.pollFinal = true;
+					break;
+					
+			}
+		
 		} else {
-			log(LOG_INFO, this.callsign + "-" + this.ssid + " disconnected from " + this.kissTNC.callsign + "-" + this.kissTNC.ssid);
+		
+			switch(packet.type) {
+			
+				case U_FRAME_SABM:
+					resetVariables();
+					properties.connected = true;
+					response.type = U_FRAME_UA;
+					break;
+
+				case U_FRAME_DISC:
+					resetVariables();
+					response.type = U_FRAME_UA;
+					break;
+					
+				case U_FRAME_UA:
+					if(properties.connecting || properties.disconnecting) {
+						resetVariables();
+						properties.connected = (properties.connecting) ? true : false;
+						response = false;
+						break;
+					} // Else, fall through to default
+					
+				case U_FRAME_UI:
+					buffers.receive.push(packet.info);
+					if(packet.pollFinal) {
+						response.type = S_FRAME_RR;
+						response.nr = properties.receiveState;
+					} else {
+						response = false;
+					}
+					break;
+					
+				case U_FRAME_DM:
+					resetVariables();
+					response = false;
+					break;
+					
+				case U_FRAME_FRMR:
+					properties.errors++;
+					if(errors >= settings.maximumErrors) {
+						response.type = U_FRAME_DISC;
+						properties.disconnecting = true;
+					} else {
+						response.type = U_FRAME_SABM;
+						properties.connecting = true;
+					}
+					properties.timer1 = time();
+					break;
+					
+				case S_FRAME_RR:
+					properties.remoteBusy = false;
+					receiveAcknowledgement(packet.nr);
+					if(packet.pollFinal && packet.command) {
+						response.type = S_FRAME_RR;
+						response.nr = properties.receiveState;
+					} else {
+						response = false;
+					}
+					break;
+					
+				case S_FRAME_RNR:
+					properties.remoteBusy = true;
+					receiveAcknowledgement(packet.nr);
+					if(packet.pollFinal) {
+						response.type = S_FRAME_RR;
+						response.nr = properties.receiveState;
+					} else {
+						response = false;
+					}
+					break;
+					
+				case S_FRAME_REJ:
+					properties.remoteBusy = false;
+					receiveAcknowledgement(packet.nr);
+					if(packet.pollFinal) {
+						response.type = S_FRAME_RR;
+						response.nr = properties.receiveState;
+						buffers.outgoing.push(response);
+					}
+					if(!resendIFrames(packet.nr)) {
+						response.type = U_FRAME_SABM;
+						properties.connecting = true;
+					} else {
+						response = false;
+					}
+					break;
+					
+				case I_FRAME:
+					receiveAcknowledgement(packet.nr);
+					if(packet.ns != properties.receiveState) {
+						if(packet.pollFinal || !properties.sentReject) {
+							response.type = S_FRAME_REJ;
+							response.command = true;
+							response.nr = properties.receiveState;
+							properties.sentReject = true;
+						} else {
+							response = false;
+						}
+					} else {
+						properties.sentReject = false;
+						properties.receiveState = (properties.receiveState + 1) % 8;
+						buffers.receive.push(packet.info);
+						if(this.canSend && !packet.pollFinal) {
+							sendIFrame(false, false);
+							response = false;
+						} else {
+							response.type = S_FRAME_RR;
+							response.nr = properties.receiveState;
+						}
+					}
+					break;
+					
+				default:
+					response = false;
+					break;
+					
+			}
+			
+		}
+		
+		if(response)
+			buffers.outgoing.push(response);
+
+	}
+	
+	this.cycle = function() {
+	
+		// Process any incoming packets and queue up responses
+		while(buffers.incoming.length > 0) {
+			this.handlePacket(buffers.incoming.shift());
+		}
+		
+		this.cycleTimers();
+		
+		// Stuff any outgoing I frames into the buffer
+		while(this.canSend) {
+			if(!sendIFrame(true, false))
+				break;
 		}
+		
+		// Send any outgoing packets to the TNC
+		while(buffers.outgoing.length > 0) {
+			sendPacket(buffers.outgoing.shift());
+		}
+		
 	}
-}
+	
+	/*	If this object was instantiated with a 'packet' argument, we assume that
+		it's a packet received from the remote side and stuff it into the
+		incoming buffer.  When .cycle() is first called, that packet will be
+		processed and responded to. */
+	if(typeof packet != "undefined")
+		this.receivePacket(packet);
+
+}
\ No newline at end of file