MUD on an Arduino

March 19, 2010

While waiting for my parts for my robot I started a little project with my Arduino.  The original idea was to make a little irc bot based on the code at http://vimeo.com/1622823

The initial project started out as a simple led on / off just like what was shown.  The code was simple for the Python script and the Arduino code was very simple.  I wanted to make a more interactive experience so the idea of doing a mini-mud came to me.

After a few iterations it became clear that IRC was not perfect for this.  The first problem was the automatic lag that occurs if you try to send too many messages at once.  This caused the bot to appear unresponsive but it was actually just waiting for the IRC to send the messages to the end user.  On a multi-user system this didn’t work at all.

Looking at the xmpppy libraries in Python it only took an hour to get a basic client that would auto-accept new invites from people and echo messages in and out.  Adding the serial communication was a trivial addition that worked immediately.  The Jabber protocol was much better for the application since you could quickly send messages with \r\n to break up the lines for a nice display.

Memory issues are the primary problems with this project.  Mostly storing the user information in RAM.  The max size for a Jabber JID is 1023 bytes which with a couple users would entire fill the available memory.  To overcome this I hashed the username into a 1 byte hash using a pearson hash.  This isn’t perfect as there are quite a few collisions.  I am going to be adding a USB host controller to the Arduino so I can plug in a 4gb flash disk which will fix all of this and also allow for users to be saved.  I don’t want to use the EEPROM for this since it wouldn’t be enough memory to be useful when you consider saving username, JID, experience, item id’s, room location and a host of other nice to haves.

The final step after solving the memory issues is to get an Ethernet shield with a PoE (Powered over Ethernet) device so it can be run simply by plugging it into an Ethernet cable on a LAN.  To do this I will need to use a simple XML parser which will work fine but will slow things down.  Since I won’t be able to power two Arduinos like this without external power it will just have to be a bit slower once it has to parse the xmpp protocol directly.

The primary data that it would have to parse looks like the XML below.

Inbound data:
<message type="chat" id="somesourceid" to="rpgduino@gmail.com/123123" from="somerecipient@gmail.com/xyz123"><body>inbound message to Arduino</body><body xmlns="http://www.w3.org/1999/xhtml">source message</body><nos:x value="disabled" xmlns:nos="google:nosave"/><arc:record otr="false" xmlns:arc="http://jabber.org/protocol/archive"/></message>
Outbound data:
<message to="recipientaddr@gmail.com/xyz123" id="19"><body>Some data out</body></message>

Python Code (Arduinos.py):

## Copyright (c) 2009 Benjamin Eckel
##
## This is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## with this; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
import serial

class Arduino():

	def __init__(self, path='/dev/cu.usbserial-A600dZBk', baud=115200):
		self.ser = serial.Serial(path, baud)	

	def send(self, data):
		self.ser.write(data)

	def read(self, bytes):
		while (1):
			if (self.ser.inWaiting() > bytes-1):
				return self.ser.read(bytes)

	def flush():
		self.ser.flushInput()
		self.ser.flushInput()

Python Code (gtalk.py):

import xmpp, sys
from Arduinos import Arduino

class rpgduinotransport:

	def __init__(self,login,pwd):
		self.arduino = Arduino()
		self.conn = xmpp.Client('gmail.com')
		self.conn.connect( server=('talk.google.com',5223) )

		self.conn.auth(login,pwd, 'Test')

		self.conn.RegisterHandler('message',self.messageCB)
		self.conn.RegisterHandler('presence',self.subscribe)
		self.conn.sendInitPresence()

	def subscribe(self, conn, mess):
		type = mess.getType()
		user=mess.getFrom()
		if type == 'subscribe':
			self.conn.Roster.Authorize(user)
			self.conn.send( xmpp.Message( user ,"Welcome to the game.  Type 'Help' for more information." ) )

	def messageCB(self,conn, mess):
		type = mess.getType()
		fromjid = mess.getFrom().getStripped()

		if type in ['message', 'chat', None]: #and fromjid == self.remotejid:
			text=mess.getBody()
			user=mess.getFrom()
			user.lang='en'      # dup
			if text is not None:
				print("user:" + str(mess.getType()))
				print("user:" + str(user))
				print("text:" + str(text))
				#if " " in text:
				#	command, args = text.split(" ", 1).join(";")
				#else:
				#	command, text = text, ""

				#command = command.lower()
				print "Command:" + str(text)
				self.conn.send( xmpp.Message( user , self.dorequest(str(user)+";"+text.replace(" ",";"))))

	def StepOn(self):
		try:
			self.conn.Process(1)
		except KeyboardInterrupt: return 0
		return 1

	def GoOn(self):
		while self.StepOn():
			pass

	def dorequest(self, data):
		self.arduino.send('~' + data + '~') # header
		data = "";
		while 1 == 1:
			tmpdata = self.arduino.read(1)
			if tmpdata == '|':
				print data
				return data
			else:
				data += tmpdata
			#if len(data) > 1500:
			#	print data
			#	return data
		return

rpgbot = rpgduinotransport('rpgduino','somepassword')
rpgbot.GoOn()

Arduino Code:

#include <Flash.h>

#include <ctype.h>

#include <avr/pgmspace.h>

#include <string.h>

#define MAX_CHARACTERS 10

#define LED 13

uint8_t * heapptr, * stackptr;

/***********

 * Defines for all flash memory data.

 */

FLASH_TABLE(int, room_data_table, 4, 

    {2, 5, 0, 0}, {3, 1, 4, 0}, {0, 2, 0, 0}, {0, 0, 0, 2},

    {1, 0, 0, 0});

prog_char pearsondata[] PROGMEM = {

  0x00, 0x77, 0xee, 0x99, 0x07, 0x70, 0xe9, 0x9e, 0x0e, 0x79, 0xe0, 0x97,

  0x09, 0x7e, 0xe7, 0x90, 0x1d, 0x6a, 0xf3, 0x84, 0x1a, 0x6d, 0xf4, 0x83,

  0x13, 0x64, 0xfd, 0x8a, 0x14, 0x63, 0xfa, 0x8d, 0x3b, 0x4c, 0xd5, 0xa2,

  0x3c, 0x4b, 0xd2, 0xa5, 0x35, 0x42, 0xdb, 0xac, 0x32, 0x45, 0xdc, 0xab,

  0x26, 0x51, 0xc8, 0xbf, 0x21, 0x56, 0xcf, 0xb8, 0x28, 0x5f, 0xc6, 0xb1,

  0x2f, 0x58, 0xc1, 0xb6, 0x76, 0x01, 0x98, 0xef, 0x71, 0x06, 0x9f, 0xe8,

  0x78, 0x0f, 0x96, 0xe1, 0x7f, 0x08, 0x91, 0xe6, 0x6b, 0x1c, 0x85, 0xf2,

  0x6c, 0x1b, 0x82, 0xf5, 0x65, 0x12, 0x8b, 0xfc, 0x62, 0x15, 0x8c, 0xfb,

  0x4d, 0x3a, 0xa3, 0xd4, 0x4a, 0x3d, 0xa4, 0xd3, 0x43, 0x34, 0xad, 0xda,

  0x44, 0x33, 0xaa, 0xdd, 0x50, 0x27, 0xbe, 0xc9, 0x57, 0x20, 0xb9, 0xce,

  0x5e, 0x29, 0xb0, 0xc7, 0x59, 0x2e, 0xb7, 0xc0, 0xed, 0x9a, 0x03, 0x74,

  0xea, 0x9d, 0x04, 0x73, 0xe3, 0x94, 0x0d, 0x7a, 0xe4, 0x93, 0x0a, 0x7d,

  0xf0, 0x87, 0x1e, 0x69, 0xf7, 0x80, 0x19, 0x6e, 0xfe, 0x89, 0x10, 0x67,

  0xf9, 0x8e, 0x17, 0x60, 0xd6, 0xa1, 0x38, 0x4f, 0xd1, 0xa6, 0x3f, 0x48,

  0xd8, 0xaf, 0x36, 0x41, 0xdf, 0xa8, 0x31, 0x46, 0xcb, 0xbc, 0x25, 0x52,

  0xcc, 0xbb, 0x22, 0x55, 0xc5, 0xb2, 0x2b, 0x5c, 0xc2, 0xb5, 0x2c, 0x5b,

  0x9b, 0xec, 0x75, 0x02, 0x9c, 0xeb, 0x72, 0x05, 0x95, 0xe2, 0x7b, 0x0c,

  0x92, 0xe5, 0x7c, 0x0b, 0x86, 0xf1, 0x68, 0x1f, 0x81, 0xf6, 0x6f, 0x18,

  0x88, 0xff, 0x66, 0x11, 0x8f, 0xf8, 0x61, 0x16, 0xa0, 0xd7, 0x4e, 0x39,

  0xa7, 0xd0, 0x49, 0x3e, 0xae, 0xd9, 0x40, 0x37, 0xa9, 0xde, 0x47, 0x30,

  0xbd, 0xca, 0x53, 0x24, 0xba, 0xcd, 0x54, 0x23, 0xb3, 0xc4, 0x5d, 0x2a,

  0xb4, 0xc3, 0x5a, 0x2d};

prog_char helptext1[] PROGMEM = "Commands\r\n *go* - turns on the LED. \r\n*stop* - turns off the LED.  \r\n*getmem* - shows memory usage. \r\n*getmem2* - shows stack pointer information. \r\n*new* - creates a new character under your nick params: \"new charactername racename\" max character name size is 8.  \r\n*listraces* - shows available races. \r\n*who* - shows who is connected.\r\n*look* - views the room you are in.  \r\n*goto* - goes to a room parameters: goto 2";

prog_char helptext2[] PROGMEM = " Available rooms are 1-3. \r\n*north* (n) *south* (s) *east* (e) *west* (w) ";

PROGMEM const char *help_table[] =

{   

  helptext1,

  helptext2

};

prog_char room_desc_1[] PROGMEM = "You are standing outside a village where a warm wind blows past.  The dust settles about you as the road you are standing on gets stired up occasionally by the wind. \r\n\r\nThe road heads *north* into the village and *south* towards mountains.";

prog_char room_desc_2[] PROGMEM = "The village is rustic with a few buildings made of bamboo and thatch.  The thatch gently blows in the breeze as it stirs dust around you.   \r\n\r\nTo the *north* a larger building stands open.  To the *south* a dusty road leads out of the village. To the *east* a small well can be seen.";

prog_char room_desc_3[] PROGMEM = "The large bamboo structure is filled with intricate designs made in the sand of the floor.  The furniture is basic but well maintained, you see a chair, table and a small rug.  \r\n\r\nThere is an exit to the *south*.";

prog_char room_desc_4[] PROGMEM = "There is a well here.  \r\n\r\nYou can go *west*.";

prog_char room_desc_5[] PROGMEM = "The mountains sure do look nice from here; someday you hope to explore them. \r\n\r\nYou can go *north*.";

PROGMEM const char *room_desc_table[] = 	 

{   

  room_desc_1,

  room_desc_2,

  room_desc_3,

  room_desc_4,

  room_desc_5 };

int charcount = 0;

prog_char race_human[] PROGMEM = "Human";

prog_char race_elf[] PROGMEM = "Elf";

prog_char race_dwarf[] PROGMEM = "Dwarf";

PROGMEM const char *race_table[] = 	 

{   

  race_human,

  race_elf,

  race_dwarf };

char buffer[512];    

void setup(){

  pinMode(LED,OUTPUT);

  Serial.begin(115200);

}

typedef struct

  {

	char ircname;

	char charname[8];

	byte race;

        byte room;

  } character;

character characters[MAX_CHARACTERS];

char pearson(char *key, int len)

{

  char hash;

  int  i;

  char tabdata;

  for (hash=len, i=0; i<len; ++i) {

    hash = pgm_read_byte_near(*pearsondata + hash^key[i]);

  }

  return (hash);

}

void loop()                

{

  char data[150];

  int pos = 0;

  int currChar = -1;

  char charIn = 0;

    if (Serial.read() == (byte)'~') {

      while (charIn != 126) {  // wait for header byte again 

       charIn = tolower(nextByte());

       if (charIn == 126) {

         break;

       }

       data[pos] = charIn;

       pos += 1;

      }

      data[pos++] = '';

      int paramcount = 0;

      char *params[5];

      //Serial.print("Parameters: ");

      char *p = data;

      while ((params[paramcount] = strtok_r(p, ";", &p)) != NULL) { // delimiter is the semicolon

         paramcount++;

         if (paramcount > 4) {

           break;

         }

      }

      // GET NICK HASH

      char nickhash = pearson(params[0], strlen(params[0]));

      for (int i=0;i<MAX_CHARACTERS;i++) {

        if (i > charcount) {

          break;

        }

        if (nickhash == characters[i].ircname) {

          currChar = i;

          break;

        }

      }

      //Serial.println("done");

      //return;

      if (strcmp(params[1], "getmem") == 0){

        Serial.print("Free Memory: ");

        Serial.print(availableMemory());

        Serial.println("|");

      } else if (strcmp(params[1], "getmem2") == 0){

        check_mem();

        Serial.print("Heap Pointers: ");

        Serial.println((int)&heapptr);

        Serial.print("Stack Pointers: ");

        Serial.println((int)&stackptr);

        Serial.println("|");

      } else if(strcmp(params[1], "help") == 0){

        int totallen = 0;

        for (int i = 0; i < 2; i++) {

          // This is the maximum a string can be.

          int stringlen = 512;

          char tmpdata;

          for (int x=totallen;x<totallen+stringlen;x++) {

            tmpdata = (char)pgm_read_byte_near(*help_table + x);

            if ((char)tmpdata == (char)'') {

              stringlen = x-totallen;

              break;

            }

            Serial.print(tmpdata);

          }

          Serial.flush();

          totallen += stringlen + 1; // add 1 for the 

        }

        Serial.println("|");

      } else if(strcmp(params[1], "go") == 0){

        Serial.println("Starting|");

        digitalWrite(LED, HIGH); 

      } else if(strcmp(params[1], "stop") == 0){

        Serial.println("Stopping|");

        digitalWrite(LED, LOW);

      } else if(strcmp(params[1], "listraces") == 0){

        int totallen = 0;

        for (int i = 0; i < 3; i++) {

          // This is the maximum a string can be.

          int stringlen = 512;

          char tmpdata;

          for (int x=totallen;x<totallen+stringlen;x++) {

            tmpdata = (char)pgm_read_byte_near(*race_table + x);

            if ((char)tmpdata == (char)'') {

              stringlen = x-totallen;

              break;

            }

            Serial.print(tmpdata);

          }

          Serial.print(", ");

          totallen += stringlen + 1; // add 1 for the 

        }

        Serial.println("|");

      } else if((strcmp(params[1], "north") == 0) || (strcmp(params[1], "n") == 0)) {

        if (currChar == -1) {

          Serial.println("You do not have a character, please use the new command.|");

          return;

        }

        if (room_data_table[characters[currChar].room][0] == 0) {

          Serial.println("You cannot go that direction.|");

        } else {

          characters[currChar].room = room_data_table[characters[currChar].room][0] - 1;

          strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

          Serial.println(buffer);

          GetOthersInRoom(currChar);

          Serial.println(buffer);

          Serial.println("|");

        }

      } else if((strcmp(params[1], "south") == 0) || (strcmp(params[1], "s") == 0)){

        if (currChar == -1) {

          Serial.println("You do not have a character, please use the new command.|");

          return;

        }

        if (room_data_table[characters[currChar].room][1] == 0) {

          Serial.println("You cannot go that direction.|");

        } else {

          characters[currChar].room = room_data_table[characters[currChar].room][1] - 1;

          strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

          Serial.println(buffer);

          GetOthersInRoom(currChar);

          Serial.println(buffer);

          Serial.println("|");

        }

      } else if ((strcmp(params[1], "east") == 0) || (strcmp(params[1], "e") == 0)) {

        if (currChar == -1) {

          Serial.println("You do not have a character, please use the new command.|");

          return;

        }

        if (room_data_table[characters[currChar].room][2] == 0) {

          Serial.println("You cannot go that direction.|");

        } else {

          characters[currChar].room = room_data_table[characters[currChar].room][2] - 1;

          strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

          Serial.println(buffer);

          GetOthersInRoom(currChar);

          Serial.println(buffer);

          Serial.println("|");

        }

      } else if ((strcmp(params[1], "west") == 0) || (strcmp(params[1], "w") == 0)) {

        if (currChar == -1) {

          Serial.println("You do not have a character, please use the new command.|");

          return;

        }

        if (room_data_table[characters[currChar].room][3] == 0) {

          Serial.println("You cannot go that direction.|");

        } else {

          characters[currChar].room = room_data_table[characters[currChar].room][3] - 1;

          strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

          Serial.println(buffer);

          GetOthersInRoom(currChar);

          Serial.println(buffer);

          Serial.println("|");

        }

      } else if(strcmp(params[1], "look") == 0){

        if (currChar == -1) {

          Serial.println("You do not have a character, please use the new command.|");

          return;

        }

        if (strcmp(params[2], "") == 0) {

          strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

          Serial.println(buffer);

          GetOthersInRoom(currChar);

          Serial.println(buffer);

          Serial.println("|");

        } else {

          Serial.println("Item Look not yet available. (but it will be soon)|");

        }

      } else if(strcmp(params[1], "goto") == 0){

        if (currChar == -1) {

          Serial.println("You do not have a character, please use the new command.|");

          return;

        }

        int room = 0;

        if( !(room = atoi(params[2])) == NULL) {

          if (room > 4) {

            Serial.println("Invalid room.|");

            return;

          }

          characters[currChar].room = room-1;

          strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

          Serial.println(buffer);

          GetOthersInRoom(currChar);

          Serial.println(buffer);

          Serial.println("|");

        } else {

          Serial.println("Invalid room.|");

        }

      } else if(strcmp(params[1], "who") == 0){

        for (int i = 0; i < MAX_CHARACTERS; i++) {

          if (characters[i].ircname != NULL) {

            Serial.print("IRC Nick Hash: ");

            Serial.print(characters[i].ircname, HEX);

            Serial.print("Name: ");

            Serial.println(characters[i].charname);

          }

        }

        Serial.println("|");

      } else if(strcmp(params[1], "new") == 0){

        if (paramcount < 4) {

          Serial.println("You must provide a character name and race to create a character, you can list the available races by calling the listraces command.  new;charactername;racename|");

          return;

        }

        if (currChar == -1) {

          if (charcount >= MAX_CHARACTERS) {

            Serial.println("No more empty character slots.|");

            return;

          }

          currChar = charcount;

          charcount++;

        } else {

          Serial.println("You already have a character.|");

          return;

        }

        characters[currChar].ircname = nickhash;

        if (strlen(params[2]) > sizeof(characters[currChar].charname)) {

           Serial.println("Character name too long.|");

           return;

        }

        strcat(characters[currChar].charname, params[2]);

        // set race        

        for (int i = 0; i < 3; i++) {

          strcpy_P(buffer, (char*)pgm_read_word(&(race_table[i])));

          if (strcmp(params[3], strlwr(buffer)) == 0) {

            characters[currChar].race = i;

          }

        }

        Serial.print("Character Created!  Welcome ");

        Serial.println(params[2]);

        strcpy_P(buffer, (char*)pgm_read_word(&(room_desc_table[characters[currChar].room])));

        Serial.println(buffer);

        GetOthersInRoom(currChar);

        Serial.println(buffer);

        Serial.println("|");

      } else {

        Serial.println("No way!|");

      }  

      Serial.flush();      

    }

}

void GetOthersInRoom(int currChar) {

  byte multiple = 0;

  strcpy(buffer, "");

  for (int i=0;i<charcount;i++) {

    if (characters[i].ircname == characters[currChar].ircname) {

      continue;

    }

    if (characters[i].room == characters[currChar].room) {

      multiple++;

      if (multiple > 1) {

        multiple = 2;

        strcat(buffer, ", ");

      }

      strcat(buffer, characters[i].charname);

    }

  }

  if (multiple >= 2) {

    strcat(buffer, " are here.");

  } else if (multiple > 0) {

    strcat(buffer, " is here.");

  }  

}

byte nextByte() { 

    while(1) {

      if(Serial.available() > 0) {

          byte b =  Serial.read();

	  return b;

       }

    }

}

int availableMemory() {

  int size = 2048; // Use 2048 with ATmega328

  byte *buf;

  while ((buf = (byte *) malloc(--size)) == NULL)

    ;

  free(buf);

  return size;

}

void check_mem() {

  stackptr = (uint8_t *)malloc(4);          // use stackptr temporarily

  heapptr = stackptr;                     // save value of heap pointer

  free(stackptr);      // free up the memory again (sets stackptr to 0)

  stackptr =  (uint8_t *)(SP);           // save value of stack pointer

}

One Response to “MUD on an Arduino”

  1. Wire Gauge said

    ehternet cables are still the ones that i use for my home networking applications ~,~

Leave a comment