IRC Bot on the ATmega328 (Arduino NANO v3.0)

Programming AVR is really fun, after experimenting with the Sainsmart 1.8" TFT screen and creating a Flappy Bird Clone I decided to start a project for my ethernet module using an ENC28J60 chip. Being able to communicate with the ATmega328 from another computer through the network was something I wanted to try and this cheap module has a couple libraries available to do that easily.

This time I wanted to try a Nano v3.0 I had purchased recently, the cost of both these components together is around $6 which makes this whole experiment even more interesting. I was curious to see what could be possible to achieve with such a cheap combination of components and after looking online for documentation I ended up on the EtherCard Github repository.

IRC Bot on the ATmega328 (Arduino NANO v3.0) - 01

EtherCard is a really nice library, one of the first thing I tried was running a web server and I have to admit I was surprised with how easy it was to use it. I was thinking about making a mini-website trying to squeeze data as much as possible while serving HTML5 with a couple animations and some Javascript on top of that but the 'challenge', if I can even call it a challenge, wasn't really interesting. There are already a couple examples coming with the library so I decided to go with something else, an IRC bot.

I had absolutely no idea if the ENC28J60 module could handle long persistent sessions, wasn't sure if the code could fit on the ATmega328 and if the 2KB of memory would be enough to parse the server replies and possible commands a user would send.

Fortunately the library documentation mentions that it is possible to deal with persistent connection by setting a flag using the persist_tcp_connection flag to true through the persistTcpSession(bool) function. I have to admit I thought it would be really easy from there but I had a couple issues during the process.

The IRC protocol:

Dealing with the IRC protocol is really easy, it's all about parsing plain text and sending a proper request when the server replies. Reading the RFC2812 is all you need to understand how it works. Important sections are:

And that's all you need to do in order to keep a bot online on IRC. I think that making an IRC bot is always a nice project for people willing to practice or learn another language since you learn to parse data properly and reply accordingly.

The way I proceed to parse the server replies is the following:

  • split reply into tokens using newline as a separator ('\n' or 0xA)
    • check if token has a carriage return at the end and remove it ('\r' or 0xD)
    • split token into reply tokens (again) by using space as separator (' ' or 0x20)
      • reply_token[0] could be a PING - if it's the case reply with a PONG with the data in reply_token[1]
      • if reply_token[0] starts with a semi colon (':' or 0x3A) then:
      • remove semi colon and parse the user whois information in the form of [user]![realname]@[ip]
      • reply_token[1] is the command or a code (see RFC2812 command response)
      • reply_token[2] is the user or channel where the message was coming from
      • reply_token[3] is the message content
  • loop until reply has been fully parsed

A reply would be similar to this:

:[user]![realname]@[ip] PRIVMSG [#channel] :[message]
^---reply_token[0]----^ ^-[1]-^ ^--[2]---^ ^--[3]--^

A PING request would be like this:

PING :67476C4D

All you have to do is reply with:

PONG :67476C4D

And your bot will stay online, if you fail to reply properly your bot will be disconnected from the server.

A tiny problem:

This is the first step to make a bot and I already started having issues without being able to understand the reason. I could connect to the server properly, initiate the client with a TCP request specifying the username, realname and hostname along with the nick. I could receive all replies from the server and parse them properly until the moment came when it send me a PING request and after that, nothing. I couldn't send anything back to the server beside the first tcp request.

There were a couple possible reasons:

  • maybe the module cannot deal with the amount of replies an IRC server sends when connecting to it
  • maybe I was not using the library properly
  • maybe the library has a problem with persistent sessions

Easiest thing to do was checking another library, if it can deal with persistent session and let me send multiple requests then I would be sure the module was working and the issue would be with EtherCard. If your code is layed out properly it shouldn't be a big issue to switch library, so I downloaded another one for this module called UIPEthernet. It looked really nice as well and seemed to be able to do a lot of things.

After including the library and changing the way my code was dealing with the server, I was relieved to see my module was not actually having a problem but it seemed to be either my lack of knowledge about using EtherCard or a possible problem with the library itself. I still wanted to use EtherCard for the reason that it was really lightweight compared to UIPEthernet. If I remember correctly compiling just the bot connecting to the server and replying to PING properly was taking up to 28KB with UIPEthernet, which would not leave much space to code a proper bot or even add more code for other components in order to remotely control them. I had to find a way to make multiple TCP requests while keeping the session persistent.

EtherCard is documented, if you have Doxygen you can generate the documentation yourself and read it offline. However it's not a full manual about how to use the library but it's still enough to have an understanding of which functions to look for. I still couldn't find a function to allow me to send a tcp request beside using tcpSend() or clientTcpReq(). Both these functions would not let me send a second request and I couldn't figure why.

Sniffing the network:

When you have to debug applications dealing with a network, you have to find a way to see what is going in and coming out of the server and client. Since I had set an IRC server locally in a virtual machine to test my bot (it's much faster than trying to connect to a remote server each time) I decided to capture the incoming and outgoing packets from it. You can use tcpdump or tshark for that, I used the later and captured what was going on when the bot was failing to send another reply by analyzing it in Wireshark.

IRC Bot on the ATmega328 (Arduino NANO v3.0) - 02

Wireshark was showing that after a PING request from the server I was replying with a PONG but before that I would send a request with SYN ACK flags, which means I was trying to initiate a new connection each time I was trying to send something back to the server. It made sense since the connection would often hang and not do anything until the server would think it was a timeout and close the session. I started to worry since my knowledge about networking and the TCP protocol wasn't good enough to let me build a complete packet from scratch with a proper header and send my request. I had to find what was going on with the library and noticed one undocumented file in it called tcpip.cpp.

When you use tcpSend() with EtherCard it does the following:

uint8_t EtherCard::tcpSend () {
www_fd = clientTcpReq(&tcp_result_cb, &tcp_datafill_cb, hisport);
return www_fd;
}

tcpSend() calls clientTcpReq() and this function does the following:

uint8_t EtherCard::clientTcpReq (uint8_t (*result_cb)(uint8_t,uint8_t,uint16_t,uint16_t),
uint16_t (*datafill_cb)(uint8_t),uint16_t port) {
client_tcp_result_cb = result_cb;
client_tcp_datafill_cb = datafill_cb;
tcp_client_port_h = port>>8;
tcp_client_port_l = port;
tcp_client_state = 1; // Flag to packetloop to initiate a TCP/IP session by send a syn
tcp_fd = (tcp_fd + 1) & 7;
return tcp_fd;
}

Notice the tcp_client_state flag, it is set to 1 to initiate a new session. packetLoop() will pick the flag and send the SYN ACK. It started to make sense, I couldn't use either tcpSend() or clientTcpReq() since they would both end up doing the same thing, I had to find another way to send data to the server without losing the session. I kept digging in the source until I found this function called httpServerReply():

void EtherCard::httpServerReply (uint16_t dlen) {
make_tcp_ack_from_any(info_data_len,0); // send ack for http get
gPB[TCP_FLAGS_P] = TCP_FLAGS_ACK_V|TCP_FLAGS_PUSH_V|TCP_FLAGS_FIN_V;
make_tcp_ack_with_data_noflags(dlen); // send data
}

Now even with my little understanding of the TCP protocol it was clear that in order to be able to send another request I should do something similar. Send an ACK to acknowledge that I received the last packets, set the flags without the TCP_FLAGS_FIN_V which tells the server to close the session once it receives the request and finally send the data. While it says no flags, it means no flags specified in the function and since we specify the flags ourselves (PSH|ACK) it should work properly.

For some reason it wasn't working if I used info_data_len, this variable is set inside the accept() function but if instead I used dlen it was working properly when replying to PING requests. I was finally able to send a second TCP request without losing the session and I thought I was almost done with the bot since I just had to start emplementing commands to the bot, or so I thought.

Sorry but it's still not working:

I started implementing the auto-join channel function, it's really easy you just check if the server send a command with the code 376 which is the end of the message of the day (RPL_ENDOFMOTD) or 422 which is an error saying that the message of the day file is missing (ERR_NOMOTD). This works in most cases, maybe some IRCD are not sending these messages if that's the case you just have to find another common code to know when to send the JOIN request.

My problem was that while the PONG request was working great, I couldn't apparently parse server replies other than this one and send another request automatically. The bot would attempt to join a channel after the MOTD message and the server would get into a retransmission loop sending the same data all over again.

IRC Bot on the ATmega328 (Arduino NANO v3.0) - 03

Watching the content of the data being sent all over again actually gave a hint. It was the last packet minus the size of the packet of my PONG request. My problem was with the ACK I was sending before the data, I had the wrong length and I just needed to send back an ACK with the full length of the last packet.

Going back to the source of tcpip.cpp and looking at the info_data_len variable I couldn't use, I decided to take a look at the accept() function where this variable was set:

uint16_t EtherCard::accept(const uint16_t port, uint16_t plen) {
uint16_t pos;

if (gPB[TCP_DST_PORT_H_P] == (port >> 8) &&
gPB[TCP_DST_PORT_L_P] == ((uint8_t) port))
{ //Packet targetted at specified port
if (gPB[TCP_FLAGS_P] & TCP_FLAGS_SYN_V)
make_tcp_synack_from_syn(); //send SYN+ACK
else if (gPB[TCP_FLAGS_P] & TCP_FLAGS_ACK_V)
{ //This is an acknowledgement to our SYN+ACK so let's start processing that payload
info_data_len = get_tcp_data_len();
if (info_data_len > 0)
{ //Got some data
pos = TCP_DATA_START; // TCP_DATA_START is a formula
if (pos <= plen - 8)
return pos;
}
else if (gPB[TCP_FLAGS_P] & TCP_FLAGS_FIN_V)
make_tcp_ack_from_any(0,0); //No data so close connection
}
}
return 0;
}

info_data_len is set using the function get_tcp_data_len() which returns the length of the TCP/IP payload, instead of sending an ACK with dlen I just had to use this function.

I ended up making the following function in tcpip.cpp:

void EtherCard::tcpSendPersist (uint16_t dlen) {
make_tcp_ack_from_any(get_tcp_data_len(),0); // send ack for tcp request
gPB[TCP_FLAGS_P] = TCP_FLAGS_ACK_V|TCP_FLAGS_PUSH_V;
make_tcp_ack_with_data_noflags(dlen);
}

Which I declared in the EtherCard class in EtherCard.h:

    /**   @brief  Send a TCP request with persistentTcpConnection enabled
* @param dlen Size of the TCP payload
*/

static void tcpSendPersist (uint16_t dlen);

It's alive! Alive!

The bot was working properly now, it would auto-join channels and would auto-rename if the nick was already used. I implemented a couple commands to show how to implement them to whoever would like to connect his ATmega328 on IRC and use the source. I really like EtherCard for the reason that it leaves me enough room to put more components and code to control them. It's a really nice library.

Since the resources are still limited, I implemented a small and rather simple authorization system to avoid letting people flooding the ATmega with queries. It's mostly for a single user since it saves only whoever used the auth last by storing his nick!realname@ip in the auth_user global variable. To save memory, generating a hash from this data would be a much better solution and would allow us to store more authorized users. I might eventually implement something like this later.

IRC Bot on the ATmega328 (Arduino NANO v3.0) - 04

Closing words and source code:

Coding for AVR is a lot of fun, it's always amazing to see such a small device being able to do so much and I'm really enjoying this. Another great thing is the cheap cost of components and I admit I purchased a lot of them just because they were around a dollar or two and will find a project to realize for these later.

You can find the source of the IRC Bot on github: github.com/mrt-prodz/ATmega328-IRC-Bot