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.
@@ -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,5 @@
1
+ """シリアル通信モジュール"""
2
+ from .SerialBase import SerialBase
3
+ from .SerialReceiver import SerialReceiver
4
+
5
+ __all__ = ["SerialBase", "SerialReceiver"]
@@ -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,3 @@
1
+ from .FileSelector import FileSelector
2
+
3
+ __all__ = ["FileSelector"]
@@ -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()
@@ -0,0 +1,4 @@
1
+ """シリアル通信モジュール"""
2
+ from .HidCodeReader import HidCodeReader
3
+
4
+ __all__ = ["HidCodeReader"]