射频识别(RFID)技术

实验:值块读写操作

作者:陈广 日期:2019-2-23


实验目的

Mifare S50 卡中的数据块可用于存放数据及值,两者的格式不同,本实验讲解值块的读写操作。我们知道,一个值块的16个字节只能存放一个 32 位有符号整数,即占用 4 个字节。其中备份地址用去 4 个字节,一个值,存储三次,共用 12 个字节。本实验通过一个公交刷卡程序讲解初始化值、加值、减值、读值等操作。

地铁刷卡程序

随着学习的深入,我们开始尝试使用 UI 写程序,本实验试图模拟一个地铁刷卡程序,实现的功能有卡的初始化、充值、刷卡扣钱。刷卡扣钱需要使用被动 选卡功能(之前的《选卡操作》实验讲过),需开一个可终止线程由上位机不断进行选卡操作。这一次使用 WPF 来实现此程序,WPF 的最大好处是界面创建不再需要语言描述,直接上代码就 OK 了。将来我准备用 WPF 重写《C#程序设计基础教程与实训》,毕竟 WPF 使用起来更接近网页 UI(虽然差别还是很大)。

新建一个 WPF 应用程序,界面代码如下:

<Window x:Class="ValueOper.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ValueOper"
        mc:Ignorable="d"
        Title="值块操作" Height="300" Width="500" Loaded="Window_Loaded">
    <Grid Background="#DDD">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition Width="159*"/>
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Column="0" Margin="0,0,0.4,-0.4">
            <Button x:Name="BtnInit" Content="钱包功能初始化" Padding="5" Margin="10,25,10,5" Click="Btn_InitValue_Click"/>
            <Button x:Name="BtnAdd" Content="充值" Padding="5" Margin="10,5,10,5" Click="Btn_Increment_Click"/>
            <Grid Margin="10,5,10,5">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="充值钱数:"  VerticalAlignment="Center"/>
                <TextBox x:Name="txtAddCoin" Grid.Column="1" Text="20"  Padding="5"/>
            </Grid>
            <Button x:Name="Btn_Work" Content="进入工作状态" Padding="5" Margin="10,5,10,5" Click="Btn_Work_Click"/>
        </StackPanel>
        <ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
            <TextBox x:Name="txtMsg" TextWrapping="Wrap"/>
        </ScrollViewer>
    </Grid>
</Window>

程序代码如下:

using HBLib;
using HBLib.HR8002Reader;
using HBLib.ISO14443A;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;

namespace ValueOper
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        const byte OPER_BLOCKNUM = 1;
        ComPort com = new ComPort("COM3", 19200, 300);
        Reader reader;
        I14443A i14443a;
        byte[] operUID; //上一轮操作被扣钱的标签ID
        CancellationTokenSource ts;
        SolidColorBrush grayBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0x55, 0x55, 0x55));
        SolidColorBrush greenBrush = new SolidColorBrush(Color.FromArgb(0xFF, 0xA0, 0xBB, 0x55));

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            com.Open();
            reader = new Reader(0x00, com);
            i14443a = new I14443A(0x00, com);
            reader.ChangeToISO14443AAsync();
        }

        private void Btn_InitValue_Click(object sender, RoutedEventArgs e)
        {
            InitValue(50, OPER_BLOCKNUM); //充值 50 块钱
        }
        //开线程等待刷卡扣钱
        private void Btn_Work_Click(object sender, RoutedEventArgs e)
        {
            if (Convert.ToString(Btn_Work.Content) == "进入工作状态") //开线程
            {
                ts = new CancellationTokenSource();
                Task.Factory.StartNew(() => Work(6, OPER_BLOCKNUM), ts.Token,
                    TaskCreationOptions.LongRunning, TaskScheduler.Default);
                Btn_Work.Background = greenBrush;
                Btn_Work.Content = "退出工作状态";
                BtnInit.IsEnabled = false;
                BtnAdd.IsEnabled = false;
            }
            else //取消线程
            {
                ts.Cancel();
                Btn_Work.Background = grayBrush;
                Btn_Work.Content = "进入工作状态";
                BtnInit.IsEnabled = true;
                BtnAdd.IsEnabled = true;
            }
        }

        private void Btn_Increment_Click(object sender, RoutedEventArgs e)
        {
            Increment(int.Parse(txtAddCoin.Text), OPER_BLOCKNUM);
        }

        /// <summary>
        /// 充值
        /// </summary>
        /// <param name="coin">充入的钱数,块为单位</param>
        /// <param name="blockNum">操作的绝对块号</param>
        /// <returns></returns>
        private async Task Increment(int coin, byte blockNum)
        {
            // 选卡操作
            byte[] uid = await Select();
            if (uid == null)
            {
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.Text = "没有选中任何卡片!";
                }));
                return;
            }
            //证实
            if (!await AuthKey((byte)(blockNum / 4)))
            {
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.Text = "证实失败!";
                }));
                return;
            }
            byte[] data = BitConverter.GetBytes(coin * 100);
            var info = await i14443a.IncrementAsync(blockNum, data);
            if (info.ReturnValue == ReturnMessage.Success)
            {
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.Text = "充值成功,已充入 " + coin.ToString() + "元钱。";
                }));
                //读取剩余钱数
                var info1 = await i14443a.ReadValueAsync(blockNum);
                if (info1.ReturnValue == ReturnMessage.Success)
                {
                    int remain = BitConverter.ToInt32(info1.ValueData, 0);
                    string money = Convert.ToString(remain / 100);
                    this.Dispatcher.Invoke(new Action(() =>
                    {
                        txtMsg.AppendText("卡内剩余:" + money + " 块钱。");
                    }));
                }
            }
        }

        /// <summary>
        /// 让程序处于工作状态,等待刷卡并扣钱
        /// </summary>
        /// <param name="coin">每次刷卡扣的钱数</param>
        /// <returns></returns>
        private async Task Work(int coin, byte blockNum)
        {
            byte[] data = BitConverter.GetBytes(coin * 100);
            while (!ts.IsCancellationRequested)
            {
                // 选卡
                byte[] uid = await Select();
                if (uid != null)
                {
                    if (CompareUID(uid, operUID)) //如果此卡之前已经扣过钱且未离开感应区,则不操作
                    {
                        continue;
                    }
                }
                else
                {
                    operUID = null; //说明卡片已经离开感应区
                    continue;
                }
                //证实
                if (!await AuthKey((byte)(blockNum / 4))) continue;
                //读取剩余钱数,判断卡内剩余的钱是否够扣
                var info1 = await i14443a.ReadValueAsync(blockNum);
                if (info1.ReturnValue == ReturnMessage.Success)
                {
                    int remain = BitConverter.ToInt32(info1.ValueData, 0);
                    if (remain < coin * 100)
                    {
                        string money = Convert.ToString(remain / 100);
                        this.Dispatcher.Invoke(new Action(() =>
                        {
                            txtMsg.AppendText("\r\n卡内余额不足,现剩余:" + money + " 块钱。");
                        }));
                        operUID = uid;
                        continue;
                    }

                }

                var info = await i14443a.ValueAsync(OperCode.Decrease, blockNum, data, blockNum); //扣钱
                if (info.ReturnValue != ReturnMessage.Success) continue;
                operUID = uid; //记录被扣钱的标签UID,以防下次重复扣钱
                await reader.BeepAsync(5, 0, 1); //蜂鸣器响一声
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.AppendText("\r\n成功刷卡,扣除" + coin.ToString() + "块钱。");
                }));
                //读取剩余钱数
                info1 = await i14443a.ReadValueAsync(blockNum);
                if (info1.ReturnValue == ReturnMessage.Success)
                {
                    int remain = BitConverter.ToInt32(info1.ValueData, 0);
                    string money = Convert.ToString(remain / 100);
                    this.Dispatcher.Invoke(new Action(() =>
                    {
                        txtMsg.AppendText("\r\n卡内剩余:" + money + " 块钱。");
                    }));
                }
                Thread.Sleep(300);
            }
        }

        /// <summary>
        /// 钱包初始化
        /// </summary>
        /// <param name="coin">第一次充入的钱,以元为单位</param>
        /// <param name="blockNum">用于初始化的绝对块号</param>
        /// <returns></returns>
        private async Task InitValue(int coin, byte blockNum)
        {
            // 选卡操作
            byte[] uid = await Select();
            if (uid == null)
            {
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.Text = "没有选中任何卡片!";
                }));
                return;
            }
            //证实
            if (!await AuthKey((byte)(blockNum / 4)))
            {
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.Text = "证实失败!";
                }));
                return;
            }
            //初始化钱包
            byte[] data = BitConverter.GetBytes(coin * 100);
            var info = await i14443a.InitValueAsync(blockNum, data);
            if (info.ReturnValue == ReturnMessage.Success)
            {
                this.Dispatcher.Invoke(new Action(() =>
                {
                    txtMsg.Text = "钱包初始化完毕,已充入 " + coin.ToString() + "元钱。";
                }));
            }
        }

        /// <summary>
        /// 选卡操作
        /// </summary>
        /// <returns>返回被选中的标签的 UID,如果未选中标签,则返回 null</returns>
        private async Task<byte[]> Select()
        {
            //请求操作
            var info = await i14443a.RequestAsync(RequestMode.AllCard);
            if (info.ReturnValue != ReturnMessage.Success)
            {
                return null;
            }
            //防冲突操作,感应区内只允许放置一张卡片
            var info1 = await i14443a.Anticoll2Async(MultiLable.AllowOne);
            if (info1.ReturnValue != ReturnMessage.Success)
            {
                return null;
            }
            //选卡操作
            var info2 = await i14443a.SelectAsync(info1.UID);
            if (info2.ReturnValue != ReturnMessage.Success)
            {
                return null;
            }
            return info1.UID;
        }

        /// <summary>
        /// 证实指定扇区
        /// </summary>
        /// <param name="sectorNum">要证实的扇区</param>
        /// <returns>证实成功则返回 true,否则返回 fasle。</returns>
        private async Task<bool> AuthKey(byte sectorNum)
        {
            byte[] keyA = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
            //密码 A 证实
            var info3 = await i14443a.AuthKeyAsync(KeyType.KeyA, sectorNum, keyA);
            if (info3.ReturnValue != ReturnMessage.Success)
            {
                Console.WriteLine(info3.GetStatusStr());
                return false;
            }
            return true;
        }

        private bool CompareUID(byte[] uid1, byte[] uid2)
        {
            if (uid1 == null || uid2 == null) return false;
            if (uid1.Length != uid2.Length) return false;

            for (int i = 0; i < uid1.Length; i++)
            {
                if (uid1[i] != uid2[i])
                {
                    return false;
                }
            }
            return true;
        }
    }
}

功能不多,但代码不少。没办法写程序就是这样,要相对完善,就得代码多,虽然这里还是有不少需要完善的地方。运行程序,拿一张标签放入感应场(此程序只能在感应场内放一张标签,放多张无效)。

图 1:地铁刷卡程序

先单击【钱包功能初始化】按钮进行初始化,然后将标签拿走,单击【进入工作状态】,将标签不断放入并离开感应场进行刷卡,查看实验结果。钱用完后可以单击【退出工作状态】后,再单击【充值】按钮进行充值。

;

© 2018 - IOT小分队文章发布系统 v0.3