Netduino home hardware projects downloads community

Jump to content


The Netduino forums have been replaced by new forums at community.wildernesslabs.co. This site has been preserved for archival purposes only and the ability to make new accounts or posts has been turned off.
Photo

Parallax RFID Reader


  • Please log in to reply
28 replies to this topic

#1 ATXcoder

ATXcoder

    New Member

  • Members
  • Pip
  • 8 posts
  • LocationAustin, Tx

Posted 24 August 2010 - 05:24 AM

BIG THANKS and credit go to JohnH whose code (http://forums.netdui...68-rfid-reader/) I used (it was extremely helpful!). I added a few lines of my own code and commented on a lot of the code for others to follow. I got the RFID reader I got from my local RadioShack for $8.49 (the same Parallax one they sell on-line for ~$40).

Netduino with Parallax RFID Reader

The Code
using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using System.IO.Ports;

namespace RFID_Demo
{
    public class Program
    {
        //This will allow us to turn the reader on and off
        static PWM rfidON = new PWM(Pins.GPIO_PIN_D9);

        public static void Main()
        {
            //On Netduino pin 0 (RX Pin)
            SerialPort SPort = new SerialPort("COM1", 2400, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One);
            
            //We don't want to read forever
            SPort.ReadTimeout = 1000;
            SPort.WriteTimeout = 1000;
            
            //This aray will hold our Cards RFID tag (all 12 bytes)
            byte[] buf = new byte[12];
            
            //This will hold the cards ID Tag (10 bytes)
            string CardId = "";

            //Turn on the reader
            rfidON.SetDutyCycle(0);

            //Open the Serial Port for communication (.NetMF 4.1 requirement)
            SPort.Open();

            //Loop it forever!
            while (true)
            {
                //Read the card, store in the array "buf" with no offset and a max read of 12 bytes)
                int readcnt = SPort.Read(buf, 0, 12);

                /*If we got 12 bytes with byte[0] a new line character (start of RFID tags)
                 * and byte[11] being a "\r" then proceed.*/
                if (readcnt == 12 && buf[0] == '\n' && buf[11] == '\r')
                {
                    for (int i = 0; i < readcnt; i++)
                    {
                        /*As long as the byte we are 
                         * looking at isn't a '\n' or a
                         * 'r', convert it to a character
                         * and append it to the string CardId*/
                        if (buf[i] != '\n' && buf[i] != '\r')
                            CardId += (char)buf[i];
                    }
                    /*Check if the CardID length is
                     * equal to 10. Remember, RFID's 
                     * are 12 characters. One "start" character,
                     * one end character, and 10 characters (ID)*/
                    if (CardId.Length == 10)
                    {
                        //Turn off the reader
                        rfidON.SetDutyCycle(100);
                        
                        //Show the ID
                        Debug.Print(CardId);
                        
                        //Clear the ID
                        CardId = "";

                        //Clear the array
                        Array.Clear(buf, 0, 12);
                        
                        //Clear the "byte" counter
                        readcnt = 0;
                       
                        /*Sleep for two seconds, otherwise
                         * you would get a bunch of 
                         * duplicate reads*/
                        Thread.Sleep(3000); 
                        
                        //Enable the reader
                        rfidON.SetDutyCycle(0);
                    }
                }
            }
        }
    }
}


The Wireing
Netduino ====> RFID Reader
5v Power ====> VCC
DPIN 9 ====> /Enable
DPIN 0 ====> SOUT
GRND ====> GRND
(Hope to get a schematic created soon and posted, but pretty simple to wire up w/out it)

The Pictures

Posted Image
http://picasaweb.goo...feat=directlink

Posted Image
http://picasaweb.goo...feat=directlink

http://www.youtube.com/watch?v=axo3g3EOZsI

The Bug
Now, the only problem is that I am getting duplicate code reads on one pass. It's not every time, but each time it does happen it is like it read the card twice. Every once in a while I will get "junk" reads on a card pass but that is to be expected (interference, card miss read, etc). Gold Star to the first person who can sqaush this bug! ;)

Next Steps
  • Use a USB to TTL cable to allow communication between reader and C# application
  • Wire up external power source

Please feel free to comment on / critique / improve / offer suggestions on anything you see here.

#2 JohnH

JohnH

    New Member

  • Members
  • Pip
  • 8 posts

Posted 24 August 2010 - 11:54 PM

Nice write-up ATX! And thanks for the compliment on the code. I'm glad it helped out!

#3 greg

greg

    Advanced Member

  • Members
  • PipPipPip
  • 169 posts
  • LocationChicago, IL

Posted 24 August 2010 - 11:57 PM

Easy fix for your duplicate read - add a variable for "lastTag" and when a tag is read stuff it into that variable. Next time a tag is read compare to lastTag. If it's the same, ignore the read. I do it all the time with reading tags - quick and simple.

#4 Tofer

Tofer

    New Member

  • Members
  • Pip
  • 2 posts

Posted 17 September 2010 - 07:31 AM

Gold Star to the first person who can sqaush this bug! ;)


When you "Turn OFF" the rfid reader... if you also close the SerialPort it won't read twice:

//Turn off the reader
rfidON.SetDutyCycle(100);
SPort.Close();

Then of course remember to add some code to open it again:
//Loop it forever!
while (true)
{
    if (!SPort.IsOpen) SPort.Open();

See if that works for you...

-Chris

P.S. I am trying this on my first project ever with the Netduino, and this is my first post :)

#5 CW2

CW2

    Advanced Member

  • Members
  • PipPipPip
  • 1592 posts
  • LocationCzech Republic

Posted 17 September 2010 - 08:09 AM

Please feel free to comment on / critique / improve / offer suggestions on anything you see here.

I would suggest using OutputPort's Write method to switch the reader on/of (instead of PWM).

#6 Tofer

Tofer

    New Member

  • Members
  • Pip
  • 2 posts

Posted 18 September 2010 - 09:16 AM

So like I said in my previous post above... using this RFID reader is my first project with the Netduino. I really couldn't stop myself putting this into it's own class. I also made a bunch of changes. I took the above code (sorta), incorporated a few things I saw in other posts, as well as things I do generally in .Net 4.0. I have never dealt with hardware before, and come from a web development background... so threading, file size, etc is all new to me.

This biggest thing I didn't know what I was doing with was the Timer. I have no idea if this is a good way to do it or not, but I just figured polling it every 20ms would allow other things to be going on at the same time while still being responsive.

Anyway, I would appreciate any feedback. Here is the class (usage example to follow)

using System;
using System.IO.Ports;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace Tofer.Netduino
{
    public class ParalaxRFID
    {
        private int _readDelay = 1500;
        private bool _readerEnabled = false;

        public SerialPort SerialPort { get; set; }
        public OutputPort RfidControl { get; set; }

        public delegate void CardReadEventHandler(ParalaxRFID sender, string cardID);
        public event CardReadEventHandler OnCardRead;

        public delegate void StatusChangedEventHandler(ParalaxRFID sender, bool readerEnabled);
        public event StatusChangedEventHandler OnStatusChanged;

        public int ReadDelay
        {
            get { return _readDelay; }
            set { _readDelay = value; }
        }

        public bool ReaderEnabled
        {
            get { return _readerEnabled; }
            private set 
            { 
                _readerEnabled = value;
                if (OnStatusChanged != null)
                {
                    OnStatusChanged(this, value);
                }
            }
        }
        
        public ParalaxRFID(string portName, Cpu.Pin controlPin, bool autoEnable = true)
            : this(new SerialPort(portName, 2400, Parity.None, 8, StopBits.One), new OutputPort(controlPin, false), autoEnable)
        {
        }

        public ParalaxRFID(SerialPort sPort, OutputPort rfidControl, bool autoEnable = true)
        {
            this.SerialPort = sPort;
            this.RfidControl = rfidControl;

            SerialPort.ReadTimeout = 1000;
            SerialPort.WriteTimeout = 1000;

            SerialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPort_DataReceived);
            Timer readerTimer = new Timer(new TimerCallback(DoReadCallback), null, 20, 20);
            
            if (autoEnable) EnableReader();
        }

        public void DisableReader()
        {
            RfidControl.Write(true);
            if (SerialPort.IsOpen) SerialPort.Close();
            ReaderEnabled = false;
        }

        public void EnableReader()
        {
            if (!SerialPort.IsOpen) SerialPort.Open();
            RfidControl.Write(false);
            ReaderEnabled = true;
        }

        private void DoReadDelay()
        {
            if (ReaderEnabled)
            {
                DisableReader();
                Thread.Sleep(ReadDelay);
                EnableReader();
            }
        }

        private void DoReadCallback(Object obj)
        {
            if (SerialPort.IsOpen && SerialPort.BytesToRead >= 12)
            {
                SerialPort.Flush();
            }
        }

        void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            byte[] buf = new byte[12];
            int count = SerialPort.Read(buf, 0, 12);

            if (count == 12 && buf[0] == '\n' && buf[11] == '\r')
            {
                String cardID = String.Empty;
                for (int i = 1; i < 11; i++)
                {
                    cardID += (char)buf[i];
                }

                if (OnCardRead != null)
                {
                    OnCardRead(this, cardID);
                }

                DoReadDelay();
            }
        }
    }
}

Example usage:
using System;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using Tofer.Netduino;

namespace SampleProgram
{
    public class Program
    {
        private static ParalaxRFID rfid;

        public static void Main()
        {
            rfid = new ParalaxRFID(SerialPorts.COM1, Pins.GPIO_PIN_D4);
            rfid.OnCardRead += new ParalaxRFID.CardReadEventHandler(rfid_OnCardRead);
            rfid.OnStatusChanged += new ParalaxRFID.StatusChangedEventHandler(rfid_OnStatusChanged);

            InterruptPort btn = new InterruptPort(Pins.ONBOARD_SW1, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);
            btn.OnInterrupt += new NativeEventHandler(btn_OnInterrupt);

            Thread.Sleep(Timeout.Infinite);
        }

        static void rfid_OnStatusChanged(ParalaxRFID sender, bool readerEnabled)
        {
            Debug.Print("RFID Reader: " + (readerEnabled ? "Enabled" : "Disabled"));
        }

        static void btn_OnInterrupt(uint data1, uint data2, DateTime time)
        {
            if (data2 == 0)
            {
                if (rfid.ReaderEnabled)
                {
                    rfid.DisableReader();
                }
                else
                {
                    rfid.EnableReader();
                }
            }
        }

        static void rfid_OnCardRead(ParalaxRFID sender, string cardID)
        {
            Debug.Print("CardID: " + cardID);
        }
    }
}


Or a really simple version:
using System.Threading;
using Microsoft.SPOT;
using SecretLabs.NETMF.Hardware.Netduino;
using Tofer.Netduino;

namespace SampleProgram
{
    public class Program
    {
        public static void Main()
        {
            ParalaxRFID rfid = new ParalaxRFID(SerialPorts.COM1, Pins.GPIO_PIN_D4);
            rfid.OnCardRead += new ParalaxRFID.CardReadEventHandler(rfid_OnCardRead);

            Thread.Sleep(Timeout.Infinite);
        }

        static void rfid_OnCardRead(ParalaxRFID sender, string cardID)
        {
            Debug.Print("CardID: " + cardID);
        }
    }
}

Sorry for my lack of comments in the code. Normally I comment like crazy, but I got a little lazy. Basically I have made it event driven, so you add an event listener to respond to card read events. I also added an event for when the card reader status changes, and the ability to manually enable and disable the reader (in this example by pushing the onboard button).

One last option I forgot to show in the example is you can optionally set the ReadDelay to set how long the reader turns off after a successful read before re-enabling itself. This is done by:

rfid.ReadDelay = 3000;

Enjoy...

-Chris

#7 Chris Walker

Chris Walker

    Secret Labs Staff

  • Moderators
  • 7767 posts
  • LocationNew York, NY

Posted 18 September 2010 - 04:29 PM

Hi Tofer, Thanks for sharing your project, and welcome to the forums! RFID is pretty cool... [I'll let others pitch in on your coding style questions.] Chris

#8 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 05:37 AM

This is a big help. I'm doing this project as we speak. I have a question though... The N+ states the digital I/Os are 5V "tolerant". I just wired this and measured the digital I/O lines...they are coming in as 5V. Will this cause any damage continuing to use 5V on these digital I/Os?

#9 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 08:06 AM

No matter what example I use I cannot use COM1 in any example. I also keep getting the following exception: #### Exception System.Exception - CLR_E_TIMEOUT (1) #### #### Message: #### System.IO.Ports.SerialPort::Read [IP: 0000] #### #### Program2::Main [IP: 0047] #### A first chance exception of type 'System.Exception' occurred in Microsoft.SPOT.Hardware.SerialPort.dll An unhandled exception of type 'System.Exception' occurred in Microsoft.SPOT.Hardware.SerialPort.dll

#10 Chris Walker

Chris Walker

    Secret Labs Staff

  • Moderators
  • 7767 posts
  • LocationNew York, NY

Posted 03 March 2011 - 08:20 AM

No matter what example I use I cannot use COM1 in any example. I also keep getting the following exception:


The following line of code in the DataReceived event will throw that exception if no data is received within 1 second (the read timeout specified earlier in the code):
int count = SerialPort.Read(buf, 0, 12);
Earlier versions of the Netduino firmware had a bug in them where the TimeoutException might not have been thrown after the timeout expired.

To address this issue in your code, wrap the line of code in a try...catch block. Here's one that does nothing if a timeout exception occurs.

try
{
    int count = SerialPort.Read(buf, 0, 12);
}
catch (Exception ex)
{
    // generally you would address the exception here--but we'll leave 
    // this section empty for now to suppress the timeout exception.
}


#11 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 08:56 AM

I added the try/catch, it catches every other second.

I bookmarked this page sometime ago and ordered the Parallax RFID reader. I have two programs using both the examples above. I cannot get any of them to work. I am not sure what I am doing wrong.

Here is my setup:

Posted Image

Posted Image


Here is the First Example, error:

Posted Image

Second example, error:

Posted Image

#12 Chris Walker

Chris Walker

    Secret Labs Staff

  • Moderators
  • 7767 posts
  • LocationNew York, NY

Posted 03 March 2011 - 10:20 AM

Also, make sure that you put your "Read" code in a loop if you need to wait for all 12 bytes.

For instance:
int count = 0;
while (count < 12)
{
    try 
    { 
        count += SerialPort.Read(buf, count, 12 - count);
    } 
    catch (Exception ex) 
    { 
        // if we get a timeout, go ahead and break free from the while loop.
        break;
    }
}


#13 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 05:48 PM

Also, make sure that you put your "Read" code in a loop if you need to wait for all 12 bytes.

For instance:

int count = 0;
while (count < 12)
{
    try 
    { 
        count += SerialPort.Read(buf, count, 12 - count);
    } 
    catch (Exception ex) 
    { 
        // if we get a timeout, go ahead and break free from the while loop.
        break;
    }
}


I am not even getting that far. I am using example two

 public void EnableReader()
        {
            if (!SerialPort.IsOpen) SerialPort.Open(); //Bombs Here
            RFIDOut.Write(false);
            ReaderEnabled = true;
        }


#### Exception System.InvalidOperationException - CLR_E_INVALID_OPERATION (1) ####
#### Message:
#### Microsoft.SPOT.Hardware.Port::ReservePin [IP: 0000] ####
#### System.IO.Ports.SerialPort::HandlePinReservations [IP: 003a] ####
#### System.IO.Ports.SerialPort::Open [IP: 001d] ####
#### BottleStats.ParalaxRFID::EnableReader [IP: 0013] ####
#### BottleStats.ParalaxRFID::.ctor [IP: 0067] ####
#### BottleStats.ParalaxRFID::.ctor [IP: 0016] ####
#### BottleStats.Program::InitializeSensors [IP: 0024] ####
#### BottleStats.Program::Main [IP: 0004] ####
A first chance exception of type 'System.InvalidOperationException' occurred in Microsoft.SPOT.Hardware.dll
#### Exception System.InvalidOperationException - CLR_E_INVALID_OPERATION (1) ####
#### Message:
#### System.IO.Ports.SerialPort::HandlePinReservations [IP: 0093] ####
#### System.IO.Ports.SerialPort::Open [IP: 001d] ####
#### BottleStats.ParalaxRFID::EnableReader [IP: 0013] ####
#### BottleStats.ParalaxRFID::.ctor [IP: 0067] ####
#### BottleStats.ParalaxRFID::.ctor [IP: 0016] ####
#### BottleStats.Program::InitializeSensors [IP: 0024] ####
#### BottleStats.Program::Main [IP: 0004] ####

#14 Chris Walker

Chris Walker

    Secret Labs Staff

  • Moderators
  • 7767 posts
  • LocationNew York, NY

Posted 03 March 2011 - 06:50 PM

Hi Ckiszka, Can you zip up your project and attach it to this thread? Also, what serial port are you using? Make sure that you create your serial port object before trying to use it. Otherwise you might be trying to open an "empty" serial port. Also, be sure that you're not using the pins from that serial port for something else... Chris

#15 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 07:11 PM

Hi Ckiszka,

Can you zip up your project and attach it to this thread?

Also, what serial port are you using? Make sure that you create your serial port object before trying to use it. Otherwise you might be trying to open an "empty" serial port.

Also, be sure that you're not using the pins from that serial port for something else...

Chris


I appreciate your help!

I assume pin D0 is the defacto COM1 port? In code, when setting up the SerialPort, I do not see a specified pin assignment. I am using D0.

Project Code

#16 Chris Walker

Chris Walker

    Secret Labs Staff

  • Moderators
  • 7767 posts
  • LocationNew York, NY

Posted 03 March 2011 - 07:21 PM

I assume pin D0 is the defacto COM1 port? In code, when setting up the SerialPort, I do not see a specified pin assignment. I am using D0.


That will cause a problem. :)

You are using D0 as the "outPin" and also trying to use COM1 (which requires exclusive use of pins D0 and D1). So when the program tries to open up the serial port it can't...because D0 is already taken by "outPin".

Try changing your outPin to another pin (D2 through D13 are good).

Chris

#17 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 07:43 PM

That will cause a problem. :)

You are using D0 as the "outPin" and also trying to use COM1 (which requires exclusive use of pins D0 and D1). So when the program tries to open up the serial port it can't...because D0 is already taken by "outPin".

Try changing your outPin to another pin (D2 through D13 are good).

Chris


Thanks! That fixed the error on the Port.Open.

Now I am not getting a "SerialPort_DataReceived" event. How does the program read data in from the RFID if the pinOut does not go to D0 or D1 and it is never assign an event handler or function in code?

#18 Ckiszka

Ckiszka

    Member

  • Members
  • PipPip
  • 14 posts

Posted 03 March 2011 - 08:45 PM

Chris...do you ever sleep? You have responded to me at all hours before. Thanks! I have found the error. The second example above is trying to define the “outPin” as an Output Port whereas like you said, the serial ports already claim D0 & D1. If I remove the instantiation of the Output Port (outPin…) and physically put the outPin lead into D0, it works perfect! P.S. I added an Output Port, the “enablePin” to the constructor. I am using this to disable/enable instead. Thanks again for pointing me in the right direction.

#19 afulki

afulki

    Member

  • Members
  • PipPip
  • 13 posts
  • LocationNew York

Posted 21 April 2011 - 01:04 AM

I'm another newbie to the netduino family, though I've been programing C# and .Net since around day 1 :)

Thanks to the author of the original post in this article it was a nice introduction to RFID, and all the comments above mine, they were most insightful.

I was playing around with the code this evening and the timeout error displayed in the debugger output window annoyed me, so I refactored the code a little and below is my version of the class. It does not use any timers to flush the receive buffer etc. and does not cause the timeout messages to be display.

public class ParalaxRFID
    {
        private int _readDelay = 1500;
        private bool _readerEnabled = false;

        public SerialPort SerialPort { get; set; }
        public OutputPort RfidControl { get; set; }

        public delegate void CardReadEventHandler(ParalaxRFID sender, string cardID);
        public event CardReadEventHandler OnCardRead;

        public delegate void StatusChangedEventHandler(ParalaxRFID sender, bool readerEnabled);
        public event StatusChangedEventHandler OnStatusChanged;

        private byte[] _messageBuffer = new byte[10];
        private int _messageCounter = -1; // -1 = waiting for start char, 0 or more means reading message

        public int ReadDelay
        {
            get { return _readDelay; }
            set { _readDelay = value; }
        }

        public bool ReaderEnabled
        {
            get { return _readerEnabled; }
            private set 
            { 
                _readerEnabled = value;
                if (OnStatusChanged != null)
                {
                    OnStatusChanged(this, value);
                }
            }
        }
        
        public ParalaxRFID(string portName, Cpu.Pin controlPin, bool autoEnable = true)
            : this(new SerialPort(portName, 2400, Parity.None, 8, StopBits.One), new OutputPort(controlPin, false), autoEnable)
        {
        }

        public ParalaxRFID(SerialPort sPort, OutputPort rfidControl, bool autoEnable = true)
        {
            this.SerialPort = sPort;
            this.RfidControl = rfidControl;

            SerialPort.WriteTimeout = 1000;

            SerialPort.Open();
            SerialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPort_DataReceived);
            
            if (autoEnable) EnableReader();
        }

        public void DisableReader()
        {
            RfidControl.Write(true);
            if (SerialPort.IsOpen) SerialPort.Close();
            ReaderEnabled = false;
        }

        public void EnableReader()
        {
            if (!SerialPort.IsOpen) SerialPort.Open();
            RfidControl.Write(false);
            ReaderEnabled = true;
        }

        private void DoReadDelay()
        {
            if (ReaderEnabled)
            {
                DisableReader();
                Thread.Sleep(ReadDelay);
                EnableReader();
            }
        }


        void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            SerialPort sp = (SerialPort)sender; 

            while (sp.BytesToRead > 0)
            {
                // temp buff to hold message,
                byte[] buf = new byte[1];

                // Read a single byte from the recv buffer,
                if (sp.Read(buf, 0, 1) > 0)
                {
                    // Start of Message?
                    if (buf[0] == '\n') 
                    {
                        _messageCounter = 0;
                    }
                    // End of message?
                    else if (buf[0] == '\r')
                    {
                        if (_messageCounter > 9)
                        {
                            String cardID = String.Empty;
                            for (int i = 0; i < 10; i++)
                            {
                                cardID += (char)_messageBuffer[i];
                            }

                            if (OnCardRead != null)
                            {
                                OnCardRead(this, cardID);
                            }
                        }

                        _messageCounter = -1;
                        DoReadDelay();
                    }
                    else // Store it in the global buffer, 
                    {
                        if (_messageCounter < 10)
                        {
                            _messageBuffer[_messageCounter++] = buf[0];
                        }
                        else
                            _messageCounter = -1;
                    }
                }
            }
        }
    }


#20 Michel Trahan

Michel Trahan

    Advanced Member

  • Members
  • PipPipPip
  • 155 posts

Posted 21 April 2011 - 03:43 AM

Please insert this project into the wiki :) sandbox.netduino.com THANKS FOR SHARING ! Great idea !
Started with C in 1985, moved to Vb3 ... to vb6 and stopped. Now started with .Net and learning C# and VB.net and wishing VB.net was on MF !




0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users

home    hardware    projects    downloads    community    where to buy    contact Copyright © 2016 Wilderness Labs Inc.  |  Legal   |   CC BY-SA
This webpage is licensed under a Creative Commons Attribution-ShareAlike License.