Monday 1 August 2022

Possibly the Crudest Pet Microchip Scanner in the World

This small project was simple and also difficult. The difficulties were mostly because of various things being undocumented or unclear. Now that those things have been worked out, there's not much to it.

Motivation

The goal was to be able to identify a particular cat who has arrived in the neighbourhood recently. We have an idea who its original owner might be, so a tag number ought to be enough to check that hypothesis.

Background

In the US this would not be easy to achieve because pet microchipping is optional and there are so many standards to choose from, some of which feature proprietary encryption.

Here in the UK, things are more straightforward. Microchipping of dogs is obligatory and is legally required to conform to ISO standards 11784:1996 and 11785:1996 based around 134.2kHz RFID. While microchipping of cats is only set to become mandatory some time in 2022, it is commonly done and usually to the same ISO standards.

For about £30 I could have bought a complete scanner. But why would I do that when, for only half the price, I could buy just the reader chip and a small antenna, and make life difficult for myself?

Parts

  • Arduino Uno (rev 3)
  • 134.2kHz RFID reader and antenna
  • USB A/B cable
  • 9V power supply for Arduino
  • Breadboard jumper wires
  • Solder and heat-shrink
  • Small cardboard box
  • Rubber bands

Equipment

  • Voltmeter
  • Soldering iron
  • Laptop
  • Test cat
Test cat


Assembly

Connecting the antenna is the only physically difficult part of assembling this. I soldered the ends of it to the halves of a female-female jumper wire, so that I could plug it onto the reader board easily. The antenna wire is coated in a thin, almost invisible layer of insulation so my first attempt to do this failed. It was worth testing that I could pass a current between the ends of the jumper wires, before calling this step complete. I used some heat-shrink to secure the joins and cover up my crap soldering.

The ends of the antenna can then be connected to the separate pair of male pins on the reader board. In the row of six pins on the other side of the board, only three need connecting:
  1. Ground
  2. +5V
  3. -
  4. Serial transmit
  5. -
  6. -


The Arduino provides a +5V power supply pin and accompanying ground pins. It only has one UART and that's needed for communicating with the laptop, but there's a readily available software serial port package meaning that we can use GPIO pin 2 to receive from the reader's pin 4.

The separate 9V power supply might not be strictly necessary, but without it the whole system would be dependent on power from the USB cable. It was hard enough to persuade the reader to emit any data at all and the small amount of documentation I had suggested that a stable power supply might help with that.

Programming

I tried VS Code with PlatformIO. It was great: very easy to set up and use. The Arduino code is in the appendix.

Testing

Scanning the test cat should give a sequence of 13 bytes on the serial console, starting with AA and ending with BB.

Decoding the Chip Number

The 13 bytes of output have the hexadecimal form AA 0F 08 00 cc cc nn nn nn nn nn xx BB in which:
  • cc cc is a big-endian, two-byte country code or manufacturer number;
  • nn nn nn nn nn is a big-endian, five-byte animal ID number;
  • xx is a checksum that should be the exclusive-or of the ten preceding bytes (i.e. not including the leading AA).
PETtrac, the most obvious place to look up a UK pet microchip number, expects a 15-digit number but doesn't say much more about what format that might have. The animal ID field only gives 12 decimal digits; the shortfall is accounted for by prefixing it with the three-decimal-digit country code.

Pitfalls

At first I tried to connect the antenna to the reader simply by winding the antenna leads round the pins on the reader. I hadn't worked with enameled wire before and it doesn't look much different from bare copper wire, so I didn't realise that that wouldn't actually make any connection!

The reader board claims to operate at 9600 baud. In the absence of further detail, that would probably mean no parity bits and one stop bit, but there was no way to know that except by successfully reading data from it.

Neither the product listing for the card reader nor the chip packing on the reader itself had any kind of part number or manufacturer information on it, making it very difficult to search upstream for documentation. Eventually the query ["134.2khz" em4305 uart] led me to a listing on Alibaba with slightly clearer explanation. The part there appears to be supplied by Taidacent (or Shenzhen Taida Century Technology Company, for long) but it's not clear to me whether that's a manufacturer or just another retailer and in either case it didn't yield any proper documentation.

As noted in Amazon reviews, the antenna range is pitifully short. Also I wasn't entirely sure whether the cat's chip would be in the expected place between her shoulder blades, or how much the antenna orientation might matter. Luckily the test cat enjoys being scratched quite hard, so I could rub her all over with the device and eventually I did get a reading from between her shoulders after all.

The SoftwareSerial setup allows one to specify inverted logic. For some reason, this seemed to make it easier to obtain output bytes from the reader board, but the output bytes (though largely repeatable) seemed to be junk. Having not yet successfully prompted any other output at all, I spend a while trying to decode them before deciding that was probably a waste of time.

The ISO specifications talk about a country code, so I was expecting the first two bytes of the code to give 826 for Great Britain. In the case of our test cat, the value was 977 which isn't any country code at all! More digging revealed that at some point the scope of the field seems to have been widened to include manufacturer codes assigned by the International Committee for Animal Recording (ICAR). 977 would correspond to Avid.

The test cat likes to rub her face on the corners of cardboard boxes and to chew things like wires, which can make getting a reading more difficult.

Discussion

The reader chip has its own UART and talks a very common serial protocol at 9600 baud. All the Arduino is really doing is providing a 5V power supply and echoing whatever it receives on its USB port. So it ought to be possible to drop the Arduino and replace it with a USB-to-UART adapter such as this one, moving the decoding logic to a script running on the host laptop.

Appendix A: Code

#include <Arduino.h>
#include "SoftwareSerial.h"

auto rfid_serial = SoftwareSerial(
/*receivePin=*/2,
/*transmitPin=*/3,
/*inverseLogic=*/false
);

auto& usb_serial = Serial;

static union {
struct {
uint8_t start_marker;
uint8_t fixed_code[3];
uint8_t country_code[2];
uint8_t animal_id[5];
uint8_t checksum;
uint8_t end_marker;
} fields;
char buffer[sizeof(fields)];
};

static_assert(sizeof(fields) == 13);

int bytes_read; // Index of next free byte in buffer.
int total_bytes_read = 0;

void decode_and_print_buffer() {
uint16_t country_code = 0;
for (int i = 0; i < 2; ++i) {
country_code = (country_code << 8) + fields.country_code[i];
}
uint64_t animal_id = 0;
for (int i = 0; i < 5; ++i) {
animal_id = (animal_id << 8) + fields.animal_id[i];
}
char decimal_buffer[16];
snprintf(
decimal_buffer, 16, "%03u%06lu%06lu",
country_code,
// Printing 64-bit integers seems not to work at all, so instead
// we print the two halves of the animal ID separately.
static_cast<uint32_t>(animal_id / 1000000),
static_cast<uint32_t>(animal_id % 1000000)
);
usb_serial.print("15-digit identifier: ");
usb_serial.println(decimal_buffer);
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
usb_serial.begin(9600);
usb_serial.print("Initialising ... ");

rfid_serial.begin(9600);
if (!rfid_serial.isListening()) {
usb_serial.println("Failed to initialise software serial port.");
return;
}

digitalWrite(LED_BUILTIN, LOW);
usb_serial.println("Ready.");
}

void loop() {
if (rfid_serial.overflow()) {
usb_serial.println("Overflow!");
}
while (rfid_serial.available()) {
int available_byte;
if ((available_byte = rfid_serial.read()) != -1) {
usb_serial.print(available_byte, HEX);
usb_serial.print(' ');
++total_bytes_read;

// Don't start filling the read buffer unless this byte is a start
// marker. That way, if we ever get into some sort of unaligned state
// then it will be possible to recover eventually.
if (bytes_read == 0 && available_byte != 0xAA) {
usb_serial.println("Ignoring non-start byte.");
continue;
}

buffer[bytes_read++] = static_cast<char>(available_byte);
if (bytes_read == sizeof(fields)) {
usb_serial.println();
decode_and_print_buffer();
memset(buffer, 0, sizeof(fields));
bytes_read = 0;
}
} else {
usb_serial.println("That's odd. An available byte could not be read.");
}
}

// Flash the LED till we've read something, to show that the loop is
// still running.
digitalWrite(LED_BUILTIN,
total_bytes_read > 0 || ((millis() / 500) % 2 == 0));
}