Newer
Older
import 'package:nfc_manager/nfc_manager.dart';
import 'dart:typed_data';
Papa THIAM
a validé
import 'inscription_page.dart';
import 'sheet_view_page.dart';
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: SimpleNfcReader(),
));
class SimpleNfcReader extends StatefulWidget {
const SimpleNfcReader({super.key});
@override
State<SimpleNfcReader> createState() => _SimpleNfcReaderState();
}
class _SimpleNfcReaderState extends State<SimpleNfcReader> {
bool _isReading = false;
String _status = 'Appuyez sur le bouton pour lire un tag NFC';
Map<String, String> _info = {};
Future<void> _startNfc() async {
bool available = await NfcManager.instance.isAvailable();
if (!available) {
setState(() => _status = '❌ NFC non disponible sur cet appareil');
return;
}
setState(() {
_isReading = true;
_status = '🔄 Approchez un tag NFC...';
_info.clear();
});
NfcManager.instance.startSession(
pollingOptions: {
NfcPollingOption.iso14443,
NfcPollingOption.iso15693,
NfcPollingOption.iso18092,
},
onDiscovered: (NfcTag tag) async {
try {
// Tentative spécifique pour bagues/pigeons (ISO15693 / ISO14443A)
final pigeonResults = await _tryReadPigeonTag(tag);
final details = _parseTag(tag);
setState(() {
// Merge results: pigeonResults can overwrite or extend details
_info = {...details, ...pigeonResults};
_status = '✅ Tag détecté !';
});
} catch (e) {
setState(() {
_status = 'Erreur: ${e.toString()}';
});
}
await NfcManager.instance.stopSession();
setState(() => _isReading = false);
},
);
}
/// Essaye de lire des informations utiles pour une "bague pigeon".
/// Retourne une Map<String,String> avec des champs additionnels si trouvés.
Future<Map<String, String>> _tryReadPigeonTag(NfcTag tag) async {
final Map<String, String> out = {};
dynamic data;
try {
data = (tag as dynamic).data;
} catch (_) {
data = null;
}
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// UID first
try {
final idv = _safeLookup(data, 'id');
if (idv != null) out['UID brute'] = _bytesToUid(idv);
} catch (_) {}
// Detect techs
String techsStr = '';
try {
if (data is Map) techsStr = (_safeLookup(data, 'techList') ?? _safeLookup(data, 'techs') ?? '').toString();
if (techsStr.isEmpty) {
try {
final t = (tag as dynamic).techs ?? (tag as dynamic).tech;
if (t != null) techsStr = t.toString();
} catch (_) {}
}
} catch (_) {}
final techsLower = techsStr.toLowerCase();
// ISO15693 -> try Read Single Block (flag=0x02: address flag 0x00 / 0x20 depends)
if (techsLower.contains('nfcv') || techsLower.contains('iso15693')) {
try {
// Build a Read Single Block command for ISO15693 (example: flags=0x02, cmd=0x20, block=0)
// Frame: [flags, cmd, ...UID (if needed), blockNumber]
// Many platforms require UID (8 bytes) after cmd when addressing is used; to keep it simple we try without UID first.
final cmd = <int>[0x02, 0x20, 0x00]; // flags=0x02, ReadSingleBlock=0x20, block=0
// Try to call transceive/sendCommand dynamically
Uint8List? resp;
try {
resp = await (tag as dynamic).transceive(Uint8List.fromList(cmd));
} catch (_) {
try {
resp = await (tag as dynamic).sendCommand(Uint8List.fromList(cmd));
} catch (_) {
resp = null;
}
}
if (resp != null && resp.isNotEmpty) {
out['ISO15693 block0'] = _bytesToHexString(resp);
}
} catch (_) {}
}
// ISO14443A / NTAG -> try read pages (MIFARE Ultralight/NTAG: READ 0x30 page)
if (techsLower.contains('nfca') || techsLower.contains('iso14443-3a') || techsLower.contains('iso14443')) {
try {
// Read pages 4..7 (application/user pages) using READ command 0x30
final results = <String>[];
for (int page = 4; page <= 7; page++) {
final cmd = <int>[0x30, page];
Uint8List? resp;
try {
resp = await (tag as dynamic).transceive(Uint8List.fromList(cmd));
} catch (_) {
try {
resp = await (tag as dynamic).sendCommand(Uint8List.fromList(cmd));
} catch (_) {
resp = null;
}
}
if (resp != null && resp.isNotEmpty) {
results.add('p$page=${_bytesToHexString(resp)}');
}
}
if (results.isNotEmpty) out['NTAG pages 4-7'] = results.join(', ');
} catch (_) {}
}
return out;
Map<String, String> _parseTag(NfcTag tag) {
final Map<String, String> result = {};
dynamic data;
try {
data = (tag as dynamic).data;
} catch (_) {
data = null;
}
// Type
try {
final t = _safeLookup(data, 'type');
result['Type de tag'] = t?.toString() ?? 'Inconnu';
} catch (_) {
result['Type de tag'] = 'Inconnu';
}
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// Techs
try {
final techs = _safeLookup(data, 'techList') ?? _safeLookup(data, 'techs') ?? '';
result['Technologies disponibles'] = techs?.toString() ?? '';
} catch (_) {
result['Technologies disponibles'] = '';
}
// UID
try {
final id = _safeLookup(data, 'id');
if (id != null) result['Numéro de série'] = _bytesToUid(id);
} catch (_) {}
// ATQA/SAK/ATS/size
try {
result['ATQA'] = _asHex(_safeLookup(data, 'atqa'));
result['SAK'] = _asHex(_safeLookup(data, 'sak'));
result['ATS'] = _asHex(_safeLookup(data, 'ats'));
final s = _safeLookup(data, 'size') ?? '---';
result['Informations mémoire'] = '${s} bytes';
} catch (_) {
result['ATQA'] = '---';
result['SAK'] = '---';
result['ATS'] = '---';
result['Informations mémoire'] = '---';
}
return result;
}
String _bytesToUid(dynamic raw) {
try {
final bytes = raw is Uint8List ? raw : Uint8List.fromList(List<int>.from(raw));
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(':');
} catch (_) {
return raw.toString();
}
}
String _asHex(dynamic v) {
if (v == null) return '---';
if (v is int) return '0x${v.toRadixString(16).toUpperCase()}';
if (v is Uint8List) {
return '0x${v.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase()}';
}
return v.toString();
}
String _bytesToHexString(dynamic maybeBytes) {
try {
Uint8List bytes;
if (maybeBytes is Uint8List) bytes = maybeBytes;
else if (maybeBytes is List<int>) bytes = Uint8List.fromList(maybeBytes);
else return maybeBytes.toString();
final sb = StringBuffer();
for (final b in bytes) sb.write(b.toRadixString(16).padLeft(2, '0'));
return sb.toString().toUpperCase();
} catch (_) {
return maybeBytes.toString();
}
}
/// Safe lookup: try Map access, bracket dynamic (caught), or common property getters.
dynamic _safeLookup(dynamic obj, String key) {
if (obj == null) return null;
try {
if (obj is Map) {
if (obj.containsKey(key)) return obj[key];
// also try case variations
final lower = key.toLowerCase();
if (obj.containsKey(lower)) return obj[lower];
final upper = key.toUpperCase();
if (obj.containsKey(upper)) return obj[upper];
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
} catch (_) {}
// try bracket access dynamically (may throw NoSuchMethodError)
try {
return (obj as dynamic)[key];
} catch (_) {}
// try common property getters by name
try {
if (key == 'type') return (obj as dynamic).type;
} catch (_) {}
try {
if (key == 'techs') return (obj as dynamic).techs;
} catch (_) {}
try {
if (key == 'techList') return (obj as dynamic).techList;
} catch (_) {}
try {
if (key == 'id') return (obj as dynamic).id;
} catch (_) {}
try {
if (key == 'atqa') return (obj as dynamic).atqa;
} catch (_) {}
try {
if (key == 'sak') return (obj as dynamic).sak;
} catch (_) {}
try {
if (key == 'ats') return (obj as dynamic).ats;
} catch (_) {}
try {
if (key == 'size') return (obj as dynamic).size;
} catch (_) {}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
Papa THIAM
a validé
appBar: AppBar(
title: const Text('📡 Lecteur NFC simple'),
actions: [
IconButton(
icon: const Icon(Icons.person_add),
tooltip: 'Inscription',
onPressed: () async {
final prefMac = _info['Numéro de série'] ?? _info['UID brute'];
await Navigator.of(context).push(MaterialPageRoute(builder: (_) => InscriptionPage(prefilledMac: prefMac)));
},
),
IconButton(
icon: const Icon(Icons.table_chart),
tooltip: 'Voir sheet',
onPressed: () async {
await Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SheetViewPage()));
},
)
],
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_status, style: const TextStyle(fontSize: 16)),
const SizedBox(height: 20),
Expanded(
child: ListView(
children: _info.entries.map((e) {
return ListTile(
title: Text(e.key, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(e.value),
);
}).toList(),
),
const SizedBox(height: 10),
Center(
child: ElevatedButton.icon(
onPressed: _isReading ? null : _startNfc,
icon: const Icon(Icons.nfc),
label: Text(_isReading ? 'Lecture en cours...' : 'Lire un tag NFC'),
),
),
],