Sunday, November 1

Halloween Automated Scarer 2015

A pinch of pygame, a hint of scapy and the Halloween concoction is ready!

This year's automated scarer was a bit more challenging than last. I stitched this together in a couple of days and ... it worked mostly as I wanted it to be. The most important thing is that children and parents enjoyed being scared by a few monstrous pumpkins and a bunch of my son's very horrific (but beautifully made) drawings.
Raspberry thirsty Vampire
Scary Witch of the South East

I wanted the animation to be easy to be activated and yes, I could have used other methods to detect people to be scared (which from now on I will refer to as scarables: a person suitable for the scarer) but I was curious about scapy and the possibility of detecting mobiles passing by.

I also found quite interesting digging through the enormous amount of information available on pygame which served perfectly the purpose of displaying the pictures and playing the sounds all of this without needing to drop into X leaving ample resources available on my RPi B

It is also entertaining to lookup the MAC addresses I collected to establish if we live in a posh area or not. The amount of Apple devices wasn't so many in the end, I will consider selling and buy elsewhere.
Ah and don't worry, the MACs I collected will be handed over to TalkTalk for safe keeping.

The Python script revolves mainly around the scapy command sniff which I used to search any broadcasting WiFi device within reach of my dongle.
The first thing to do is to setup the WiFi card into monitor mode which is done outside of python on the shell:

#/usr/bin sh
sudo service ifplugd stop
sudo ifconfig wlan0 down
sudo iwconfig wlan0 mode monitor
sudo ifconfig wlan0 up
sudo service ifplugd start

You can verify things are set properly by issuing iwconfig, this is my output:

wlan0     IEEE 802.11bgn  Mode:Monitor  Frequency:2.412 GHz  Tx-Power=20 dBm
          Retry short limit:7   RTS thr:off   Fragment thr:off
          Power Management:off

Within scapy's lamda function we need to create a list to hold the MAC addresses that we'll acquire. Around the area where I live there are normally quite a few Access Points and other WiFi devices. The first thing to do then is to store all these MACs on a file so that they will not trigger any false "scares" each time that the Pi starts. I did this by running the script in "silent mode" for a couple of hours. I had created in this way a WiFi fingerprint of the area which I stored on a file that gets read at the start of the script and pre-loads all the MACs in the neighbourhood.

if path.isfile(fileName):
    with open(fileName, 'rb') as MAC_file:
        MAC_list = pickle.load(MAC_file)

for the serialisation I am using pickle which for the purpose works just fine

From here on any new MAC detected is a potential scarable,

Scapy offers a sniff command that can take as arguments an interface and a function amongst other options.

sniff(iface = "wlan0", prn = PacketHandler, store=0)

The store=0 turned out to be vitally important as it tells the sniff command to just monitor the packets and not store them in memory. I learned this the hard way as I the Pi stopped responding all together as its memory got full.
I have obviously specified to monitor packets on wlan0 and given a PacketHandler function which I use to specify what I am interested in about the packets seen by wlan0

def PacketHandler(pkt):
        if pkt.haslayer(Dot11):
                if pkt.type == 0:
                        if pkt.addr2 not in MAC_list:
                                with open(fileName, 'wb') as MAC_file:
                                        pickle.dump(MAC_list, MAC_file)
                                        Call the Scare function

the layer I am interested in is Dot11 as in 802.11x and the packet type is 0 as in management. If the MAC is not in my list I will add it and write the list on a file.
The other very important thing that I do in the last if block is that I will start the animation for each new MAC added to the list.
Now, this could have done it in a number of ways, I wanted to explore the pygame events, I wanted to use two threads on for WiFi scanning the other for the animation, in the end at 16:30 of the 31st of November I decided to call a function directly after writing the list to a file.

image = images[random.randint(0, len(images)) - 1]
sound = sounds[random.randint(0, len(sounds)) - 1]
Scare(image, sound, screenSize)

The Scare function receives a random image and a random sound from two preloaded lists. I paste the PrepareImages only as the PrepareSounds is essentially the same

def PrepareImages():
        imagesNames = glob.glob('*.png')
        imageCache = []

        for imageName in imagesNames:
                        raise UserWarning, "Could not load images"

        return imageCache

All .wav and .png files where in the same path as the script.
Everywhere I looked for examples of pygame.image.load I found that it is highly recommended to convert the images to improve performance. I certainly noted an improvement

The Scare function looks like this

def Scare(image, sound, screenSize):
        image = pygame.transform.scale(image, screenSize)
        frame = image.get_rect()
        white = (255, 255, 255)
        black = (0, 0, 0)
        screen = pygame.display.set_mode(screenSize)

        playing =
        while playing.get_busy():
                screen.blit(image, frame)

it will resize the picture (sometimes deforming it as the source did not have the same proportions of the screenSize I selected), set the screen, play the sound.
Whilst playing the sound the picture would be displayed (blit) and a 1 second delay also added to get the shivers deep in the scarable's spine.
After the sound is played the screen turns black again.

That's it really, ciao!

Post a Comment