Как стать автором
Обновить

Читаем и пишем NFC Tag на MeeGo Harmattan

Время на прочтение14 мин
Количество просмотров13K
Этот пост участвует в конкурсе „Умные телефоны за умные посты

Введение


Что такое NFC?

Если верить Википедии, NFC(Near Field Communication) — это технология беспроводной высокочастотной связи малого радиуса действия, которая дает возможность обмена данными между устройствами, находящимися на расстоянии около 10 сантиметров.

Существует три наиболее популярных варианта использования NFC технологии в мобильных телефонах:
эмуляция карт — телефон прикидывается картой, например пропуском или платежной картой;
режим считывания — телефон считывает пассивную метку (Tag), например для интерактивной рекламы;
режим P2P — два телефона связываются и обмениваются информацией.

Мы будем рассматривать второй способ использования, а именно чтение пассивной метки, мало того, мы также научимся записывать информацию на такие метки с помощью телефона

О чем рассказ?

Я буду рассказывать не только о методах работы с NFC, но и о пользовательском интерфейсе разработанной мной специально для этой статьи программы. То есть в процессе чтения вы пройдете полный путь создания приложения для работы с NFC Tag для MeeGo Harmattan.

Оглавление




Qt Ambassador
UPDATE: Сегодня, 20.12.2011, пришло письмо, что приложение приняли в Qt Ambassador
UPDATE: Прошла ночь и проект был опубликован в программе: Qt Ambassador Showcase



Что такое NFC Tag?

NFC Tag — это и есть наша пассивная метка. На картинке показан внешний вид того как она может выглядеть, то есть, как правило, это наклейка из плотной бумаги, в которую встроен микрочип и антенна из фольги. NFC теги бывают нескольких типов, от типа также зависит максимальный допустимый размер данных. Я являюсь счастливым обладателем нескольких меток Type 2, 192 байта, привезенных с Qt Developer Days 2011. Чтож, 192 байта не густо, но для наших экспериментов хватит.

Логика программы


Заставляем приложение перехватывать обработку NFC Tags

Итак, для того чтобы начать обрабатывать теги нам потребуется объект класса QNdefManager
NfcManager::NfcManager(QObject *parent)
    : QObject(parent), m_manager(new QNearFieldManager(this)), m_cachedTarget(0), m_mode(NfcManager::Read)
{
    connect(m_manager, SIGNAL(targetDetected(QNearFieldTarget*)), this, SLOT(targetDetected(QNearFieldTarget*)));
    connect(m_manager, SIGNAL(targetLost(QNearFieldTarget*)), this, SLOT(targetLost(QNearFieldTarget*)));
    m_manager->setTargetAccessModes(QNearFieldManager::NdefReadTargetAccess | QNearFieldManager::NdefWriteTargetAccess);
}

Создадим его в конструкторе нашего класса NfcManager, который мы будем использовать для работы с NFC. Нам обязательно надо подключить сигналы targetDetected и targetLost этого объекта к нашим слотам, которые, собственно, и будут являться главными обработчиками события появления тега в поле зрения телефона. В третьей строке конструктора мы выставляем режим чтения и записи, чтобы мы могли не только читать, но и записывать теги.

Перехватчик

Теперь рассмотрим описанные слоты:
void NfcManager::targetDetected(QNearFieldTarget *target)
{
    if (m_cachedTarget)
        delete m_cachedTarget;
    m_cachedTarget = target;

    if (m_mode == Read)
        readTarget(m_cachedTarget);
    if (m_mode == Write)
        writeTarget(m_cachedTarget);
}

При обнаружении тега мы на всякий случай сохраняем указатель на объект типа QNearFieldTarget, который является программным интерфейсом к самому тегу.
Далее идут два условия и в зависимости от режима (чтение или запись) вызывают соответствующие методы обработки. С точки зрения красивой архитектуры, это не самое лучшее решение, но я сделал это преднамеренно, чтобы не усложнять код.

void NfcManager::targetLost(QNearFieldTarget *target)
{
    m_cachedTarget = 0;
    target->deleteLater();
}

При потере тега, мы просто освобождаем занятые ресурсы.

Чтение

Теперь рассмотрим методы чтения тега:
void NfcManager::readTarget(QNearFieldTarget *target)
{
    connect(target, SIGNAL(error(QNearFieldTarget::Error,QNearFieldTarget::RequestId)), this, SLOT(errorHandler(QNearFieldTarget::Error,QNearFieldTarget::RequestId)));
    connect(target, SIGNAL(ndefMessageRead(QNdefMessage)), this, SLOT(readRecords(QNdefMessage)));
    target->readNdefMessages();

}

Чтение происходит в асинхронном режиме, поэтому в данном методе мы просто подключаем сигнал обработки ошибок и сигнал завершения чтения, который вызовется только в случае, если чтение произошло без ошибок.
После этого мы просто вызываем метод для чтения:
void NfcManager::readRecords(const QNdefMessage &message)
{
    if (message.isEmpty())
        return;

    QNdefRecord record = message.at(0); // Read only first
    readRecord(record);
}

Если чтение прошло успешно, то мы попадем в данный слот, где получим первую запись из списка присутсвующих на теге записей.
Да-да, по спецификации на теге может быть несколько записей, но как говорит документация, для Symbian и Harmattan доступно чтение и запись только одной записи.

void NfcManager::readRecord(const QtMobility::QNdefRecord &record)
{
    DataContainer *result = 0;

    if (record.isRecordType<QNdefNfcUriRecord>()) {
        QNdefNfcUriRecord uriRecord(record);
        result = new UriDataContainer(uriRecord.payload(), uriRecord.uri().toString());
    }
    else if (record.isRecordType<QNdefNfcTextRecord>()) {
        QNdefNfcTextRecord textRecord(record);
        result = new TextDataContainer(textRecord.payload(), textRecord.text());
    }
    else if (record.isRecordType<NdefNfcSmartPosterRecord>()) {
        NdefNfcSmartPosterRecord smartPosterRecord(record);
        result = new SmartPosterDataContainer(smartPosterRecord.payload(), smartPosterRecord.uri().toString(), smartPosterRecord.title());
    }
    else {
        result = new DataContainer(record.payload());
    }

    emit tagReadFinished(result);
}

И вот, после нескольких переходов по вспомогательным методам мы добрались до самого главного метода, который превращает информацию закодированную в теги в привычные нам буквы.
На данный момент Qt Mobility из коробки поддерживает только два вида записей это ссылки (Uri) и текст (Text), к третьему типу — Smart Poster мы еще вернемся ниже.
Как можно заметить данные из записи сразу помещаются в некий новый объект, это простые объекты, которые я специально создал для облегчения передачи данных в QML

В конце вызывается сигнал, содержащий объект с данными. В дальнейшем мы будем ловить этот сигнал в QML.

Запись

void NfcManager::setDataForWrite(const QString &text, const QString &uri)
{
    m_textForWrite = text;
    m_uriForWrite = uri;
}

Этот метод должен вызываться перед попыткой записи для того, чтобы установить новые значения Uri и/или Text. Если его не вызвать на тег будут записаны предыдущие данные (такой подход пригодится, если нужно записать много однотипных тегов)

void NfcManager::writeTarget(QNearFieldTarget *target)
{
    if (m_textForWrite.isEmpty() && m_uriForWrite.isEmpty())
        return;

    m_cachedTarget = target;
    QNdefMessage message;

    if (!m_textForWrite.isEmpty() && !m_uriForWrite.isEmpty()) {
        NdefNfcSmartPosterRecord smartPosterRecord;
        smartPosterRecord.setTitle(m_textForWrite);
        smartPosterRecord.setUri(QUrl(m_uriForWrite));
        message.append(smartPosterRecord);
    } else if (!m_textForWrite.isEmpty()) {
        QNdefNfcTextRecord textRecord;
        textRecord.setText(m_textForWrite);
        message.append(textRecord);
    } else {
        QNdefNfcUriRecord uriRecord;
        uriRecord.setUri(QUrl(m_uriForWrite));
        message.append(uriRecord);
    }

    connect(target, SIGNAL(error(QNearFieldTarget::Error,QNearFieldTarget::RequestId)), this, SLOT(errorHandler(QNearFieldTarget::Error,QNearFieldTarget::RequestId)));
    connect(target, SIGNAL(ndefMessagesWritten()), this, SIGNAL(tagWriteFinished()));
    target->writeNdefMessages(QList<QNdefMessage>() << message);
}

Главный метод записи не сложнее чем метод чтения. В блоке условий мы просто выбираем вид записи. Если присутствует только Uri или Text, то создается соответствующий тип, если же заполнены оба поля то создается запись типа Smart Poster.
После этого мы снова подключаем обработчик ошибок. Но, обратите внимание, поскольку в бэкенде нам не нужно никакой логики обработки успешного завершения чтения, мы пробрасываем сигнал на сигнал, который в дальнейшем поймаем в QML.

Smart Poster, что это?

Итак, Smart Poster это особый вид NFC записи, который может содержать в себе одновременно ссылку, текстовый заголовок (на нескольких языках), графические иконки в форматах jpeg или png и даже анимированную иконку в формате mpeg.
Помимо этого могут присутсвовать еще два поля:
Action — подсказывает телефону какое приложение и как нужно открыть для обработки uriRecord
Size — простое целочисленное число, отображающее размер загружаемого контента по ссылке.

Пишем свой класс для Smart Poster

Ниже я расскажу как создать свой тип NDEF записи на примере создания типа для Smart Poster записи.
Сразу оговорюсь, что мой тип упрощен. Он не поддерживает ни Action ни Size ни даже иконки, но он позволяет одновременно хранить текст и ссылку.

Так выглядит объявление для класса нашего Smart Poster'а:
class NdefNfcSmartPosterRecord : public QNdefRecord
{
public:
    Q_DECLARE_NDEF_RECORD(NdefNfcSmartPosterRecord, QNdefRecord::NfcRtd, "Sp", QByteArray())

    void setTitle(const QString &title, const QString &locale = "en");
    void setUri(const QUrl &uri);

    QString title(const QString &locale = "en") const;
    QUrl uri() const;

    //TODO:  Add icon, action and size fields support
private:
     RecordPart readPart(int &offset) const;
};

Q_DECLARE_ISRECORDTYPE_FOR_NDEF_RECORD(NdefNfcSmartPosterRecord, QNdefRecord::NfcRtd, "Sp")

Итак, разработчики Qt Mobility уже позаботились о том, чтобы нам было проще жить, и создали два специальных макроса, которые выполняют всю самую черновую работу.

Параметрами для макросов служат: имя класса, тип записи (для Smart Poster'а это QNdefRecord::NfcRtd) и «Имя типа» — аббревиатура для распознавания в теге. А также последний параметр в Q_DECLARE_NDEF_RECORD являются данные для первоначальной инициализации данных, в нашем случае это пустой массив байт.

Теперь посмотрим на реализацию методов чтения и записи.

Простая структура для хранения разобранной части записи:
struct RecordPart {
    enum Type {
        Uri,
        Text,
        Action,
        Icon,
        Size,
        Unknown
    };

    Type type;
    QString text;
    QString locale; // For text type
    quint8 prefix; // For Uri type
    RecordPart()
        : type(Unknown), text(QString()), locale(QString()), prefix(0)
    {

    }
};


Для начала рассмотрим методы для чтения:
static const char * const abbreviations[] = {
    0,
    "http://www.",
    "https://www.",
    "http://",
//  пропускаю множество форматов
    "urn:epc:",
    "urn:nfc:",
};

Массив различных префиксов для uri поддерживаемых спецификацией.

QUrl NdefNfcSmartPosterRecord::uri() const
{
    const QByteArray p = payload();

    if (p.isEmpty())
        return QUrl();

    if (p.isEmpty())
        return QString();

    int offset = 0;
    QString uri;

    while (offset < p.size()) {
        RecordPart part = readPart(offset);
        if (part.type == RecordPart::Uri) {
            if (part.prefix > 0 && part.prefix < (sizeof(abbreviations) / sizeof(*abbreviations)))
                uri = QString(abbreviations[part.prefix]) + part.text;
        }
    }

    if (uri.isEmpty())
        return QUrl();

    return QUrl(uri);
}

Метод чтения uri достаточно прост на первый вгляд — мы загружаем в p все байты прочитанные из записи, а дальше читаем в массиве части до тех пор пока не найдем часть типа Uri (по спецификации она может быть только одна)
«Волшебный» метод readPart мы рассмотрим чуть ниже.

QString NdefNfcSmartPosterRecord::title(const QString &locale) const
{
    const QByteArray p = payload();

    if (p.isEmpty())
        return QString();

    int offset = 0;
    QMap<QString, QString> title;

    while (offset < p.size()) {
        RecordPart part = readPart(offset);
        if (part.type == RecordPart::Text) {
            title.insert(part.locale, part.text);
        }
    }

    if (title.isEmpty())
        return QString();
    if (title.contains(locale))
        return title.value(locale);
    if (title.contains("en"))
        return title.value("en");
    return title.constBegin().value();
}

Метод для title отличается только тем, что тайтлов может быть много на разных языках. Поэтому мы сначала выбираем их все, а потом пытаемся найти нужный.

Вся магия происходит в методе readPart, который превращает внутренний формат записи в простую и понятную структуру RecordPart
RecordPart NdefNfcSmartPosterRecord::readPart(int &offset) const
{
    RecordPart result;

    const QByteArray p = payload();

.....
    //This block has pointer arithmetic, don't edit

    quint8 typeLength = p[++offset];
    quint8 payloadLength = p[++offset];
    QString type = QString(p.mid(++offset, typeLength));
    offset += typeLength - 1;

    if (type == "U") {
        result.type = RecordPart::Uri;
        result.prefix = p[++offset];
        result.text = QString(p.mid(++offset, payloadLength - 1));
        offset += payloadLength - 1;
    }

    if (type == "T") {
        result.type = RecordPart::Text;
        quint8 localeLength = p[++offset];

        result.locale = QString(p.mid(++offset, localeLength)); // 5 bytes of locale string
        offset += localeLength - 1;
        result.text = QString(p.mid(++offset, payloadLength - 1 - localeLength));
        offset += payloadLength - 1 - localeLength;
    }
.....
    //TODO: Add handler for icon

    return result;
}

Заголовок каждого блока состоит из:
1 байт, технические флаги, в нашем упрощенном классе мы не будем проверять целостность и т. д., поэтому мы просто пропускаем этот байта;
2 байт, длина строки с «Именем типа»;
3 байт, длина основого поля с информацией;
4-n байты, строка с «Именем типа» — строковый идентификатор типа, для Text равен 'T', для Uri — 'U'.
Дальше идет основной блок данных.

Для Uri это всего два поля
1 байт на номер префикса из массива выше и все остальное на тело ссылки.

Для Text это три поля:
1 байт поле статуса, в котором содержатся дополнительные флаги и длина сроки локали.
Строка локали, например "en" или "ru-RU"
И, собственно, сам текст.

Обратите внимание, что данный метод принимает offset по неконстантной ссылке, и модифицирует его, таким образом позволяя нам переходить от одной записи к другой в цикле.

Теперь поговорим о методах записи. Для простоты рассмотрим только setUri. Метод для тайтла относительно идентичен.
void NdefNfcSmartPosterRecord::setUri(const QUrl &uri)
{
    //Don't edit - pointer arithmetic

    QByteArray p;

    int abbrevs = sizeof(abbreviations) / sizeof(*abbreviations);

    for (int i = 1; i < abbrevs; ++i) {
        if (uri.toString().startsWith(QLatin1String(abbreviations[i]))) {

            p[0] = i;
            p += uri.toString().mid(qstrlen(abbreviations[i])).toUtf8();
        }
    }

    QByteArray oldPayload = payload();

    QByteArray uHeader(4, 0);

    uHeader[0] = 0b01 + 0b00010000;
    uHeader[1] = 1;
    uHeader[2] = p.size();
    uHeader[3] = 'U';

    if (!oldPayload.isEmpty())
    {
        uHeader[0] = uHeader[0] + 0b10000000;
        // change MB flag here
        oldPayload[0] = oldPayload[0] & 0b01111111;
    }

    if (oldPayload.isEmpty())
    {
        uHeader[0] = uHeader[0] + 0b10000000 + 0b01000000;
    }

    p.prepend(uHeader);

    p.append(oldPayload);
    setPayload(p);
}

Сложность методов установки в том, что необходимо учитывать случай, когда некоторая часть Smart Poster (например Text) уже установлена, и теперь необходимо установить еще и Uri. А это значит что мы должны сохранить и старый payload и добавить новый. Казалось бы, проблемы никакой в конкатенации двух QByteArray быть не может, но тут в игру вступают те самые первые байты с флагами, дело в том что нам необходимо модифицировать флаг первой части (MB) при добавлении новой.
Этим и занимается эта строчка кода:
// change MB flag here
oldPayload[0] = oldPayload[0] & 0b01111111;

Как можно заметить мы добавляем новую часть перед старой, а не после. Это лишь потому, что если бы мы добавляли в конец, нам для поиска флага последней части и его модификации (ME)
пришлось бы бегать по всему старому payload.

На этом все про Smart Poster и про NFC в целом.

Интерфейс программы


Page и PageStack

Основная идея мобильных приложений на QML это переключение экранов в очереди. В терминах Qt компонентов экраны называются страницами, а главный контейнер окном.
import QtQuick 1.1
import com.nokia.meego 1.0

PageStackWindow {
    id: appWindow

    initialPage: mainPage

    MainPage {
        id: mainPage
    }
}

main.qml здесь создает контейнер-окно и указывается главная страница в качестве страницы инициализации.

Page {
    id: mainPage
.....
    Header {
        id: header

        anchors {
            top: parent.top
            right: parent.right
            left: parent.left
        }
    }
.....

Так выглядит описание страницы. Кстати если вы обратили внимание, то все стандартные приложения от Nokia имеют аккуратный цветной заголовок. Так вот стандартного компонента для этого заголвока нет, не смотря на то, что они побуждают использовать его везде в своих UI Guidelines.

Для перехода между страницами используется объект типа PageStack, любая Page имеет указатель на инстанс этого класса с именем pageStack. Таким образом, чтобы перейти на новую страницу мы должны использовать конструкцию
pageStack.push(Qt.resolvedUrl("NewPage.qml"))


а чтобы вернуться на предыдущую:
pageStack.pop()

Кстати, если для метода pop задать параметром идентификатор конкретной страницы, то можно вернуться не только на страницу назад, но и на любую произвольную находящуюся в стеке.

ListView

На главном экране мы можем наблюдать список действий которые можно выполнить, подобный список делается так:
ListView {
    id: actionList

....
    delegate: ListDelegate {
        anchors {
            left : parent.left
            leftMargin: 20
        }

        onClicked: {
            pageStack.push(Qt.resolvedUrl(model.source))
        }

        MoreIndicator {
            anchors {
                verticalCenter: parent.verticalCenter
                right: parent.right
                rightMargin: 30
            }
        }
    }

    model: ListModel {
        ListElement {
            title: "Read Tag"
            subtitle: ""
            source: "ReadPage.qml"
        }
        ......
    }
}

Элемент ListView является собственно самим списком, у которого есть два ключевых свойства
delegate — делегат для отрисовки одного элемента списка и model — модель данных для списка.
Пакет com.nokia.extras содержит уже готовый компонент ListDelegate для создания простого делегата. Элемент ListModel позволяет задать простую модель данных. А ListElement — ни что иное как одна запись этой модели.

Toolbar

Для различных действий у мобильного приложения также может присутствовать Toolbar с иконками, мое приложение простое и тулбар на внутренних страницах содержит только кнопку назад
Page {
    id: readPage

.....    
    tools: ToolBarLayout {
        ToolIcon {
            iconId: "toolbar-back"

            onClicked: {
                pageStack.pop()
            }
        }
    }
.....

Для того чтобы подключить тулбар к странице его надо присвоить свойству tools, которое по умолчанию равно null

Label и TextField

Для отображения текста можно использовать компонент Label — это не более чем стилизованная обертка над стандартным Text элементом.
Label {
    id: touchLabel
.....
    font.pixelSize: 60
    text: qsTr("Touch a tag")
}


А для поля ввода следует использовать TextField — это продвинутая обертка над стандартным TextInput
TextField {
    id: textEdit
.....
    placeholderText: qsTr("Text")
    text: "yandex"
}


InfoBanner

В случае, если при чтении/записи тега произошла ошибка, мы должны каким-то образом сообщить пользователю об этом и попросить поднести метку к телефону еще раз, для этого можно использовать элемент InfoBanner
InfoBanner{
    id: errorBanner
    timerEnabled: true
    timerShowTime: 3 * 1000
    topMargin: header.height + 20
    leftMargin: 20
}


Соединяем все вместе


Мы рассмотрели по отдельности все основные QML компоненты которые потребуются для нашего приложения, а также всю необходимую логику программы. Настало время связать обе части вместе.

setContextProperty

Для того, чтобы наш QML код мог видеть наш класс для управления чтением и записью, мы должны сообщить декларативному движку о существовании объекта этого класса, поэтому в main.cpp мы пишем:
NfcManager *nfcManager = new NfcManager();
viewer->rootContext()->setContextProperty("NfcManager", nfcManager);

То есть мы создаем объект NfcManager и указываем движку, что мы должны иметь доступ к нему из QML.

Кстати в последнем обновлении QtSDK что-то сломали, и для того, чтобы этот код корректно заработал, надо применить workaround описанный в багтрекере.

qmlRegisterType

Как вы, конечно, помните после того как метка была прочтена мы испускаем сигнал содержащий объект с полученными данными. Для того чтобы этот объект стал доступен в QML мы должны зарегистрировать класс этого объекта в QML
qmlRegisterType<DataContainer>();
qmlRegisterType<UriDataContainer>();
qmlRegisterType<TextDataContainer>();
qmlRegisterType<SmartPosterDataContainer>();

Вставив этот код в main.cpp, мы регистрируем классы данных для всех типов имеющихся у нас данных.
Однако, запрещаем создавать подобные объекты напрямую из QML.

Взаимодействие

Когда пользователь попадает на страницу для записи или чтения метки, мы должны выполнить следующий код:

Для чтения
function tagWasRead(container) {
    NfcManager.stopDetection()

    readPage.dataContainer = container
    pageStack.push(Qt.resolvedUrl("ReadResultPage.qml"), {dataContainer: readPage.dataContainer})
}

function readError(string) {
    errorBanner.text = string
    errorBanner.show()
}

Component.onCompleted: {
    NfcManager.tagReadFinished.connect(readPage.tagWasRead)
    NfcManager.accessError.connect(readPage.readError)
    NfcManager.setReadMode()
    NfcManager.startDetection()
}

Метод Component.onCompleted выполняется когда страница полностью создана. В этом методе мы цепляем обработчики для ошибки и для успешного результата к нашим сигналам из NfcManager (обратите внимание на синтаксис подключения С++ сигнала к QML слоту)
После, мы выставляем режим для чтения и сообщаем нашему менеджеру, что следует ожидать прикладывания тега.

Также обратите внимание на вызов push
pageStack.push(Qt.resolvedUrl("ReadResultPage.qml"), {dataContainer: readPage.dataContainer})

второй параметр, позволяет нам передать контейнер с данными на следущую страницу, которая просто его обработает

пример:
.....
Label {
    id: rawDataLabel

    width: parent.width

    font.pixelSize: 30
    font.family: "Courier New"
    text: readPage.dataContainer.rawHexData()
    wrapMode: Text.WrapAnywhere
}
.....


Для записи
function tagWasWritten() {
.....    
}

function writeError(string) {
.....    
}

Component.onCompleted: {
    NfcManager.tagWriteFinished.connect(writePage.tagWasWritten)
    NfcManager.accessError.connect(writePage.writeError)
    NfcManager.setWriteMode()
    NfcManager.setDataForWrite(writePage.text, writePage.uri)
    NfcManager.startDetection()
}

Очень похоже, не правда ли? Единственным отличием является вызов метода setDataForWrite, который передает данные для записи.

Заключение


Таким образом мы получили простое, но функциональное приложение для платформы MeeGo Harmattan. Впрочем, минимальными усилиями можно превратить его в приложение для Symbian. Насколько мне известно, некоторые телефоны на Symbian (С7 например) также имеют встроенный NFC чип.
Еще хочется добавить, что формально на NFC Tag можно записать информацию в любом формате что сделает его понятным только для вашего приложения. Таким образом можно придумать еще много способов использования этой технологии.

Что почитать

Если вас заинтересовала эта тема, рекомендую ознакомиться с официальными спецификациями NFC и NDEF. Их можно скачать по запросу абсолютно бесплатно, с этой страницы.
Документация по Qt Connectivity включена в QtSDK, но иногда, например при разработке собственного формата QNdefRecord, ее не достаточно, тогда милости прошу в исходники Qt Mobility — много интересного можно почерпнуть там.
По MeeGo Qt Components также сужествует официальная документация в QtSDK, но порой она оставляет желать лучшего я рекомендую ознакомится с кодом qt-components-examples которые можно найти здесь.

Дополнительно

Я собираюсь продолжать развивать приложение, и вероятно это не последняя публикация на тему NFC Tag.
Чтобы оставаться в курсе вы можете наблюдать за проектом на gitorius
Или подписаться на мой блог, ссылку на который можно найти в профиле.
В ближайшем будущем, я планирую разместить приложение в Nokia Store, так что ищите его там.
Сейчас deb пакет можно скачать здесь.

Благодарности

Выражаю благодарность за вычитку текста статьи на предмет ошибок и опечаток хабрапользователям:
dreary_eyes и tass.
Теги:
Хабы:
+22
Комментарии6

Публикации

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн