host.nfc_tool.write_tab

Write tab — battery data write workflow with UID verification.

  1"""Write tab — battery data write workflow with UID verification."""
  2
  3import datetime
  4import time
  5import tkinter as tk
  6from tkinter import ttk, messagebox
  7
  8from host.nfc_tool.battery_data import BatteryData
  9
 10
 11# Write workflow states
 12IDLE = 'idle'
 13SEARCHING = 'searching'
 14TAG_FOUND = 'tag_found'
 15VERIFYING = 'verifying'
 16WRITING = 'writing'
 17DONE = 'done'
 18ERROR = 'error'
 19
 20# Verify retry settings
 21VERIFY_RETRY_INTERVAL_MS = 1000  # 1 second between retries
 22VERIFY_TIMEOUT_S = 10  # give up after 10 seconds
 23
 24
 25class WriteTab(ttk.Frame):
 26    """Multi-step write workflow with tag verification safety."""
 27
 28    def __init__(self, parent, request_poll=None, request_write=None):
 29        super().__init__(parent)
 30        self._request_poll = request_poll
 31        self._request_write = request_write
 32        self._state = IDLE
 33        self._found_uid = None  # UID captured when tag first detected
 34        self._found_tag_data = None  # Full tag data from detection
 35        self._poll_id = None
 36        self._verify_start_time = 0
 37
 38        self._build_ui()
 39
 40    def _build_ui(self):
 41        # Form section
 42        form_frame = ttk.LabelFrame(self, text="Battery Information")
 43        form_frame.pack(fill=tk.X, padx=10, pady=(10, 5))
 44
 45        ttk.Label(form_frame, text="Serial Number:").grid(
 46            row=0, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 47        self._sn_var = tk.StringVar()
 48        self._sn_entry = ttk.Entry(form_frame, textvariable=self._sn_var,
 49                                   width=30)
 50        self._sn_entry.grid(row=0, column=1, sticky=tk.W, pady=4)
 51
 52        ttk.Label(form_frame, text="Year:").grid(
 53            row=1, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 54        current_year = datetime.datetime.now().year
 55        years = [str(y) for y in
 56                 range(current_year + 1, current_year - 6, -1)]
 57        self._year_var = tk.StringVar(value=str(current_year))
 58        self._year_combo = ttk.Combobox(
 59            form_frame, textvariable=self._year_var,
 60            values=years, width=8,
 61        )
 62        self._year_combo.grid(row=1, column=1, sticky=tk.W, pady=4)
 63
 64        ttk.Label(form_frame, text="Note:").grid(
 65            row=2, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 66        self._note_var = tk.StringVar()
 67        self._note_entry = ttk.Entry(
 68            form_frame, textvariable=self._note_var, width=40)
 69        self._note_entry.grid(row=2, column=1, sticky=tk.W, pady=4)
 70
 71        ttk.Label(form_frame, text="URI:").grid(
 72            row=3, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 73        self._uri_var = tk.StringVar(value="http://www.raptacon.org")
 74        self._uri_entry = ttk.Entry(
 75            form_frame, textvariable=self._uri_var, width=40)
 76        self._uri_entry.grid(row=3, column=1, sticky=tk.W, pady=4)
 77
 78        # Buttons
 79        btn_frame = ttk.Frame(self)
 80        btn_frame.pack(fill=tk.X, padx=10, pady=5)
 81
 82        self._start_btn = ttk.Button(
 83            btn_frame, text="Start Write",
 84            command=self._on_start_write,
 85        )
 86        self._start_btn.pack(side=tk.LEFT)
 87
 88        self._cancel_btn = ttk.Button(
 89            btn_frame, text="Cancel",
 90            command=self._on_cancel,
 91            state='disabled',
 92        )
 93        self._cancel_btn.pack(side=tk.LEFT, padx=(8, 0))
 94
 95        # Status / log area
 96        log_frame = ttk.LabelFrame(self, text="Write Progress")
 97        log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10))
 98
 99        self._log_text = tk.Text(log_frame, height=10, wrap=tk.WORD,
100                                 state='disabled', font=('Consolas', 9))
101        scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
102                                  command=self._log_text.yview)
103        self._log_text.configure(yscrollcommand=scrollbar.set)
104        self._log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True,
105                            padx=(4, 0), pady=4)
106        scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 4), pady=4)
107
108    def _log(self, msg):
109        self._log_text.configure(state='normal')
110        self._log_text.insert(tk.END, msg + '\n')
111        self._log_text.see(tk.END)
112        self._log_text.configure(state='disabled')
113
114    def _clear_log(self):
115        self._log_text.configure(state='normal')
116        self._log_text.delete('1.0', tk.END)
117        self._log_text.configure(state='disabled')
118
119    def _set_state(self, state):
120        self._state = state
121        form_enabled = state in (IDLE, DONE, ERROR)
122        entry_state = 'normal' if form_enabled else 'disabled'
123        self._sn_entry.configure(state=entry_state)
124        self._year_combo.configure(
125            state='normal' if form_enabled else 'disabled')
126        self._note_entry.configure(state=entry_state)
127        self._uri_entry.configure(state=entry_state)
128
129        self._start_btn.configure(
130            state='normal' if form_enabled else 'disabled')
131        self._cancel_btn.configure(
132            state='normal' if state == SEARCHING else 'disabled')
133
134    # --- User actions ---
135
136    def _on_start_write(self):
137        sn = self._sn_var.get().strip()
138        if not sn:
139            messagebox.showwarning(
140                "Missing Data", "Serial number is required.",
141                parent=self)
142            return
143
144        self._clear_log()
145        self._found_uid = None
146        self._found_tag_data = None
147        self._set_state(SEARCHING)
148        self._log("Searching for tag... Place tag on reader.")
149        self._start_search_poll()
150
151    def _on_cancel(self):
152        self._stop_poll()
153        self._set_state(IDLE)
154        self._log("Cancelled.")
155
156    # --- Polling helpers ---
157
158    def _start_search_poll(self):
159        self._stop_poll()
160        self._do_search_poll()
161
162    def _stop_poll(self):
163        if self._poll_id is not None:
164            self.after_cancel(self._poll_id)
165            self._poll_id = None
166
167    def _do_search_poll(self):
168        if self._state != SEARCHING:
169            return
170        if self._request_poll:
171            self._request_poll('write_search')
172        self._poll_id = self.after(300, self._do_search_poll)
173
174    def _do_verify_poll(self):
175        """Poll for tag during verify phase, with timeout."""
176        if self._state != VERIFYING:
177            return
178        elapsed = time.monotonic() - self._verify_start_time
179        if elapsed >= VERIFY_TIMEOUT_S:
180            # Timed out
181            self._set_state(ERROR)
182            messagebox.showerror(
183                "Write Failed",
184                "Tag not detected after 10 seconds.\n\n"
185                "Make sure the tag is placed on the reader "
186                "and try again.",
187                parent=self)
188            self._log("Write failed: tag not detected (timed out).")
189            return
190        if self._request_poll:
191            self._request_poll('write_verify')
192        self._poll_id = self.after(VERIFY_RETRY_INTERVAL_MS,
193                                   self._do_verify_poll)
194
195    # --- Tab lifecycle ---
196
197    def on_tab_selected(self):
198        pass
199
200    def on_tab_deselected(self):
201        if self._state in (SEARCHING, VERIFYING):
202            self._on_cancel()
203
204    # --- Results from worker ---
205
206    def on_search_result(self, tag_data):
207        """Called when searching for a tag during write workflow."""
208        if self._state != SEARCHING:
209            return
210
211        if tag_data is None:
212            return  # Keep searching
213
214        self._stop_poll()
215        self._found_uid = tag_data.uid_hex
216        self._found_tag_data = tag_data
217        self._set_state(TAG_FOUND)
218
219        # Build the confirmation message
220        self._log(f"Tag found! UID: {tag_data.uid_hex}")
221
222        existing_info = ""
223        if len(tag_data.user_data) > 0:
224            text = tag_data.get_text()
225            uri = tag_data.get_uri()
226            if text:
227                self._log(f"Existing text: {text}")
228                existing_info += f"Existing text: {text}\n"
229            if uri:
230                self._log(f"Existing URI: {uri}")
231                existing_info += f"Existing URI: {uri}\n"
232            if not text and not uri:
233                self._log("Tag is blank (no NDEF data).")
234                existing_info = "Tag is currently blank.\n"
235        else:
236            self._log("Could not read tag data (UID only).")
237            existing_info = "Could not read existing data.\n"
238
239        # Build new data preview
240        bat = BatteryData(
241            sn=self._sn_var.get().strip(),
242            year=self._year_var.get().strip(),
243            note=self._note_var.get().strip(),
244        )
245        new_uri = self._uri_var.get().strip()
246
247        # Show confirmation dialog
248        msg = (
249            f"Tag detected: UID {tag_data.uid_hex}\n\n"
250            f"{existing_info}\n"
251            f"--- Write the following? ---\n"
252            f"URI: {new_uri}\n"
253            f"{bat.to_text()}\n\n"
254            f"Proceed with write?"
255        )
256
257        proceed = messagebox.askyesno(
258            "Confirm Write", msg, parent=self)
259
260        if proceed:
261            self._log("")
262            self._log("Write confirmed. Verifying tag...")
263            self._set_state(VERIFYING)
264            self._verify_start_time = time.monotonic()
265            self._do_verify_poll()
266        else:
267            self._set_state(IDLE)
268            self._log("Write cancelled by user.")
269
270    def on_verify_result(self, tag_data):
271        """Called during write verification — check UID matches."""
272        if self._state != VERIFYING:
273            return
274
275        if tag_data is None:
276            # Tag not found yet — retry will happen via _do_verify_poll
277            elapsed = time.monotonic() - self._verify_start_time
278            remaining = max(0, VERIFY_TIMEOUT_S - elapsed)
279            self._log(f"Tag not detected, retrying... "
280                      f"({remaining:.0f}s remaining)")
281            return
282
283        self._stop_poll()
284
285        current_uid = tag_data.uid_hex
286        if current_uid != self._found_uid:
287            self._set_state(ERROR)
288            messagebox.showerror(
289                "Write Failed",
290                f"Tag changed during write!\n\n"
291                f"Expected UID: {self._found_uid}\n"
292                f"Detected UID: {current_uid}\n\n"
293                f"Place the original tag back and try again.",
294                parent=self)
295            self._log(
296                f"Write failed: tag changed "
297                f"(expected {self._found_uid}, "
298                f"got {current_uid})")
299            return
300
301        # UID matches — proceed with write
302        self._set_state(WRITING)
303        self._log("Tag verified. Writing...")
304        if self._request_write:
305            self._request_write(
306                sn=self._sn_var.get().strip(),
307                year=self._year_var.get().strip(),
308                note=self._note_var.get().strip(),
309                uri=self._uri_var.get().strip(),
310            )
311
312    def on_write_result(self, success, error_msg=''):
313        """Called when write operation completes."""
314        if success:
315            self._set_state(DONE)
316            self._log("Write successful!")
317            self._log("Tag has been programmed. You may remove it.")
318            messagebox.showinfo(
319                "Write Complete",
320                "Tag has been successfully programmed!",
321                parent=self)
322        else:
323            self._set_state(ERROR)
324            self._log(f"Write failed: {error_msg}")
325            messagebox.showerror(
326                "Write Failed",
327                f"Tag write failed:\n\n{error_msg}\n\n"
328                f"Click 'Start Write' to try again.",
329                parent=self)
IDLE = 'idle'
SEARCHING = 'searching'
TAG_FOUND = 'tag_found'
VERIFYING = 'verifying'
WRITING = 'writing'
DONE = 'done'
ERROR = 'error'
VERIFY_RETRY_INTERVAL_MS = 1000
VERIFY_TIMEOUT_S = 10
class WriteTab(tkinter.ttk.Frame):
 26class WriteTab(ttk.Frame):
 27    """Multi-step write workflow with tag verification safety."""
 28
 29    def __init__(self, parent, request_poll=None, request_write=None):
 30        super().__init__(parent)
 31        self._request_poll = request_poll
 32        self._request_write = request_write
 33        self._state = IDLE
 34        self._found_uid = None  # UID captured when tag first detected
 35        self._found_tag_data = None  # Full tag data from detection
 36        self._poll_id = None
 37        self._verify_start_time = 0
 38
 39        self._build_ui()
 40
 41    def _build_ui(self):
 42        # Form section
 43        form_frame = ttk.LabelFrame(self, text="Battery Information")
 44        form_frame.pack(fill=tk.X, padx=10, pady=(10, 5))
 45
 46        ttk.Label(form_frame, text="Serial Number:").grid(
 47            row=0, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 48        self._sn_var = tk.StringVar()
 49        self._sn_entry = ttk.Entry(form_frame, textvariable=self._sn_var,
 50                                   width=30)
 51        self._sn_entry.grid(row=0, column=1, sticky=tk.W, pady=4)
 52
 53        ttk.Label(form_frame, text="Year:").grid(
 54            row=1, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 55        current_year = datetime.datetime.now().year
 56        years = [str(y) for y in
 57                 range(current_year + 1, current_year - 6, -1)]
 58        self._year_var = tk.StringVar(value=str(current_year))
 59        self._year_combo = ttk.Combobox(
 60            form_frame, textvariable=self._year_var,
 61            values=years, width=8,
 62        )
 63        self._year_combo.grid(row=1, column=1, sticky=tk.W, pady=4)
 64
 65        ttk.Label(form_frame, text="Note:").grid(
 66            row=2, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 67        self._note_var = tk.StringVar()
 68        self._note_entry = ttk.Entry(
 69            form_frame, textvariable=self._note_var, width=40)
 70        self._note_entry.grid(row=2, column=1, sticky=tk.W, pady=4)
 71
 72        ttk.Label(form_frame, text="URI:").grid(
 73            row=3, column=0, sticky=tk.W, padx=(8, 4), pady=4)
 74        self._uri_var = tk.StringVar(value="http://www.raptacon.org")
 75        self._uri_entry = ttk.Entry(
 76            form_frame, textvariable=self._uri_var, width=40)
 77        self._uri_entry.grid(row=3, column=1, sticky=tk.W, pady=4)
 78
 79        # Buttons
 80        btn_frame = ttk.Frame(self)
 81        btn_frame.pack(fill=tk.X, padx=10, pady=5)
 82
 83        self._start_btn = ttk.Button(
 84            btn_frame, text="Start Write",
 85            command=self._on_start_write,
 86        )
 87        self._start_btn.pack(side=tk.LEFT)
 88
 89        self._cancel_btn = ttk.Button(
 90            btn_frame, text="Cancel",
 91            command=self._on_cancel,
 92            state='disabled',
 93        )
 94        self._cancel_btn.pack(side=tk.LEFT, padx=(8, 0))
 95
 96        # Status / log area
 97        log_frame = ttk.LabelFrame(self, text="Write Progress")
 98        log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10))
 99
100        self._log_text = tk.Text(log_frame, height=10, wrap=tk.WORD,
101                                 state='disabled', font=('Consolas', 9))
102        scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL,
103                                  command=self._log_text.yview)
104        self._log_text.configure(yscrollcommand=scrollbar.set)
105        self._log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True,
106                            padx=(4, 0), pady=4)
107        scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 4), pady=4)
108
109    def _log(self, msg):
110        self._log_text.configure(state='normal')
111        self._log_text.insert(tk.END, msg + '\n')
112        self._log_text.see(tk.END)
113        self._log_text.configure(state='disabled')
114
115    def _clear_log(self):
116        self._log_text.configure(state='normal')
117        self._log_text.delete('1.0', tk.END)
118        self._log_text.configure(state='disabled')
119
120    def _set_state(self, state):
121        self._state = state
122        form_enabled = state in (IDLE, DONE, ERROR)
123        entry_state = 'normal' if form_enabled else 'disabled'
124        self._sn_entry.configure(state=entry_state)
125        self._year_combo.configure(
126            state='normal' if form_enabled else 'disabled')
127        self._note_entry.configure(state=entry_state)
128        self._uri_entry.configure(state=entry_state)
129
130        self._start_btn.configure(
131            state='normal' if form_enabled else 'disabled')
132        self._cancel_btn.configure(
133            state='normal' if state == SEARCHING else 'disabled')
134
135    # --- User actions ---
136
137    def _on_start_write(self):
138        sn = self._sn_var.get().strip()
139        if not sn:
140            messagebox.showwarning(
141                "Missing Data", "Serial number is required.",
142                parent=self)
143            return
144
145        self._clear_log()
146        self._found_uid = None
147        self._found_tag_data = None
148        self._set_state(SEARCHING)
149        self._log("Searching for tag... Place tag on reader.")
150        self._start_search_poll()
151
152    def _on_cancel(self):
153        self._stop_poll()
154        self._set_state(IDLE)
155        self._log("Cancelled.")
156
157    # --- Polling helpers ---
158
159    def _start_search_poll(self):
160        self._stop_poll()
161        self._do_search_poll()
162
163    def _stop_poll(self):
164        if self._poll_id is not None:
165            self.after_cancel(self._poll_id)
166            self._poll_id = None
167
168    def _do_search_poll(self):
169        if self._state != SEARCHING:
170            return
171        if self._request_poll:
172            self._request_poll('write_search')
173        self._poll_id = self.after(300, self._do_search_poll)
174
175    def _do_verify_poll(self):
176        """Poll for tag during verify phase, with timeout."""
177        if self._state != VERIFYING:
178            return
179        elapsed = time.monotonic() - self._verify_start_time
180        if elapsed >= VERIFY_TIMEOUT_S:
181            # Timed out
182            self._set_state(ERROR)
183            messagebox.showerror(
184                "Write Failed",
185                "Tag not detected after 10 seconds.\n\n"
186                "Make sure the tag is placed on the reader "
187                "and try again.",
188                parent=self)
189            self._log("Write failed: tag not detected (timed out).")
190            return
191        if self._request_poll:
192            self._request_poll('write_verify')
193        self._poll_id = self.after(VERIFY_RETRY_INTERVAL_MS,
194                                   self._do_verify_poll)
195
196    # --- Tab lifecycle ---
197
198    def on_tab_selected(self):
199        pass
200
201    def on_tab_deselected(self):
202        if self._state in (SEARCHING, VERIFYING):
203            self._on_cancel()
204
205    # --- Results from worker ---
206
207    def on_search_result(self, tag_data):
208        """Called when searching for a tag during write workflow."""
209        if self._state != SEARCHING:
210            return
211
212        if tag_data is None:
213            return  # Keep searching
214
215        self._stop_poll()
216        self._found_uid = tag_data.uid_hex
217        self._found_tag_data = tag_data
218        self._set_state(TAG_FOUND)
219
220        # Build the confirmation message
221        self._log(f"Tag found! UID: {tag_data.uid_hex}")
222
223        existing_info = ""
224        if len(tag_data.user_data) > 0:
225            text = tag_data.get_text()
226            uri = tag_data.get_uri()
227            if text:
228                self._log(f"Existing text: {text}")
229                existing_info += f"Existing text: {text}\n"
230            if uri:
231                self._log(f"Existing URI: {uri}")
232                existing_info += f"Existing URI: {uri}\n"
233            if not text and not uri:
234                self._log("Tag is blank (no NDEF data).")
235                existing_info = "Tag is currently blank.\n"
236        else:
237            self._log("Could not read tag data (UID only).")
238            existing_info = "Could not read existing data.\n"
239
240        # Build new data preview
241        bat = BatteryData(
242            sn=self._sn_var.get().strip(),
243            year=self._year_var.get().strip(),
244            note=self._note_var.get().strip(),
245        )
246        new_uri = self._uri_var.get().strip()
247
248        # Show confirmation dialog
249        msg = (
250            f"Tag detected: UID {tag_data.uid_hex}\n\n"
251            f"{existing_info}\n"
252            f"--- Write the following? ---\n"
253            f"URI: {new_uri}\n"
254            f"{bat.to_text()}\n\n"
255            f"Proceed with write?"
256        )
257
258        proceed = messagebox.askyesno(
259            "Confirm Write", msg, parent=self)
260
261        if proceed:
262            self._log("")
263            self._log("Write confirmed. Verifying tag...")
264            self._set_state(VERIFYING)
265            self._verify_start_time = time.monotonic()
266            self._do_verify_poll()
267        else:
268            self._set_state(IDLE)
269            self._log("Write cancelled by user.")
270
271    def on_verify_result(self, tag_data):
272        """Called during write verification — check UID matches."""
273        if self._state != VERIFYING:
274            return
275
276        if tag_data is None:
277            # Tag not found yet — retry will happen via _do_verify_poll
278            elapsed = time.monotonic() - self._verify_start_time
279            remaining = max(0, VERIFY_TIMEOUT_S - elapsed)
280            self._log(f"Tag not detected, retrying... "
281                      f"({remaining:.0f}s remaining)")
282            return
283
284        self._stop_poll()
285
286        current_uid = tag_data.uid_hex
287        if current_uid != self._found_uid:
288            self._set_state(ERROR)
289            messagebox.showerror(
290                "Write Failed",
291                f"Tag changed during write!\n\n"
292                f"Expected UID: {self._found_uid}\n"
293                f"Detected UID: {current_uid}\n\n"
294                f"Place the original tag back and try again.",
295                parent=self)
296            self._log(
297                f"Write failed: tag changed "
298                f"(expected {self._found_uid}, "
299                f"got {current_uid})")
300            return
301
302        # UID matches — proceed with write
303        self._set_state(WRITING)
304        self._log("Tag verified. Writing...")
305        if self._request_write:
306            self._request_write(
307                sn=self._sn_var.get().strip(),
308                year=self._year_var.get().strip(),
309                note=self._note_var.get().strip(),
310                uri=self._uri_var.get().strip(),
311            )
312
313    def on_write_result(self, success, error_msg=''):
314        """Called when write operation completes."""
315        if success:
316            self._set_state(DONE)
317            self._log("Write successful!")
318            self._log("Tag has been programmed. You may remove it.")
319            messagebox.showinfo(
320                "Write Complete",
321                "Tag has been successfully programmed!",
322                parent=self)
323        else:
324            self._set_state(ERROR)
325            self._log(f"Write failed: {error_msg}")
326            messagebox.showerror(
327                "Write Failed",
328                f"Tag write failed:\n\n{error_msg}\n\n"
329                f"Click 'Start Write' to try again.",
330                parent=self)

Multi-step write workflow with tag verification safety.

WriteTab(parent, request_poll=None, request_write=None)
29    def __init__(self, parent, request_poll=None, request_write=None):
30        super().__init__(parent)
31        self._request_poll = request_poll
32        self._request_write = request_write
33        self._state = IDLE
34        self._found_uid = None  # UID captured when tag first detected
35        self._found_tag_data = None  # Full tag data from detection
36        self._poll_id = None
37        self._verify_start_time = 0
38
39        self._build_ui()

Construct a Ttk Frame with parent master.

STANDARD OPTIONS

class, cursor, style, takefocus

WIDGET-SPECIFIC OPTIONS

borderwidth, relief, padding, width, height
def on_tab_selected(self):
198    def on_tab_selected(self):
199        pass
def on_tab_deselected(self):
201    def on_tab_deselected(self):
202        if self._state in (SEARCHING, VERIFYING):
203            self._on_cancel()
def on_search_result(self, tag_data):
207    def on_search_result(self, tag_data):
208        """Called when searching for a tag during write workflow."""
209        if self._state != SEARCHING:
210            return
211
212        if tag_data is None:
213            return  # Keep searching
214
215        self._stop_poll()
216        self._found_uid = tag_data.uid_hex
217        self._found_tag_data = tag_data
218        self._set_state(TAG_FOUND)
219
220        # Build the confirmation message
221        self._log(f"Tag found! UID: {tag_data.uid_hex}")
222
223        existing_info = ""
224        if len(tag_data.user_data) > 0:
225            text = tag_data.get_text()
226            uri = tag_data.get_uri()
227            if text:
228                self._log(f"Existing text: {text}")
229                existing_info += f"Existing text: {text}\n"
230            if uri:
231                self._log(f"Existing URI: {uri}")
232                existing_info += f"Existing URI: {uri}\n"
233            if not text and not uri:
234                self._log("Tag is blank (no NDEF data).")
235                existing_info = "Tag is currently blank.\n"
236        else:
237            self._log("Could not read tag data (UID only).")
238            existing_info = "Could not read existing data.\n"
239
240        # Build new data preview
241        bat = BatteryData(
242            sn=self._sn_var.get().strip(),
243            year=self._year_var.get().strip(),
244            note=self._note_var.get().strip(),
245        )
246        new_uri = self._uri_var.get().strip()
247
248        # Show confirmation dialog
249        msg = (
250            f"Tag detected: UID {tag_data.uid_hex}\n\n"
251            f"{existing_info}\n"
252            f"--- Write the following? ---\n"
253            f"URI: {new_uri}\n"
254            f"{bat.to_text()}\n\n"
255            f"Proceed with write?"
256        )
257
258        proceed = messagebox.askyesno(
259            "Confirm Write", msg, parent=self)
260
261        if proceed:
262            self._log("")
263            self._log("Write confirmed. Verifying tag...")
264            self._set_state(VERIFYING)
265            self._verify_start_time = time.monotonic()
266            self._do_verify_poll()
267        else:
268            self._set_state(IDLE)
269            self._log("Write cancelled by user.")

Called when searching for a tag during write workflow.

def on_verify_result(self, tag_data):
271    def on_verify_result(self, tag_data):
272        """Called during write verification — check UID matches."""
273        if self._state != VERIFYING:
274            return
275
276        if tag_data is None:
277            # Tag not found yet — retry will happen via _do_verify_poll
278            elapsed = time.monotonic() - self._verify_start_time
279            remaining = max(0, VERIFY_TIMEOUT_S - elapsed)
280            self._log(f"Tag not detected, retrying... "
281                      f"({remaining:.0f}s remaining)")
282            return
283
284        self._stop_poll()
285
286        current_uid = tag_data.uid_hex
287        if current_uid != self._found_uid:
288            self._set_state(ERROR)
289            messagebox.showerror(
290                "Write Failed",
291                f"Tag changed during write!\n\n"
292                f"Expected UID: {self._found_uid}\n"
293                f"Detected UID: {current_uid}\n\n"
294                f"Place the original tag back and try again.",
295                parent=self)
296            self._log(
297                f"Write failed: tag changed "
298                f"(expected {self._found_uid}, "
299                f"got {current_uid})")
300            return
301
302        # UID matches — proceed with write
303        self._set_state(WRITING)
304        self._log("Tag verified. Writing...")
305        if self._request_write:
306            self._request_write(
307                sn=self._sn_var.get().strip(),
308                year=self._year_var.get().strip(),
309                note=self._note_var.get().strip(),
310                uri=self._uri_var.get().strip(),
311            )

Called during write verification — check UID matches.

def on_write_result(self, success, error_msg=''):
313    def on_write_result(self, success, error_msg=''):
314        """Called when write operation completes."""
315        if success:
316            self._set_state(DONE)
317            self._log("Write successful!")
318            self._log("Tag has been programmed. You may remove it.")
319            messagebox.showinfo(
320                "Write Complete",
321                "Tag has been successfully programmed!",
322                parent=self)
323        else:
324            self._set_state(ERROR)
325            self._log(f"Write failed: {error_msg}")
326            messagebox.showerror(
327                "Write Failed",
328                f"Tag write failed:\n\n{error_msg}\n\n"
329                f"Click 'Start Write' to try again.",
330                parent=self)

Called when write operation completes.