host.nfc_tool.read_tab

Read tab — poll and display tag data.

  1"""Read tab — poll and display tag data."""
  2
  3import tkinter as tk
  4from tkinter import ttk
  5
  6from host.nfc_tool.battery_data import BatteryData
  7
  8
  9class ReadTab(ttk.Frame):
 10    """Tag reading with parsed battery data display."""
 11
 12    def __init__(self, parent, request_poll=None):
 13        super().__init__(parent)
 14        self._request_poll = request_poll
 15        self._polling = False
 16        self._poll_id = None
 17
 18        self._build_ui()
 19
 20    def _build_ui(self):
 21        # Status indicator row
 22        status_frame = ttk.Frame(self)
 23        status_frame.pack(fill=tk.X, padx=10, pady=(10, 5))
 24
 25        self._canvas = tk.Canvas(status_frame, width=20, height=20,
 26                                 highlightthickness=0)
 27        self._canvas.pack(side=tk.LEFT, padx=(0, 8))
 28        self._dot = self._canvas.create_oval(2, 2, 18, 18, fill='gray')
 29
 30        self._status_var = tk.StringVar(value="Idle")
 31        ttk.Label(status_frame, textvariable=self._status_var).pack(
 32            side=tk.LEFT)
 33
 34        # Controls
 35        ctrl_frame = ttk.Frame(self)
 36        ctrl_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
 37
 38        self._auto_var = tk.BooleanVar(value=False)
 39        ttk.Checkbutton(
 40            ctrl_frame, text="Auto-poll",
 41            variable=self._auto_var,
 42            command=self._on_auto_toggle,
 43        ).pack(side=tk.LEFT)
 44
 45        ttk.Button(
 46            ctrl_frame, text="Read Once",
 47            command=self._read_once,
 48        ).pack(side=tk.LEFT, padx=(10, 0))
 49
 50        # Tag info section
 51        info_frame = ttk.LabelFrame(self, text="Tag Info")
 52        info_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
 53
 54        fields = [
 55            ("UID:", "_uid_var"),
 56            ("URI:", "_uri_var"),
 57        ]
 58        for i, (label, attr) in enumerate(fields):
 59            ttk.Label(info_frame, text=label).grid(
 60                row=i, column=0, sticky=tk.W, padx=(8, 4), pady=2)
 61            var = tk.StringVar(value="\u2014")
 62            setattr(self, attr, var)
 63            ttk.Label(info_frame, textvariable=var,
 64                      font=('Consolas', 10)).grid(
 65                row=i, column=1, sticky=tk.W, pady=2)
 66
 67        # Battery data section
 68        bat_frame = ttk.LabelFrame(self, text="Battery Data")
 69        bat_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
 70
 71        bat_fields = [
 72            ("Serial Number:", "_sn_var"),
 73            ("Year:", "_year_var"),
 74            ("Note:", "_note_var"),
 75        ]
 76        for i, (label, attr) in enumerate(bat_fields):
 77            ttk.Label(bat_frame, text=label).grid(
 78                row=i, column=0, sticky=tk.W, padx=(8, 4), pady=2)
 79            var = tk.StringVar(value="\u2014")
 80            setattr(self, attr, var)
 81            ttk.Label(bat_frame, textvariable=var).grid(
 82                row=i, column=1, sticky=tk.W, pady=2)
 83
 84        # Raw NDEF text
 85        raw_frame = ttk.LabelFrame(self, text="Raw NDEF Text")
 86        raw_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
 87
 88        self._raw_text = tk.Text(raw_frame, height=6, wrap=tk.WORD,
 89                                 state='disabled', font=('Consolas', 9))
 90        self._raw_text.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
 91
 92    def on_tab_selected(self):
 93        if self._auto_var.get():
 94            self._start_polling()
 95
 96    def on_tab_deselected(self):
 97        self._stop_polling()
 98
 99    def _on_auto_toggle(self):
100        if self._auto_var.get():
101            self._start_polling()
102        else:
103            self._stop_polling()
104
105    def _read_once(self):
106        if self._request_poll:
107            self._request_poll('read')
108
109    def _start_polling(self):
110        if self._polling:
111            return
112        self._polling = True
113        self._do_poll()
114
115    def _stop_polling(self):
116        self._polling = False
117        if self._poll_id is not None:
118            self.after_cancel(self._poll_id)
119            self._poll_id = None
120
121    def _do_poll(self):
122        if not self._polling:
123            return
124        if self._request_poll:
125            self._request_poll('read')
126        self._poll_id = self.after(500, self._do_poll)
127
128    def update_tag_data(self, tag_data):
129        """Called by app when worker returns read result.
130
131        Data fields are latched — they persist when the tag is removed.
132        Only the dot color and status text update to reflect presence.
133        """
134        if tag_data is None:
135            # Tag gone — update indicator only, keep latched data
136            self._canvas.itemconfig(self._dot, fill='#cc3333')
137            self._status_var.set("No tag (last read shown)")
138            return
139
140        if len(tag_data.user_data) == 0:
141            self._canvas.itemconfig(self._dot, fill='orange')
142            self._status_var.set("Tag detected but data read failed")
143            self._uid_var.set(tag_data.uid_hex)
144            return
145
146        self._canvas.itemconfig(self._dot, fill='#33cc33')
147        self._uid_var.set(tag_data.uid_hex)
148
149        # URI
150        uri = tag_data.get_uri()
151        self._uri_var.set(uri if uri else "\u2014")
152
153        # Parse battery data from text records
154        text = tag_data.get_text()
155        self._set_raw_text(text)
156
157        if 'bat:' in text:
158            bat = BatteryData.from_text(text)
159            self._status_var.set("Battery tag recognized")
160            self._sn_var.set(bat.sn if bat.sn else "\u2014")
161            self._year_var.set(bat.year if bat.year else "\u2014")
162            self._note_var.set(bat.note if bat.note else "\u2014")
163        else:
164            self._status_var.set("Tag read (unrecognized format)")
165            self._sn_var.set("\u2014")
166            self._year_var.set("\u2014")
167            self._note_var.set("\u2014")
168
169    def _set_raw_text(self, text):
170        self._raw_text.configure(state='normal')
171        self._raw_text.delete('1.0', tk.END)
172        if text:
173            self._raw_text.insert('1.0', text)
174        self._raw_text.configure(state='disabled')
class ReadTab(tkinter.ttk.Frame):
 10class ReadTab(ttk.Frame):
 11    """Tag reading with parsed battery data display."""
 12
 13    def __init__(self, parent, request_poll=None):
 14        super().__init__(parent)
 15        self._request_poll = request_poll
 16        self._polling = False
 17        self._poll_id = None
 18
 19        self._build_ui()
 20
 21    def _build_ui(self):
 22        # Status indicator row
 23        status_frame = ttk.Frame(self)
 24        status_frame.pack(fill=tk.X, padx=10, pady=(10, 5))
 25
 26        self._canvas = tk.Canvas(status_frame, width=20, height=20,
 27                                 highlightthickness=0)
 28        self._canvas.pack(side=tk.LEFT, padx=(0, 8))
 29        self._dot = self._canvas.create_oval(2, 2, 18, 18, fill='gray')
 30
 31        self._status_var = tk.StringVar(value="Idle")
 32        ttk.Label(status_frame, textvariable=self._status_var).pack(
 33            side=tk.LEFT)
 34
 35        # Controls
 36        ctrl_frame = ttk.Frame(self)
 37        ctrl_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
 38
 39        self._auto_var = tk.BooleanVar(value=False)
 40        ttk.Checkbutton(
 41            ctrl_frame, text="Auto-poll",
 42            variable=self._auto_var,
 43            command=self._on_auto_toggle,
 44        ).pack(side=tk.LEFT)
 45
 46        ttk.Button(
 47            ctrl_frame, text="Read Once",
 48            command=self._read_once,
 49        ).pack(side=tk.LEFT, padx=(10, 0))
 50
 51        # Tag info section
 52        info_frame = ttk.LabelFrame(self, text="Tag Info")
 53        info_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
 54
 55        fields = [
 56            ("UID:", "_uid_var"),
 57            ("URI:", "_uri_var"),
 58        ]
 59        for i, (label, attr) in enumerate(fields):
 60            ttk.Label(info_frame, text=label).grid(
 61                row=i, column=0, sticky=tk.W, padx=(8, 4), pady=2)
 62            var = tk.StringVar(value="\u2014")
 63            setattr(self, attr, var)
 64            ttk.Label(info_frame, textvariable=var,
 65                      font=('Consolas', 10)).grid(
 66                row=i, column=1, sticky=tk.W, pady=2)
 67
 68        # Battery data section
 69        bat_frame = ttk.LabelFrame(self, text="Battery Data")
 70        bat_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
 71
 72        bat_fields = [
 73            ("Serial Number:", "_sn_var"),
 74            ("Year:", "_year_var"),
 75            ("Note:", "_note_var"),
 76        ]
 77        for i, (label, attr) in enumerate(bat_fields):
 78            ttk.Label(bat_frame, text=label).grid(
 79                row=i, column=0, sticky=tk.W, padx=(8, 4), pady=2)
 80            var = tk.StringVar(value="\u2014")
 81            setattr(self, attr, var)
 82            ttk.Label(bat_frame, textvariable=var).grid(
 83                row=i, column=1, sticky=tk.W, pady=2)
 84
 85        # Raw NDEF text
 86        raw_frame = ttk.LabelFrame(self, text="Raw NDEF Text")
 87        raw_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
 88
 89        self._raw_text = tk.Text(raw_frame, height=6, wrap=tk.WORD,
 90                                 state='disabled', font=('Consolas', 9))
 91        self._raw_text.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
 92
 93    def on_tab_selected(self):
 94        if self._auto_var.get():
 95            self._start_polling()
 96
 97    def on_tab_deselected(self):
 98        self._stop_polling()
 99
100    def _on_auto_toggle(self):
101        if self._auto_var.get():
102            self._start_polling()
103        else:
104            self._stop_polling()
105
106    def _read_once(self):
107        if self._request_poll:
108            self._request_poll('read')
109
110    def _start_polling(self):
111        if self._polling:
112            return
113        self._polling = True
114        self._do_poll()
115
116    def _stop_polling(self):
117        self._polling = False
118        if self._poll_id is not None:
119            self.after_cancel(self._poll_id)
120            self._poll_id = None
121
122    def _do_poll(self):
123        if not self._polling:
124            return
125        if self._request_poll:
126            self._request_poll('read')
127        self._poll_id = self.after(500, self._do_poll)
128
129    def update_tag_data(self, tag_data):
130        """Called by app when worker returns read result.
131
132        Data fields are latched — they persist when the tag is removed.
133        Only the dot color and status text update to reflect presence.
134        """
135        if tag_data is None:
136            # Tag gone — update indicator only, keep latched data
137            self._canvas.itemconfig(self._dot, fill='#cc3333')
138            self._status_var.set("No tag (last read shown)")
139            return
140
141        if len(tag_data.user_data) == 0:
142            self._canvas.itemconfig(self._dot, fill='orange')
143            self._status_var.set("Tag detected but data read failed")
144            self._uid_var.set(tag_data.uid_hex)
145            return
146
147        self._canvas.itemconfig(self._dot, fill='#33cc33')
148        self._uid_var.set(tag_data.uid_hex)
149
150        # URI
151        uri = tag_data.get_uri()
152        self._uri_var.set(uri if uri else "\u2014")
153
154        # Parse battery data from text records
155        text = tag_data.get_text()
156        self._set_raw_text(text)
157
158        if 'bat:' in text:
159            bat = BatteryData.from_text(text)
160            self._status_var.set("Battery tag recognized")
161            self._sn_var.set(bat.sn if bat.sn else "\u2014")
162            self._year_var.set(bat.year if bat.year else "\u2014")
163            self._note_var.set(bat.note if bat.note else "\u2014")
164        else:
165            self._status_var.set("Tag read (unrecognized format)")
166            self._sn_var.set("\u2014")
167            self._year_var.set("\u2014")
168            self._note_var.set("\u2014")
169
170    def _set_raw_text(self, text):
171        self._raw_text.configure(state='normal')
172        self._raw_text.delete('1.0', tk.END)
173        if text:
174            self._raw_text.insert('1.0', text)
175        self._raw_text.configure(state='disabled')

Tag reading with parsed battery data display.

ReadTab(parent, request_poll=None)
13    def __init__(self, parent, request_poll=None):
14        super().__init__(parent)
15        self._request_poll = request_poll
16        self._polling = False
17        self._poll_id = None
18
19        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):
93    def on_tab_selected(self):
94        if self._auto_var.get():
95            self._start_polling()
def on_tab_deselected(self):
97    def on_tab_deselected(self):
98        self._stop_polling()
def update_tag_data(self, tag_data):
129    def update_tag_data(self, tag_data):
130        """Called by app when worker returns read result.
131
132        Data fields are latched — they persist when the tag is removed.
133        Only the dot color and status text update to reflect presence.
134        """
135        if tag_data is None:
136            # Tag gone — update indicator only, keep latched data
137            self._canvas.itemconfig(self._dot, fill='#cc3333')
138            self._status_var.set("No tag (last read shown)")
139            return
140
141        if len(tag_data.user_data) == 0:
142            self._canvas.itemconfig(self._dot, fill='orange')
143            self._status_var.set("Tag detected but data read failed")
144            self._uid_var.set(tag_data.uid_hex)
145            return
146
147        self._canvas.itemconfig(self._dot, fill='#33cc33')
148        self._uid_var.set(tag_data.uid_hex)
149
150        # URI
151        uri = tag_data.get_uri()
152        self._uri_var.set(uri if uri else "\u2014")
153
154        # Parse battery data from text records
155        text = tag_data.get_text()
156        self._set_raw_text(text)
157
158        if 'bat:' in text:
159            bat = BatteryData.from_text(text)
160            self._status_var.set("Battery tag recognized")
161            self._sn_var.set(bat.sn if bat.sn else "\u2014")
162            self._year_var.set(bat.year if bat.year else "\u2014")
163            self._note_var.set(bat.note if bat.note else "\u2014")
164        else:
165            self._status_var.set("Tag read (unrecognized format)")
166            self._sn_var.set("\u2014")
167            self._year_var.set("\u2014")
168            self._note_var.set("\u2014")

Called by app when worker returns read result.

Data fields are latched — they persist when the tag is removed. Only the dot color and status text update to reflect presence.