ikkToolKit 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ikkToolKit/ComPort/SerialBase.py +105 -0
- ikkToolKit/ComPort/SerialReceiver.py +278 -0
- ikkToolKit/ComPort/__init__.py +5 -0
- ikkToolKit/FileSelector/FileSelector.py +62 -0
- ikkToolKit/FileSelector/__init__.py +3 -0
- ikkToolKit/HID/HidCodeReader.py +130 -0
- ikkToolKit/HID/__init__.py +4 -0
- ikkToolKit/Lookup/CsvLookup.py +168 -0
- ikkToolKit/Lookup/__init__.py +4 -0
- ikkToolKit/QrCode/QrCodeBase.py +83 -0
- ikkToolKit/QrCode/__init__.py +4 -0
- ikkToolKit/Setting/SettingEditor.py +121 -0
- ikkToolKit/Setting/XmlSettingBase.py +123 -0
- ikkToolKit/Setting/__init__.py +5 -0
- ikkToolKit/Tcp/ModbusTcpSlave.py +188 -0
- ikkToolKit/Tcp/TcpServerBase.py +156 -0
- ikkToolKit/Tcp/__init__.py +5 -0
- ikkToolKit/Tcp/binary_echo_server.py +37 -0
- ikkToolKit/__init__.py +18 -0
- ikktoolkit-0.0.0.dist-info/METADATA +98 -0
- ikktoolkit-0.0.0.dist-info/RECORD +24 -0
- ikktoolkit-0.0.0.dist-info/WHEEL +5 -0
- ikktoolkit-0.0.0.dist-info/licenses/LICENSE +21 -0
- ikktoolkit-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# シリアル通信の基本クラス
|
|
2
|
+
# 現時点ではバーコードリーダー向け用途に対応しているのでRead可能であるがWriteは未実装
|
|
3
|
+
# 自動接続などの機能は継承先で実装してね!
|
|
4
|
+
import logging
|
|
5
|
+
import serial
|
|
6
|
+
import serial.tools.list_ports
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional,Dict
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class SerialBase:
|
|
13
|
+
#--------------------------------------------------------------------------
|
|
14
|
+
def __init__(self):
|
|
15
|
+
'''コンストラクタ
|
|
16
|
+
'''
|
|
17
|
+
self._sp: Optional[serial.Serial] = None # シリアルオブジェクト
|
|
18
|
+
#--------------------------------------------------------------------------
|
|
19
|
+
@staticmethod
|
|
20
|
+
def PortLists() -> Dict[str, str]:
|
|
21
|
+
"""有効なシリアルポート一覧を辞書にして返す
|
|
22
|
+
Returns:
|
|
23
|
+
{ポート名: デバイス説明文} の辞書
|
|
24
|
+
"""
|
|
25
|
+
ports = serial.tools.list_ports.comports()
|
|
26
|
+
return {port.device: port.description for port in ports}
|
|
27
|
+
#--------------------------------------------------------------------------
|
|
28
|
+
@property
|
|
29
|
+
def BaudRate(self) -> int:
|
|
30
|
+
"""現在のボーレート"""
|
|
31
|
+
return self._sp.baudrate if self._sp else 0
|
|
32
|
+
@property
|
|
33
|
+
def portName(self) -> Optional[str]:
|
|
34
|
+
"""接続しているデバイスの名称"""
|
|
35
|
+
return self._sp.name if self._sp else None
|
|
36
|
+
@property
|
|
37
|
+
def deviceName(self) -> Optional[str]:
|
|
38
|
+
"""接続しているデバイスの説明文(description)"""
|
|
39
|
+
if not self._sp:
|
|
40
|
+
return None
|
|
41
|
+
# 現在のポート名と一致する ListPortInfo を探して description を返す
|
|
42
|
+
for info in serial.tools.list_ports.comports():
|
|
43
|
+
if info.device == self._sp.port:
|
|
44
|
+
return info.description
|
|
45
|
+
return None
|
|
46
|
+
#==========================================================================
|
|
47
|
+
def close(self):
|
|
48
|
+
sp = self._sp
|
|
49
|
+
self._sp = None
|
|
50
|
+
if not sp:
|
|
51
|
+
return
|
|
52
|
+
try:
|
|
53
|
+
if sp.is_open:
|
|
54
|
+
sp.close()
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
logger.warning("serial close failed during shutdown: %s", exc)
|
|
57
|
+
#==========================================================================
|
|
58
|
+
def open(self, port: str, baudrate: int = 9600) -> bool:
|
|
59
|
+
try:
|
|
60
|
+
self._sp = serial.Serial(
|
|
61
|
+
port=port,
|
|
62
|
+
baudrate=baudrate,
|
|
63
|
+
timeout=1,
|
|
64
|
+
write_timeout=1
|
|
65
|
+
)
|
|
66
|
+
# ⭐ 重要
|
|
67
|
+
self._sp.reset_input_buffer()
|
|
68
|
+
self._sp.reset_output_buffer()
|
|
69
|
+
# ⭐ CH340安定化
|
|
70
|
+
self._sp.dtr = False
|
|
71
|
+
time.sleep(0.05)
|
|
72
|
+
self._sp.dtr = True
|
|
73
|
+
return True
|
|
74
|
+
except serial.SerialException:
|
|
75
|
+
return False
|
|
76
|
+
#==========================================================================
|
|
77
|
+
def readLine(self, encoding: str = "utf-8", errors: str = "ignore") -> str:
|
|
78
|
+
'''シリアルから1行読み取る(ブロッキング)'''
|
|
79
|
+
sp = self._sp
|
|
80
|
+
if not sp or not sp.is_open:
|
|
81
|
+
raise serial.SerialException("Port not open")
|
|
82
|
+
buffer = bytearray()
|
|
83
|
+
while True:
|
|
84
|
+
ch = sp.read(1)
|
|
85
|
+
if ch == b'\n' or ch == b'\r': # 改行コードで終了
|
|
86
|
+
break
|
|
87
|
+
if ch == b'':
|
|
88
|
+
return ""
|
|
89
|
+
buffer += ch
|
|
90
|
+
return buffer.decode(encoding=encoding, errors=errors).strip()
|
|
91
|
+
###############################################################################
|
|
92
|
+
# テスト
|
|
93
|
+
###############################################################################
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
reader = SerialBase()
|
|
96
|
+
print("******** 有効なシリアルポート一覧 *********")
|
|
97
|
+
print(SerialBase.PortLists())
|
|
98
|
+
print("*****************************************")
|
|
99
|
+
#キーワードで開くテスト(環境に合わせてキーワードを変更してください)
|
|
100
|
+
for port in SerialBase.PortLists().keys():
|
|
101
|
+
if reader.open(port=port):
|
|
102
|
+
print(f"👌成功:{reader.portName} :{reader.deviceName} ")
|
|
103
|
+
reader.close()
|
|
104
|
+
else:
|
|
105
|
+
print("❌オープン失敗")
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# シリアル通信を継承したバーコードリーダークラス
|
|
2
|
+
# 受信特化で、接続監視と自動再接続機能を備える
|
|
3
|
+
# コードの内容などのユーザーごとの都合は継承先で実装してね!
|
|
4
|
+
import threading
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
import serial
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from .SerialBase import SerialBase
|
|
12
|
+
except ImportError:
|
|
13
|
+
from SerialBase import SerialBase
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class SerialReceiver(SerialBase):
|
|
18
|
+
#==========================================================================
|
|
19
|
+
def __init__(self, keyword: str, baudrate: int = 9600, debug: bool = False, encoding: str = "utf-8", decode_errors: str = "ignore", port_selector: Optional[Callable[[list[dict[str, str]]], Optional[str]]] = None):
|
|
20
|
+
'''コンストラクタ'''
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._keyword = keyword
|
|
23
|
+
self._bordRrate = baudrate
|
|
24
|
+
self._debugMode = debug
|
|
25
|
+
self._encoding = encoding
|
|
26
|
+
self._decodeErrors = decode_errors
|
|
27
|
+
self._portSelector = port_selector
|
|
28
|
+
self._preferredPort = ""
|
|
29
|
+
self._preferredVid = ""
|
|
30
|
+
self._preferredPid = ""
|
|
31
|
+
self._preferredSerialNumber = ""
|
|
32
|
+
self._lastSelectionPromptAt = 0.0
|
|
33
|
+
self._lastSelectionSignature: tuple[tuple[str, str, str, str], ...] = tuple()
|
|
34
|
+
self.on_receive: Optional[Callable[[str], None]] = None
|
|
35
|
+
self.on_connected: Optional[Callable[[], None]] = None
|
|
36
|
+
self.on_disconnected: Optional[Callable[[], None]] = None
|
|
37
|
+
self._running: bool = False
|
|
38
|
+
self._thread: Optional[threading.Thread] = None
|
|
39
|
+
self._log:Optional[logging.Logger] = None
|
|
40
|
+
def setLogger(self, logger: logging.Logger):
|
|
41
|
+
self._log = logger
|
|
42
|
+
def setPortSelector(self, selector: Optional[Callable[[list[dict[str, str]]], Optional[str]]]) -> None:
|
|
43
|
+
self._portSelector = selector
|
|
44
|
+
def setPreferredDevice(self, port: Optional[str] = None, vid: Optional[str] = None, pid: Optional[str] = None, serial_number: Optional[str] = None) -> None:
|
|
45
|
+
self._preferredPort = (port or "").strip().upper()
|
|
46
|
+
self._preferredVid = (vid or "").strip().upper()
|
|
47
|
+
self._preferredPid = (pid or "").strip().upper()
|
|
48
|
+
self._preferredSerialNumber = (serial_number or "").strip()
|
|
49
|
+
#==========================================================================
|
|
50
|
+
@property
|
|
51
|
+
def BaudRate(self) -> int:
|
|
52
|
+
"""現在のボーレート"""
|
|
53
|
+
return self._bordRrate
|
|
54
|
+
@property
|
|
55
|
+
def Encoding(self) -> str:
|
|
56
|
+
"""受信デコードに使う文字コード"""
|
|
57
|
+
return self._encoding
|
|
58
|
+
#==========================================================================
|
|
59
|
+
def start(self):
|
|
60
|
+
'''接続監視'''
|
|
61
|
+
# すでに動いてるなら何もしない
|
|
62
|
+
if self._running:
|
|
63
|
+
return
|
|
64
|
+
self._running = True
|
|
65
|
+
# デバッグモードならキー入力ループ、通常モードなら接続監視ループをスレッドで開始
|
|
66
|
+
if self._debugMode:
|
|
67
|
+
self._thread = threading.Thread(
|
|
68
|
+
target=self._debugLoop,
|
|
69
|
+
daemon=True
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
self._thread = threading.Thread(
|
|
73
|
+
target=self._loopRx,
|
|
74
|
+
daemon=True
|
|
75
|
+
)
|
|
76
|
+
self._thread.start()
|
|
77
|
+
#================================================================
|
|
78
|
+
def stop(self):
|
|
79
|
+
self._running = False
|
|
80
|
+
self.close()
|
|
81
|
+
if self._thread:
|
|
82
|
+
self._thread.join(timeout=1.2)
|
|
83
|
+
if self._thread.is_alive():
|
|
84
|
+
logger.warning("SerialReceiver thread did not stop within timeout")
|
|
85
|
+
self._thread = None
|
|
86
|
+
#----------------------------------------------------------------
|
|
87
|
+
def _debugLoop(self):
|
|
88
|
+
key_map = self._getDebugKeyMap()
|
|
89
|
+
print("=== DEBUG MODE ===")
|
|
90
|
+
if self.on_connected:
|
|
91
|
+
self.on_connected() #デバッグモードでも接続イベントは発火させる
|
|
92
|
+
for k, v in key_map.items():
|
|
93
|
+
print(f"{k} → {v}")
|
|
94
|
+
print("q → quit debug")
|
|
95
|
+
while self._running:
|
|
96
|
+
try:
|
|
97
|
+
key = input("> ").strip().lower()
|
|
98
|
+
if key == "q":
|
|
99
|
+
break
|
|
100
|
+
if key in key_map:
|
|
101
|
+
self._emitBarcode(key_map[key])
|
|
102
|
+
except (KeyboardInterrupt, EOFError):
|
|
103
|
+
break
|
|
104
|
+
self._running = False
|
|
105
|
+
#----------------------------------------------------------------
|
|
106
|
+
def _getDebugKeyMap(self):
|
|
107
|
+
"""デバッグモードで模擬するメッセージと発動キーのマップ。継承先で上書きする(これはひな形)
|
|
108
|
+
"""
|
|
109
|
+
return {
|
|
110
|
+
"a": "AAAA",
|
|
111
|
+
"b": "BBBB",
|
|
112
|
+
"c": "CCCC",
|
|
113
|
+
"1": "1111",
|
|
114
|
+
"2": "2222",
|
|
115
|
+
"3": "3333",
|
|
116
|
+
}
|
|
117
|
+
#----------------------------------------------------------------
|
|
118
|
+
def _emitBarcode(self, code: str):
|
|
119
|
+
if self.on_receive: #受信ベント発火🔥
|
|
120
|
+
self.on_receive(code)
|
|
121
|
+
#----------------------------------------------------------------
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _portInfoToCandidate(port_info) -> dict[str, str]:
|
|
124
|
+
vid = getattr(port_info, "vid", None)
|
|
125
|
+
pid = getattr(port_info, "pid", None)
|
|
126
|
+
return {
|
|
127
|
+
"port": str(getattr(port_info, "device", "") or ""),
|
|
128
|
+
"description": str(getattr(port_info, "description", "") or ""),
|
|
129
|
+
"manufacturer": str(getattr(port_info, "manufacturer", "") or ""),
|
|
130
|
+
"product": str(getattr(port_info, "product", "") or ""),
|
|
131
|
+
"serial_number": str(getattr(port_info, "serial_number", "") or ""),
|
|
132
|
+
"vid": f"{vid:04X}" if vid is not None else "",
|
|
133
|
+
"pid": f"{pid:04X}" if pid is not None else "",
|
|
134
|
+
}
|
|
135
|
+
#----------------------------------------------------------------
|
|
136
|
+
def _keywordMatches(self, port_info) -> bool:
|
|
137
|
+
description = str(getattr(port_info, "description", "") or "")
|
|
138
|
+
return bool(self._keyword) and self._keyword.upper() in description.upper()
|
|
139
|
+
#----------------------------------------------------------------
|
|
140
|
+
def _isFallbackCandidate(self, port_info) -> bool:
|
|
141
|
+
return False
|
|
142
|
+
#----------------------------------------------------------------
|
|
143
|
+
def _matchesPreferredIdentity(self, port_info) -> bool:
|
|
144
|
+
candidate = self._portInfoToCandidate(port_info)
|
|
145
|
+
candidate_serial = candidate["serial_number"]
|
|
146
|
+
if self._preferredSerialNumber and candidate_serial == self._preferredSerialNumber:
|
|
147
|
+
return True
|
|
148
|
+
if self._preferredVid and self._preferredPid:
|
|
149
|
+
return candidate["vid"] == self._preferredVid and candidate["pid"] == self._preferredPid
|
|
150
|
+
return False
|
|
151
|
+
#----------------------------------------------------------------
|
|
152
|
+
def _notifyConnected(self) -> None:
|
|
153
|
+
if self.on_connected:
|
|
154
|
+
self.on_connected()
|
|
155
|
+
#----------------------------------------------------------------
|
|
156
|
+
def _tryOpenPort(self, port_info) -> bool:
|
|
157
|
+
if self.open(port_info.device, self._bordRrate):
|
|
158
|
+
self._preferredPort = str(port_info.device).upper()
|
|
159
|
+
self._notifyConnected()
|
|
160
|
+
return True
|
|
161
|
+
return False
|
|
162
|
+
#----------------------------------------------------------------
|
|
163
|
+
def _requestFallbackSelection(self, candidates: list[dict[str, str]]) -> Optional[str]:
|
|
164
|
+
if not self._portSelector or not candidates:
|
|
165
|
+
return None
|
|
166
|
+
signature = tuple(
|
|
167
|
+
(candidate["port"], candidate["vid"], candidate["pid"], candidate["serial_number"])
|
|
168
|
+
for candidate in candidates
|
|
169
|
+
)
|
|
170
|
+
now = time.monotonic()
|
|
171
|
+
if signature == self._lastSelectionSignature and (now - self._lastSelectionPromptAt) < 10.0:
|
|
172
|
+
return None
|
|
173
|
+
self._lastSelectionSignature = signature
|
|
174
|
+
self._lastSelectionPromptAt = now
|
|
175
|
+
try:
|
|
176
|
+
selected_port = self._portSelector(candidates)
|
|
177
|
+
return selected_port.strip().upper() if selected_port else None
|
|
178
|
+
except Exception:
|
|
179
|
+
logger.exception("COMポート選択コールバックで例外発生")
|
|
180
|
+
return None
|
|
181
|
+
#----------------------------------------------------------------
|
|
182
|
+
def _connectAvailablePort(self) -> bool:
|
|
183
|
+
port_infos = list(serial.tools.list_ports.comports())
|
|
184
|
+
if not port_infos:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
tried_ports: set[str] = set()
|
|
188
|
+
|
|
189
|
+
def try_open(predicate: Callable[[object], bool]) -> bool:
|
|
190
|
+
for port_info in port_infos:
|
|
191
|
+
port_name = str(getattr(port_info, "device", "") or "").upper()
|
|
192
|
+
if not port_name or port_name in tried_ports:
|
|
193
|
+
continue
|
|
194
|
+
if not predicate(port_info):
|
|
195
|
+
continue
|
|
196
|
+
tried_ports.add(port_name)
|
|
197
|
+
if self._tryOpenPort(port_info):
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
if try_open(self._matchesPreferredIdentity):
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
if self._preferredPort:
|
|
205
|
+
preferred_port = self._preferredPort.upper()
|
|
206
|
+
for port_info in port_infos:
|
|
207
|
+
port_name = str(getattr(port_info, "device", "") or "").upper()
|
|
208
|
+
if port_name != preferred_port:
|
|
209
|
+
continue
|
|
210
|
+
tried_ports.add(port_name)
|
|
211
|
+
if self._tryOpenPort(port_info):
|
|
212
|
+
return True
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if try_open(self._keywordMatches):
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
fallback_candidates = [
|
|
219
|
+
self._portInfoToCandidate(port_info)
|
|
220
|
+
for port_info in port_infos
|
|
221
|
+
if str(getattr(port_info, "device", "") or "").upper() not in tried_ports and self._isFallbackCandidate(port_info)
|
|
222
|
+
]
|
|
223
|
+
selected_port = self._requestFallbackSelection(fallback_candidates)
|
|
224
|
+
if not selected_port:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
for port_info in port_infos:
|
|
228
|
+
port_name = str(getattr(port_info, "device", "") or "").upper()
|
|
229
|
+
if port_name != selected_port:
|
|
230
|
+
continue
|
|
231
|
+
return self._tryOpenPort(port_info)
|
|
232
|
+
return False
|
|
233
|
+
#----------------------------------------------------------------
|
|
234
|
+
def _loopRx(self):
|
|
235
|
+
"""仮想COMの受信ループ"""
|
|
236
|
+
while self._running:
|
|
237
|
+
# --- 未接続なら接続試行 ---
|
|
238
|
+
if not self._sp or not self._sp.is_open:
|
|
239
|
+
connected = self._connectAvailablePort()
|
|
240
|
+
if not connected:
|
|
241
|
+
time.sleep(1)
|
|
242
|
+
logger.debug("Waiting for device...")
|
|
243
|
+
continue
|
|
244
|
+
# --- 接続中は受信処理 ---
|
|
245
|
+
try:
|
|
246
|
+
line = self.readLine(encoding=self._encoding, errors=self._decodeErrors)
|
|
247
|
+
# logger.debug(f"Received line: {repr(line)}")
|
|
248
|
+
if line:
|
|
249
|
+
if self.on_receive:
|
|
250
|
+
self.on_receive(line) #受信ベント発火🔥
|
|
251
|
+
except serial.SerialException:
|
|
252
|
+
self.close()
|
|
253
|
+
if self.on_disconnected:
|
|
254
|
+
self.on_disconnected()
|
|
255
|
+
# ループ終了時安全クローズ
|
|
256
|
+
self.close()
|
|
257
|
+
###############################################################################
|
|
258
|
+
# テスト
|
|
259
|
+
###############################################################################
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
#コマンド引数 d があればデバッグモードで起動
|
|
262
|
+
import sys
|
|
263
|
+
debug_mode = "d" in sys.argv
|
|
264
|
+
|
|
265
|
+
#キーワードとデバッグモードを指定
|
|
266
|
+
sr = SerialReceiver(keyword="CH340", debug=debug_mode)
|
|
267
|
+
#イベントハンドラの設定(動作確認用のプリント)
|
|
268
|
+
sr.on_connected = lambda: print("🔗Device connected.")
|
|
269
|
+
sr.on_disconnected = lambda: print("⚡Device disconnected.")
|
|
270
|
+
sr.on_receive = lambda code: print(f"📥 Recive: {repr(code)}", flush=True)
|
|
271
|
+
sr.start()
|
|
272
|
+
print("Press Ctrl+C to stop...")
|
|
273
|
+
try:
|
|
274
|
+
while True:
|
|
275
|
+
time.sleep(1)
|
|
276
|
+
except KeyboardInterrupt:
|
|
277
|
+
print("Stopping...")
|
|
278
|
+
sr.stop()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import filedialog
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class FileSelector:
|
|
6
|
+
"""ファイル選択クラス(UIツールキットに依存しない)"""
|
|
7
|
+
def __init__(self, base_folder: str, extensions: list[str]):
|
|
8
|
+
"""
|
|
9
|
+
Args:
|
|
10
|
+
base_folder: 初期表示フォルダ
|
|
11
|
+
extensions: 拡張子リスト ['*.csv', '*.txt']
|
|
12
|
+
"""
|
|
13
|
+
self._base_folder = base_folder
|
|
14
|
+
self._extensions = extensions
|
|
15
|
+
self._last_folder = base_folder
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def base_folder(self) -> str:
|
|
19
|
+
return self._base_folder
|
|
20
|
+
@base_folder.setter
|
|
21
|
+
def base_folder(self, folder: str):
|
|
22
|
+
self._base_folder = folder
|
|
23
|
+
def select(self, root=None) -> str | None:
|
|
24
|
+
"""
|
|
25
|
+
ファイル選択ダイアログを表示
|
|
26
|
+
Args:
|
|
27
|
+
root: 外部から渡された tk.Tk インスタンス(オプション)
|
|
28
|
+
None の場合は自前で生成・破棄
|
|
29
|
+
Returns:
|
|
30
|
+
選択ファイルのフルパス、キャンセルの場合は None
|
|
31
|
+
"""
|
|
32
|
+
# 外部から root が渡されたかチェック
|
|
33
|
+
if root is None:
|
|
34
|
+
root = tk.Tk()
|
|
35
|
+
root.withdraw()
|
|
36
|
+
should_destroy = True
|
|
37
|
+
else:
|
|
38
|
+
should_destroy = False
|
|
39
|
+
try:
|
|
40
|
+
filetypes = [("対象ファイル", " ".join(self._extensions)),
|
|
41
|
+
("すべてのファイル", "*.*")]
|
|
42
|
+
file_path = filedialog.askopenfilename(
|
|
43
|
+
initialdir=self._last_folder,
|
|
44
|
+
title="ファイルを選択",
|
|
45
|
+
filetypes=filetypes
|
|
46
|
+
)
|
|
47
|
+
if file_path:
|
|
48
|
+
self._last_folder = str(Path(file_path).parent)
|
|
49
|
+
return file_path
|
|
50
|
+
else:
|
|
51
|
+
return None
|
|
52
|
+
finally:
|
|
53
|
+
if should_destroy:
|
|
54
|
+
root.destroy()
|
|
55
|
+
###############################################################################
|
|
56
|
+
#
|
|
57
|
+
###############################################################################
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
# ikkToolKit 側(root不要)
|
|
60
|
+
selector = FileSelector("C:\\", ["*.pdf"])
|
|
61
|
+
file_path = selector.select() # 自前で root を生成・破棄
|
|
62
|
+
print("Selected file:", file_path)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import keyboard
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
class HidCodeReader:
|
|
7
|
+
def __init__(self, debug=False, prefix='pause', suffix='right'):
|
|
8
|
+
"""
|
|
9
|
+
HIDバーコードリーダーの読み取りクラス
|
|
10
|
+
:param debug: デバッグモードの有無 (デフォルト: False)
|
|
11
|
+
:param prefix: 読み取り開始のキー名 (デフォルト: 'pause')
|
|
12
|
+
:param suffix: 読み取り終了のキー名 (デフォルト: 'right')
|
|
13
|
+
"""
|
|
14
|
+
self._debugMode = debug
|
|
15
|
+
self.prefix = prefix
|
|
16
|
+
self.suffix = suffix
|
|
17
|
+
self.on_begin_capture: Optional[Callable] = None
|
|
18
|
+
self.on_receive: Optional[Callable[[str], None]] = None
|
|
19
|
+
self._buffer = []
|
|
20
|
+
self._is_reading = False
|
|
21
|
+
self._is_running = False
|
|
22
|
+
|
|
23
|
+
def _handle_key_event(self, event):
|
|
24
|
+
"""内部のキーボードイベントハンドラ"""
|
|
25
|
+
if event.event_type != keyboard.KEY_DOWN:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
# 1. プレフィックスを検知
|
|
29
|
+
if event.name == self.prefix:
|
|
30
|
+
self._is_reading = True
|
|
31
|
+
self._buffer = []
|
|
32
|
+
if self.on_begin_capture: #キャプチャ開始イベントが登録されていれば呼び出す
|
|
33
|
+
self.on_begin_capture()
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# 2. サフィックスを検知
|
|
37
|
+
elif event.name == self.suffix:
|
|
38
|
+
if self._is_reading:
|
|
39
|
+
barcode_data = "".join(self._buffer)
|
|
40
|
+
self._is_reading = False
|
|
41
|
+
self._buffer = []
|
|
42
|
+
|
|
43
|
+
# 上位モジュールへイベント(コールバック)を通知
|
|
44
|
+
if self.on_receive and barcode_data:
|
|
45
|
+
self.on_receive(barcode_data)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# 3. データ蓄積
|
|
49
|
+
elif self._is_reading:
|
|
50
|
+
if len(event.name) == 1:
|
|
51
|
+
self._buffer.append(event.name)
|
|
52
|
+
#----------------------------------------------------------------
|
|
53
|
+
def start(self):
|
|
54
|
+
"""バックグラウンドでバーコードリーダーの監視を開始する"""
|
|
55
|
+
if self._is_running:
|
|
56
|
+
return # 既に実行中の場合は何もしない
|
|
57
|
+
self._is_running = True
|
|
58
|
+
# デバッグモードならキー入力ループ、通常モードなら接続監視ループをスレッドで開始
|
|
59
|
+
if self._debugMode:
|
|
60
|
+
self._thread = threading.Thread(
|
|
61
|
+
target=self._debugLoop,
|
|
62
|
+
daemon=True
|
|
63
|
+
)
|
|
64
|
+
self._thread.start()
|
|
65
|
+
else:
|
|
66
|
+
# keyboard.hook はグローバルでフックするため、
|
|
67
|
+
# startメソッドを実行するだけでバックグラウンド(別スレッド)でOS全体の入力を監視し始めます
|
|
68
|
+
keyboard.hook(self._handle_key_event)
|
|
69
|
+
#----------------------------------------------------------------
|
|
70
|
+
def stop(self):
|
|
71
|
+
"""監視を停止する"""
|
|
72
|
+
if not self._is_running:
|
|
73
|
+
return
|
|
74
|
+
self._is_running = False
|
|
75
|
+
if self._debugMode:
|
|
76
|
+
# デバッグスレッドの終了を待つ
|
|
77
|
+
if hasattr(self, '_thread') and self._thread.is_alive():
|
|
78
|
+
self._thread.join(timeout=2)
|
|
79
|
+
else:
|
|
80
|
+
# フックを解除して停止
|
|
81
|
+
keyboard.unhook(self._handle_key_event)
|
|
82
|
+
self._buffer = []
|
|
83
|
+
#----------------------------------------------------------------
|
|
84
|
+
def _debugLoop(self):
|
|
85
|
+
key_map = self._getDebugKeyMap()
|
|
86
|
+
print("=== DEBUG MODE ===")
|
|
87
|
+
for k, v in key_map.items():
|
|
88
|
+
print(f"{k} → {v}")
|
|
89
|
+
print("q → quit debug")
|
|
90
|
+
while self._is_running:
|
|
91
|
+
try:
|
|
92
|
+
key = input("> ").strip().lower()
|
|
93
|
+
if key == "q":
|
|
94
|
+
break
|
|
95
|
+
if key in key_map:
|
|
96
|
+
self._emitBarcode(key_map[key])
|
|
97
|
+
except (KeyboardInterrupt, EOFError):
|
|
98
|
+
break
|
|
99
|
+
self._is_running = False
|
|
100
|
+
#----------------------------------------------------------------
|
|
101
|
+
def _emitBarcode(self, code: str):
|
|
102
|
+
if self.on_receive: #受信ベント発火🔥
|
|
103
|
+
self.on_receive(code)
|
|
104
|
+
#----------------------------------------------------------------
|
|
105
|
+
def _getDebugKeyMap(self):
|
|
106
|
+
"""デバッグモードで模擬するメッセージと発動キーのマップ。継承先で上書きする(これはひな形)
|
|
107
|
+
"""
|
|
108
|
+
return {
|
|
109
|
+
"a": "AAAA",
|
|
110
|
+
"b": "BBBB",
|
|
111
|
+
"c": "CCCC",
|
|
112
|
+
"1": "1111",
|
|
113
|
+
"2": "2222",
|
|
114
|
+
"3": "3333",
|
|
115
|
+
}
|
|
116
|
+
###############################################################################
|
|
117
|
+
# テスト
|
|
118
|
+
###############################################################################
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
|
|
121
|
+
reader = HidCodeReader()
|
|
122
|
+
reader.on_receive = lambda code: print(f"📥 Recive: {repr(code)}", flush=True)
|
|
123
|
+
reader.start()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
while True:
|
|
127
|
+
time.sleep(1) # メインスレッドをアイドル状態にしておく
|
|
128
|
+
except KeyboardInterrupt:
|
|
129
|
+
print("Stopping...")
|
|
130
|
+
reader.stop()
|