Initial testing for FDX-B agri eartag readers

Introduction

For our farm we have the sheep tagged with electronic eartags. These conform to ISO 11784 and ISO 11785, with Full Duplex (FDX/FDX-B) communication. The Wikipedia page on the matter is very good, as is the priority1design on the protocol.

As I wanted to mess around with some code around these earmarks, I needed hardware to actually read these tags. The important bit here is that these tags are 134.2Khz based and not the more common 124KHz RFID tags, and thus those readers cannot be used. The one advantage is that the only remaining found boards usually seem to be fairly high quality. I picked a Jedrek board as I could find at least some reasonable documentation on them. The board I picked pushes the data out via serial and thus I also picked up a cheapo UART Serial to USB converter.

The hardware

  1. A Jedrek based board (not listed on their website) "134.2KHz HDX FDX-B ISO/IEC 11784/5 Animal Tag reader Module RS232 TTL Interface"
  2. UART to USB converter for serial

The setup

I've 3D printed a little antenna holder, such that there is no stress on the small wires to the antenna PCB. I've got three wires from the UART board to VCC, GND and TXD (of course, RXD on the UART board). Some attention when inserting the black PCB onto the antenna board is required, as there is no good indication of the pins on the board. However, looking at the bottom, seeing what pin is GND and which one is VCC is a good way to make sure you're powering the correct pins.

alt text

alt text

Decyphering the protocol

If you've read the Wikipedia page and the priority1design page on the protocol, you might reasonably expect the board to output some nice bitstrings that conform to the protocol. However, for some reason the board embeds the data in some sort of odd ASCII padded data. This might have something to do with the BDP transmission system, but I do not know enough about electronics to figure it out.

unfortunately, decyphering how they get from the given ASCII to the ISO contents isn't actually that clear in the Jedrek documentation. Thus, I found this blog post to be warranted.

Step 1: Getting the data

The first step is reading some data from the board. As I didn't know how exactly the data would be tagged coming from the board I did some repeat reads until I got a repeating pattern which I could figure out. All the code here is Python 3. For the serial connection I used the pyserial module

import serial
ser = serial.Serial('COM3', stopbits=2)

for _ in range(4):
    ser.read(32)

Step 2: ASCII shenanigns

The final data, adjusted for the 02 start and 03 end that the documentation calls for, resulted in b'\x020B07AA3571010120010000000000w\x88\x03' We take that as the base for the rest of the operation. However, to do actual meaning full operations on the ASCII data, this first has to be converted to an easier to work with binary format. To that end I used the bitstring module.

tag = BitArray(bytes=ser.read(32))
base = tag.hex
'023131383039433943313730303745333030303030303030303030303005fa03'

Stripping the 02 start padding and the last 6 characters which contain the checksum etc, we get the payload:

core = base[2:-6]

The core: '31313830394339433137303037453330303030303030303030303030'

Now this is where the ascii embedding really becomes visible, the 03's infront of nearly every byte. To convert this to a meaning full number we have to take each of the ascii chars and convert them to decimal, via a binary step. What I mean by this is, we firstly take that long hex string and split it up into ascii chars again:

core = [str(chr(int(x, 16))) for x in core]

Now, for just the country tag, we take these bytes:

countrystr = core[10:10+3]

This is then a set of three ASCII chars, which we take the binary data from to get the actual number. One important bit here is that we only care about the lower 4 bits for every ascii char. Additionality, the order is reversed. So, to get the actual number we finally do:

binarray = [BitArray(uint=int(x, 16), length=4).bin for x in countrystr]
binary = "".join(reversed(binarray))
num = BitArray(bin=binary).int

The final result

Taking all three of these steps together I've built a small Python script which very simply reads from a serial port and prints the country number and tag number. Two small additions are that I read in a continious non blocking fashion for better responsiveness and that I find the correct bitstring in what is read by using a very basic regex pattern

import serial
import argparse
from time import sleep
from bitstring import BitArray
import re

pattern = r'.*(02.{56}03).*'
pattern = re.compile(pattern)

def extract_num(readarray):
    """
    Find occurances of numbers, remove them from the bit array

    Return the new bitarray, foundtag or none
    """

    readstring = readarray.hex
    result = re.match(pattern, readstring)

    if result != None:
        hextag = '0x' + result.group(1)

        # Remove any pre bullshit data
        res = list(readarray.split(hextag, count=2))[1]

        # And remove the found tag
        res.replace(hextag, '')
        return res, BitArray(hex=hextag)
    else:
        return readarray, None

def parse_tag(tag):
    base = tag.hex
    # Split it in ascii chars
    core = [(base[i:i+2]) for i in range(0, len(base), 2)]
    core = core[1:-3]

    # Now convert to ascii
    core = [str(chr(int(x, 16))) for x in core]

    animalstr = core[:10]
    countrystr = core[10:10+3]

    countrynum = parse_asciitag(countrystr)
    animalnum = parse_asciitag(animalstr)
    print("Country {}, Tag: {}".format(countrynum, animalnum))

def parse_asciitag(asciistr):
    binarray = [BitArray(uint=int(x, 16), length=4).bin for x in asciistr]
    binary = "".join(reversed(binarray))
    num = BitArray(bin=binary).int

    return num

if __name__ == "__main__":
    serdev = 'COM3'
    readarray = BitArray()

    # zero time out, non blocking
    with serial.Serial(serdev, stopbits=2, timeout=0) as ser:
        while True:
            readres = ser.read(32)

            readarray.append(BitArray(bytes=readres))

            readarray, tag = extract_num(readarray)

            if tag is not None:
                print("Found tag {}".format(tag))
                parse_tag(tag)

            sleep(1)

References

http://www.jedreksys.com/download/134-K-Animal-Tag-FDX-B-ISO11784-Reader-Module.html https://en.wikipedia.org/wiki/ISO_11784_and_ISO_11785 http://www.maxmicrochip.com/ISO_types.htm http://www.priority1design.com.au/fdx-b_animal_identification_protocol.html https://en.wikipedia.org/wiki/ISO_3166-1