This guide explains how to add support for new NFC readers or tag types to the davi-nfc-agent.
┌─────────────────────────────────────────────────────────────┐
│ MultiManager │
│ Aggregates multiple managers, routes device requests │
├──────────────────┬──────────────────┬───────────────────────┤
│ HardwareManager │ remotenfc.Manager│ YourManager │
│ (PC/SC readers) │ (WebNFC/mobile) │ (custom) │
└────────┬─────────┴────────┬─────────┴──────────┬────────────┘
│ │ │
▼ ▼ ▼
Device Device Device
│ │ │
▼ ▼ ▼
Tag[] Tag[] Tag[]
| Interface | Purpose |
|---|---|
Manager |
Device discovery and connection |
Device |
Hardware communication |
Tag |
Tag operations (read/write/transceive) |
package myreader
import "github.com/dotside-studios/davi-nfc-agent/nfc"
type MyManager struct {
// Your connection state (USB, serial, network, etc.)
}
func NewManager() *MyManager {
return &MyManager{}
}
// ListDevices returns available device identifiers
func (m *MyManager) ListDevices() ([]string, error) {
// Enumerate connected devices
// Return identifiers like "myreader:usb:001" or "myreader:192.168.1.100"
return []string{"myreader:default"}, nil
}
// OpenDevice opens a device by its identifier
// The device should be fully initialized and ready to use when returned
func (m *MyManager) OpenDevice(deviceStr string) (nfc.Device, error) {
// Parse deviceStr and connect to the hardware
device := &MyDevice{
connection: deviceStr,
}
// Perform any device-specific initialization here
// The returned device should be ready to use immediately
return device, nil
}
type MyDevice struct {
connection string
// Your hardware handle (serial port, USB handle, socket, etc.)
}
func (d *MyDevice) Close() error {
// Clean up resources
return nil
}
func (d *MyDevice) String() string {
return "My NFC Reader"
}
func (d *MyDevice) Connection() string {
return d.connection
}
// DeviceType returns the device type identifier (implements DeviceInfoProvider)
func (d *MyDevice) DeviceType() string {
return "myreader"
}
// SupportedTagTypes returns supported tag types (implements DeviceInfoProvider)
func (d *MyDevice) SupportedTagTypes() []string {
return []string{"MIFARE Classic", "NTAG"}
}
func (d *MyDevice) Transceive(txData []byte) ([]byte, error) {
// Send raw bytes to the reader and return response
// This is for device-level commands, not tag communication
return nil, nfc.NewNotSupportedError("Transceive")
}
func (d *MyDevice) GetTags() ([]nfc.Tag, error) {
// Poll for tags on the reader
// Return detected tags
// Example: detect a tag and wrap it
tagUID := "04A1B2C3D4E5F6"
tagType := "MIFARE Classic 1K"
tag := &MyTag{
uid: tagUID,
tagType: tagType,
device: d,
}
return []nfc.Tag{tag}, nil
}
The Tag interface bundles several role interfaces (identity, connection, read,
write, transceive, lock). Rather than implement all of them, embed
nfc.BaseTag and override only the methods your tag actually supports. The
base provides safe defaults: connection is a no-op, and write/transceive/lock
operations report “not supported”.
You only ever need to implement the four methods that have no sensible default:
UID(), Type(), NumericType(), and ReadData().
type MyTag struct {
nfc.BaseTag // supplies Connect/Disconnect/WriteData/Transceive/IsWritable/CanMakeReadOnly/MakeReadOnly
uid string
tagType string
device *MyDevice
}
// --- TagIdentifier (required) ---
func (t *MyTag) UID() string {
return t.uid
}
func (t *MyTag) Type() string {
return t.tagType
}
func (t *MyTag) NumericType() int {
return 0 // Your type code
}
// --- TagReader (required) ---
func (t *MyTag) ReadData() ([]byte, error) {
// Read NDEF data from the tag and return raw NDEF bytes.
return nil, nil
}
// --- TagCapabilityProvider (optional but recommended) ---
func (t *MyTag) Capabilities() nfc.TagCapabilities {
return nfc.TagCapabilities{
CanRead: true,
CanWrite: true,
CanTransceive: false,
CanLock: false,
TagFamily: "MIFARE Classic",
Technology: "ISO14443A",
MemorySize: 1024,
SupportsNDEF: true,
}
}
// --- Override only what you support ---
// If your tag is writable, override WriteData (otherwise it inherits the
// BaseTag default that returns a NotSupported error):
func (t *MyTag) WriteData(data []byte) error {
// Write NDEF data to the tag.
return nil
}
// Connect/Disconnect/Transceive/IsWritable/CanMakeReadOnly/MakeReadOnly are all
// inherited from nfc.BaseTag. Override any of them the same way if your tag
// supports them.
Keep
Capabilities()in sync with what you actually override: if you advertiseCanWrite: true, make sure you overrideWriteData. See Capability-Based Implementation for a test helper that catches drift.
In your main.go or initialization code:
import (
"github.com/dotside-studios/davi-nfc-agent/nfc"
"github.com/dotside-studios/davi-nfc-agent/nfc/multimanager"
"myproject/myreader"
)
func main() {
manager := multimanager.NewMultiManager(
multimanager.ManagerEntry{Name: nfc.ManagerTypeHardware, Manager: nfc.NewManager()},
multimanager.ManagerEntry{Name: "myreader", Manager: myreader.NewManager()},
)
// Use the manager...
}
Implement DeviceChangeNotifier to notify the system when devices are added or removed:
type MyManager struct {
devices map[string]*MyDevice
devicesChan chan struct{}
mu sync.RWMutex
}
// DeviceChanges returns a channel that signals when devices change.
// Implements nfc.DeviceChangeNotifier.
func (m *MyManager) DeviceChanges() <-chan struct{} {
return m.devicesChan
}
// Call this when a device is added or removed
func (m *MyManager) notifyDeviceChange() {
select {
case m.devicesChan <- struct{}{}:
default:
// Channel full, skip notification
}
}
The MultiManager automatically listens to managers that implement DeviceChangeNotifier and forwards change notifications.
You don’t need to implement all methods if your device doesn’t support them.
Embed nfc.BaseTag so unsupported operations already return the right
“not supported” error, then advertise what is supported via Capabilities():
type MyTag struct {
nfc.BaseTag // WriteData/Transceive/MakeReadOnly default to "not supported"
// ...
}
func (t *MyTag) Capabilities() nfc.TagCapabilities {
return nfc.TagCapabilities{
CanRead: true,
CanWrite: false, // Read-only device — no need to override WriteData
CanTransceive: false,
CanLock: false,
}
}
Because the defaults come from BaseTag, a read-only tag literally just needs
UID, Type, NumericType, and ReadData — there is no WriteData boilerplate
to write.
Capabilities() and actual method behavior are two separate sources of truth, so
they can drift. Drop nfc.AssertCapabilitiesConsistent into your tests to catch
the common cases (it performs only non-mutating checks and never writes or locks
the tag):
func TestMyTagCapabilities(t *testing.T) {
tag := &MyTag{ /* ... */ }
if err := nfc.AssertCapabilitiesConsistent(tag); err != nil {
t.Fatal(err)
}
}
Callers can check capabilities before calling methods:
caps := nfc.GetTagCapabilities(tag)
if caps.CanWrite {
tag.WriteData(data)
} else {
log.Println("Tag does not support writing")
}
Use these convenience functions for common capability checks:
// Check tag capabilities
if nfc.CanTagRead(tag) {
data, _ := tag.ReadData()
}
if nfc.CanTagWrite(tag) {
tag.WriteData(data)
}
if nfc.CanTagTransceive(tag) {
resp, _ := tag.Transceive(apdu)
}
if nfc.CanTagLock(tag) {
tag.MakeReadOnly()
}
Use GetDeviceCapabilities() to inspect device capabilities:
caps := nfc.GetDeviceCapabilities(device)
// DeviceCapabilities struct:
// - CanTransceive: bool // Device supports raw transceive
// - CanPoll: bool // Device supports polling for tags
// - SupportedTagTypes: []string // e.g., ["MIFARE Classic", "NTAG"]
// - DeviceType: string // e.g., "libnfc", "smartphone"
// - MaxBaudRate: int // Max baud rate in bps
// - SupportsEvents: bool // Tag arrival/removal events
if caps.SupportsEvents {
// Event-driven device (e.g., smartphone)
} else if caps.CanPoll {
// Polling device (e.g., hardware reader)
}
Capabilities are automatically built from optional interfaces the device implements (DeviceInfoProvider, DeviceEventEmitter).
Use the structured error types for consistent error handling:
import "github.com/dotside-studios/davi-nfc-agent/nfc"
// For unsupported operations
return nfc.NewNotSupportedError("Transceive")
// For authentication failures
return nfc.NewAuthError("ReadData", tag.UID(), err)
// For read/write failures
return nfc.NewReadError("ReadData", err)
return nfc.NewWriteError("WriteData", err)
// For generic errors with context
return nfc.WrapError(nfc.ErrCodeReadFailed, "ReadSector", "failed to read sector 1", err)
Callers can handle errors programmatically:
if nfc.IsNotSupportedError(err) {
// Operation not supported, try alternative
}
if nfc.IsAuthError(err) {
// Authentication failed, maybe try different key
}
code := nfc.GetErrorCode(err)
switch code {
case nfc.ErrCodeTagRemoved:
// Tag was removed, retry
case nfc.ErrCodeReadFailed:
// Read failed, handle error
}
For tags that need special write handling, implement the AdvancedWriter interface:
// TagWriteOptions controls write behavior
type TagWriteOptions struct {
// ForceInitialize wipes and reinitializes the tag before writing.
// WARNING: This erases all existing data.
ForceInitialize bool
}
// AdvancedWriter is an optional interface for tags supporting write options.
// Implement WriteDataWithOptions to handle special write cases.
func (t *MyTag) WriteDataWithOptions(data []byte, opts nfc.TagWriteOptions) error {
if opts.ForceInitialize {
// Wipe and reinitialize the tag
if err := t.format(); err != nil {
return err
}
}
return t.WriteData(data)
}
The NFCReader automatically uses WriteDataWithOptions when available:
// Writing with force initialization
opts := nfc.TagWriteOptions{ForceInitialize: true}
if writer, ok := tag.(nfc.AdvancedWriter); ok {
err := writer.WriteDataWithOptions(data, opts)
} else {
// Fallback to standard write
err := tag.WriteData(data)
}
If your device needs WebSocket handlers (like smartphone NFC):
import "github.com/dotside-studios/davi-nfc-agent/server"
// Implement server.ServerHandler
func (m *MyManager) Register(s server.HandlerServer) {
s.HandleMessage("myreader:scan", m.handleScan)
}
// Implement server.ServerHandlerCloser for cleanup
func (m *MyManager) Close() {
// Cleanup resources
}
Create mock implementations for testing:
func TestMyDevice(t *testing.T) {
device := &MyDevice{connection: "test"}
// Test capabilities
caps := device.Capabilities()
if !caps.CanPoll {
t.Error("Expected CanPoll to be true")
}
// Test GetTags
tags, err := device.GetTags()
if err != nil {
t.Errorf("GetTags failed: %v", err)
}
// Test tag capabilities
for _, tag := range tags {
tagCaps := nfc.GetTagCapabilities(tag)
if !tagCaps.CanRead {
t.Error("Expected tag to support reading")
}
}
}
A device that receives tag data over the network (read-only):
type NetworkTag struct {
nfc.BaseTag // write/transceive/lock default to "not supported"
uid string
tagType string
data []byte // Pre-loaded data
}
func (t *NetworkTag) UID() string { return t.uid }
func (t *NetworkTag) Type() string { return t.tagType }
func (t *NetworkTag) NumericType() int { return 0 }
func (t *NetworkTag) Capabilities() nfc.TagCapabilities {
return nfc.TagCapabilities{
CanRead: true,
CanWrite: false,
CanTransceive: false,
CanLock: false,
TagFamily: t.tagType,
}
}
func (t *NetworkTag) ReadData() ([]byte, error) {
return t.data, nil
}
// No WriteData needed — inherited from nfc.BaseTag as "not supported".
A device connected via serial port:
type PN532Device struct {
port io.ReadWriteCloser
conn string
}
func (d *PN532Device) DeviceType() string {
return "pn532-serial"
}
func (d *PN532Device) SupportedTagTypes() []string {
return []string{"MIFARE Classic", "NTAG", "ISO14443-4"}
}
func (d *PN532Device) GetTags() ([]nfc.Tag, error) {
// Send InListPassiveTarget command
cmd := []byte{0xD4, 0x4A, 0x01, 0x00}
resp, err := d.sendCommand(cmd)
if err != nil {
return nil, err
}
// Parse response and create tags
// ...
}
These four have no sensible default and must be implemented on every tag:
| Method | Interface |
|---|---|
UID() |
TagIdentifier |
Type() |
TagIdentifier |
NumericType() |
TagIdentifier |
ReadData() |
TagReader |
nfc.BaseTagEmbedding nfc.BaseTag supplies these with safe defaults. Override only the ones
your tag supports:
| Method | Interface | BaseTag default |
|---|---|---|
Connect() |
TagConnection | no-op (returns nil) |
Disconnect() |
TagConnection | no-op (returns nil) |
WriteData() |
TagWriter | returns NotSupported |
Transceive() |
TagTransceiver | returns NotSupported |
IsWritable() |
TagLocker | returns false |
CanMakeReadOnly() |
TagLocker | returns false |
MakeReadOnly() |
TagLocker | returns NotSupported |
| Method | Interface | Purpose |
|---|---|---|
Capabilities() |
TagCapabilityProvider | Runtime tag capability discovery |
WriteDataWithOptions() |
AdvancedWriter | Write with initialization options |
DeviceChanges() |
DeviceChangeNotifier | Device add/remove notifications |
DeviceType() |
DeviceInfoProvider | Device type identifier (“libnfc”, “smartphone”) |
SupportedTagTypes() |
DeviceInfoProvider | List of supported tag types |
SupportsEvents() |
DeviceEventEmitter | Whether device emits tag events |
SupportsTransceive() |
DeviceTransceiver | Whether device supports raw transceive |
IsHealthy() |
DeviceHealthChecker | Connection health validation |
Register() |
server.ServerHandler | WebSocket integration |
Close() |
server.ServerHandlerCloser | Cleanup on shutdown |
Implement this interface to provide device metadata. Capabilities are built automatically from this:
func (d *MyDevice) DeviceType() string {
return "myreader"
}
func (d *MyDevice) SupportedTagTypes() []string {
return []string{"MIFARE Classic", "NTAG"}
}
For event-based devices (like smartphones) that receive tags via events rather than polling:
func (d *MyDevice) SupportsEvents() bool {
return true // Tags arrive as events, not via polling
}
When SupportsEvents() returns true, BuildDeviceCapabilities() will automatically set:
CanPoll: falseCanTransceive: falseSupportsEvents: truePolling devices default to CanTransceive: true. If your polling device’s
Transceive actually returns a NotSupported error, implement
DeviceTransceiver so the reported capabilities match reality:
func (d *MyDevice) SupportsTransceive() bool {
return false // Device cannot do raw transceive
}
When present, SupportsTransceive() is authoritative for CanTransceive in the
capabilities built by BuildDeviceCapabilities().
For devices that support connection health checking:
func (d *MyDevice) IsHealthy() error {
if !d.isConnected {
return fmt.Errorf("device not connected")
}
return nil
}
The DeviceManager uses this interface to check device health before operations.