See the first post: project introduction and the second: using the NXP NFC explorer board.
Previously on this project I explained the topic, and how to get going quickly with NXP.
In this post I’m going to cover how to build a class around the NFC idea and code, which will allow us to group together the bits we need – that is, data input and output, reading NFC cards and anything else we need along the way.
We’ll start by making a really basic stub - enter in a pi terminal:
sudo nano nfc.py
and put in the following code - I’ll explain how to fill in along the way:
import nxppy import os,json,requests from xml.dom import minidom class NFC(object): def __init__(self,pifile,peoplefile): self.pi=pifile self.peoplef=peoplefile self.achievements=self.LoadPi() self.people=self.LoadPeople() def ReadCard(self): return None def Load(self,file,type): if os.path.exists(file): source=open(file) try: dom1=minidom.parse(source) except: impl=minidom.getDOMImplementation() dom1=impl.createDocument(None,type,None) return dom1 else: impl=minidom.getDOMImplementation() doc=impl.createDocument(None, type,None) return doc def LoadPi(self): return None def SavePi(self): return None def LoadPeople(self): return None def SavePeople(self): return None def AddAchievement(self,desc,question,answers): return None def UpdateAchievement(self,updated_entry): return None def DeleteAchievement(self,id): return None def GetAchievement(self, id): return None
So what do we have? Well, __init__ is our initialiser: this is what gets called when a copy of this class is instantiated later. In here you should put any variables that will be part of the class, and do any method calls which are necessary straight away.
The only other stub we have filled in is Load, which will be a generic method to open up any xml file we need - we'll be using xml to save all of the data for our system.
The other methods allow us to make a data class which covers everything - so we have load and saving to xml output (Load/SavePi and Load/SavePeople) and CRUD (create, review, update, delete) methods for the variables inside the class for usage later.
Gradually let's fill the stub in, starting with Load/SavePi - the pi file will be where we store achievements and details about the station we're currently making:
def LoadPi(self): dom=self.Load(self.pi,"piSyst") try: pi_tag=dom.getElementsByTagName("pi")[0] except: self.SavePi() dom=self.Load(self.pi,"piSyst") pi_tag=dom.getElementsByTagName("pi")[0] a_tags=pi_tag.getElementsByTagName("achievement") achievements={} achievements[pi_tag.getAttribute("ID")]={"question":None,"answers":None} for a in a_tags: id=a.getAttribute("ID") qtag=a.getElementsByTagName("question")[0] question=qtag.childNodes[0].data atag=a.getElementsByTagName("answer") answers=[] for an in atag: answers.append(an.childNodes[0].data) achievements[id]={"question":question,"answers":answers} return achievements def SavePi(self): dom=self.Load(self.pi,"piSyst") top=dom.documentElement old_achievements=self.LoadPi() if(len(dom.getElementsByTagName("pi"))==0): pi=dom.createElement("pi") id=0 pi.setAttribute("ID",str(id)) top.appendChild(pi) else: pitag=dom.getElementsByTagName("pi")[0] if old_achievements != self.achievements: try: os.remove(self.pifile) except: pass dom=self.Load(self.pi,"piSyst") pitag=dom.createElement("pi") id=0 pitag.setAttribute("ID",str(id)) top.appendChild(pitag) for id,a in self.achievements.iteritems(): if a["question"]!=None: ac=dom.createElement("achievement") ac.setAttribute("ID",id) q=dom.createElement("question") text=dom.createTextNode(str(a["question"])) q.appendChild(text) ac.appendChild(q) for answer in a["answers"]: ans=dom.createElement("answer") txt=dom.createTextNode(str(answer)) ans.appendChild(txt) ac.appendChild(ans) pitag.appendChild(ac) file=open(self.pi,'w') dom.writexml(file)
Here we're using a library called minidom - DOM stands for Document Object Model, and minidom is a lightweight library for handling xml files and turning them into document object models we can peruse and pull and save data to.
Starting with LoadPi, the algorithm loads up the dom from the load method and tries to pull out a pi tag - this will be the tag present in every document and there should only ever be one.
If it's not found it means we've not actually used this station before, so the system will save the Pi for us and create the pi tag whilst it's doing so.
Following this little problem, we go on to hunt for achievements - as a brief explanation:
- Every pi with a unique ID will be classed as an achievement in it's own right: these aren't listed as achievements as they won't require questions and answers in the actual file, but the system automatically adds them to the achievement dictionary.
- Each pi can also have achievements created by the admin, which at present can just be question and answer style but can have multiple answers.
The system pulls out each one, the question and the answers and creates an entry in the achievement list indexed by it's unique ID.
For simplicity, in saving I've chosen to delete the entire file and recreate it: I figure that this is nicer syntax and in order to update everything we'd have to parse the entire tree anyway.
Following on from this let's take loading and saving people who've scanned their NFC cards:
def LoadPeople(self): dom=self.Load(self.peoplef,"People") p_tags=dom.getElementsByTagName("person") people={} for p in p_tags: id=p.getAttribute("ID") ntag=p.getElementsByTagName("name")[0] name=ntag.childNodes[0].data atag=p.getElementsByTagName("achievement") achievements=[] for a in atag: if a.childNodes[0].data not in achievements: achievements.append(a.childNodes[0].data) people[id]={"name":name,"achievements":achievements} return people def SavePeople(self): dom=self.Load(self.peoplef,"People") top=dom.documentElement old_p=self.LoadPeople() people_tags=top.getElementsByTagName("person") for id,p in self.people.iteritems(): if id in old_p.keys(): for pe in people_tags: if pe.getAttribute("ID")==id: for achievement in p["achievements"]: if achievement not in old_p[id]["achievements"]: ac_tag=dom.createElement("achievement") ac_text=dom.createTextNode(achievement) ac_tag.appendChild(ac_text) pe.appendChild(ac_tag) else: peep=dom.createElement("person") peep.setAttribute("ID",id) n=dom.createElement("name") text=dom.createTextNode(str(p["name"])) n.appendChild(text) peep.appendChild(n) for achievement in p["achievements"]: ac=dom.createElement("achievement") txt=dom.createTextNode(str(achievement)) ac.appendChild(txt) peep.appendChild(ac) top.appendChild(peep) file=open(self.peoplef,'w') dom.writexml(file)
The structure for people will be a bit simpler: each person gets added as <Person ID="NFC_ID"></Person>, and within here has <achievement>0</achievement> to indicate what achievement IDs have been collected.
With the save method here, as there's no way to remove or delete a person, all we have to check is whether the person already had a particular ID in their achievement list: this doesn't actually matter as the load code will check for duplicates anyway, but just to be sure.
Now that we have all the "infrastructure" for saving and loading the data we pick up from the scanner, let's make the card reading method:
def ReadCard(self): id=nxppy.rad_mifare() if id == None: return None else: if id not in self.people.keys(): print "new ID :", id name=raw_input('Please enter your name:') self.people[id]={"name":name,"achievements":[]} if id in self.people.keys(): for aid in self.achievements.keys(): if aid not in self.people[id]['achievements']: if self.achievements[aid]['question']==None: self.people[id]['achievements'].append(aid) print "Achievement unlocked!" else: print self.achievements[aid]['question'] ans=[] for a in range(len(self.achievements[aid]['answers'])): answer=raw_input('Enter answer:') ans.append(answer) correct=0 for an in range(len(ans)): found=False for answ in self.achievements[aid]['answers']: if answ==ans[an]: found=True break if not found: print "answer" + str(ans) + " incorrect!" else: correct+=1 if correct == len(self.achievements[aid]['answers']): print "achievement unlocked!" self.people[id]['achievements'].append(aid) self.SavePeople() return self.people[id]
Here we have something a bit more complex than the previous tutorial because we've now got data to play with.
From the top, we check whether there's been a card to scan in range of the scanner. If not, nothing will happen.
If there is, and that person is'nt in the list of people who've been to this station before, then we ask them to add themselves to our database.
If we've had them scanned before, we'll proceed to look at each and every achievement and check whether they've collected that already. If not, and it's just a pi
achievement, add it straight away and inform the user. If there's a question, we'll make them answer the question and check all their answers are correct.
Finally, we save the file and return the user's entry in the dictionary.
So now we can actually use this class - we'll leave the CRUD methods (Add/View/Update/Delete) when we cover making an admin portal, except for view which would be nice to have for the NFC reader file:
def GetAchievement(self, id): if id in self.achievements.keys(): return self.achievements[id] else: return None
Now we can close this file - enter CTRL+X, then Y, then press enter.
Now enter in the terminal:
sudo nano read.py
and put in the following:
from nfc import NFC self=NFC('pi.xml','people.xml') person=self.ReadCard() if person != None: print "Hello " + person["name"] print "you have collected ",len(person["achievements"])," achievements" for a in person["achievements"]: print "ID: ", a achievement=self.GetAchievement(a) if "Description" in achievement.keys(): print "Description: ", achievement['Description']
Again this has got a bit more complex than before: we've now imported the class we just made, instantiated it (self=NFC('pi.xml','people.xml')), and read the person.
Then we check there's an entry, and inform the user of what they've got - note that as there's several print statements in self.ReadCard, those will come before the prints here and will inform the user if they've won things from this particular scan.
We've put in a little contingiency so that we don't have to update this file later:
if "Description" in achievement.keys(): print "Description: ", achievement['Description']
this is because when we come to step 5 and 6 we'll be saving a description, but this will be pulled out from the web API rather than the xml file.
Now you can close this file (CTRL+X,y,and enter) and enter:
sudo python read.py
whilst placing your NFC card over the reader.
Now we should get:
aand when you enter your name, 2 files should exist - people.xml:
and pi.xml:
In my next update, we'll create an admin section so that we can edit the achievements available.