ikkToolKit 0.0.0__tar.gz
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-0.0.0/LICENSE +21 -0
- ikktoolkit-0.0.0/PKG-INFO +98 -0
- ikktoolkit-0.0.0/README.md +67 -0
- ikktoolkit-0.0.0/ikkToolKit/ComPort/SerialBase.py +105 -0
- ikktoolkit-0.0.0/ikkToolKit/ComPort/SerialReceiver.py +278 -0
- ikktoolkit-0.0.0/ikkToolKit/ComPort/__init__.py +5 -0
- ikktoolkit-0.0.0/ikkToolKit/FileSelector/FileSelector.py +62 -0
- ikktoolkit-0.0.0/ikkToolKit/FileSelector/__init__.py +3 -0
- ikktoolkit-0.0.0/ikkToolKit/HID/HidCodeReader.py +130 -0
- ikktoolkit-0.0.0/ikkToolKit/HID/__init__.py +4 -0
- ikktoolkit-0.0.0/ikkToolKit/Lookup/CsvLookup.py +168 -0
- ikktoolkit-0.0.0/ikkToolKit/Lookup/__init__.py +4 -0
- ikktoolkit-0.0.0/ikkToolKit/QrCode/QrCodeBase.py +83 -0
- ikktoolkit-0.0.0/ikkToolKit/QrCode/__init__.py +4 -0
- ikktoolkit-0.0.0/ikkToolKit/Setting/SettingEditor.py +121 -0
- ikktoolkit-0.0.0/ikkToolKit/Setting/XmlSettingBase.py +123 -0
- ikktoolkit-0.0.0/ikkToolKit/Setting/__init__.py +5 -0
- ikktoolkit-0.0.0/ikkToolKit/Tcp/ModbusTcpSlave.py +188 -0
- ikktoolkit-0.0.0/ikkToolKit/Tcp/TcpServerBase.py +156 -0
- ikktoolkit-0.0.0/ikkToolKit/Tcp/__init__.py +5 -0
- ikktoolkit-0.0.0/ikkToolKit/Tcp/binary_echo_server.py +37 -0
- ikktoolkit-0.0.0/ikkToolKit/__init__.py +18 -0
- ikktoolkit-0.0.0/ikkToolKit.egg-info/PKG-INFO +98 -0
- ikktoolkit-0.0.0/ikkToolKit.egg-info/SOURCES.txt +28 -0
- ikktoolkit-0.0.0/ikkToolKit.egg-info/dependency_links.txt +1 -0
- ikktoolkit-0.0.0/ikkToolKit.egg-info/not-zip-safe +1 -0
- ikktoolkit-0.0.0/ikkToolKit.egg-info/requires.txt +5 -0
- ikktoolkit-0.0.0/ikkToolKit.egg-info/top_level.txt +1 -0
- ikktoolkit-0.0.0/pyproject.toml +48 -0
- ikktoolkit-0.0.0/setup.cfg +4 -0
ikktoolkit-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Insight.k.k
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ikkToolKit
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Insight.k.k. ToolKit for easy application development
|
|
5
|
+
Author-email: "Insight.k.k. Team" <maintainer@insightkk.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://insightkk.net/oss/ikkToolKit
|
|
8
|
+
Project-URL: Documentation, https://insightkk.net/oss/ikkToolKit/api/index.html
|
|
9
|
+
Project-URL: Issues, https://github.com/Insight-kk/ikkToolKit/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/Insight-kk/ikkToolKit
|
|
11
|
+
Keywords: framework,toolkit,async,serial,network
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Hardware
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: cantok>=0.0.1
|
|
26
|
+
Requires-Dist: asyncio-cancel-token>=0.2.0
|
|
27
|
+
Requires-Dist: pyserial>=3.5
|
|
28
|
+
Requires-Dist: keyboard>=0.13.0
|
|
29
|
+
Requires-Dist: qrcode[pil]>=8.0
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# ikkToolKit
|
|
33
|
+
|
|
34
|
+
Insight.k.k. による Insight Production開発向けPython ネットワーク・デバイス通信ツールキット
|
|
35
|
+
|
|
36
|
+
## 概要
|
|
37
|
+
|
|
38
|
+
UT装置の周辺デバイスやハードウェア制御を伴うアプリケーション開発を効率化するための統合ツールキット。シリアル通信、TCP/IP、HID入力、QRコード生成、XML設定管理など、組み込みシステム開発に必要な機能を提供します。
|
|
39
|
+
|
|
40
|
+
**Python 3.10 以上対応**
|
|
41
|
+
|
|
42
|
+
## 主なモジュール
|
|
43
|
+
|
|
44
|
+
### ComPort - シリアル通信
|
|
45
|
+
|
|
46
|
+
- **SerialBase**: シリアルポート通信の基本クラス
|
|
47
|
+
- **SerialReceiver**: バーコードリーダー対応の受信特化クラス(接続監視・自動再接続機能)
|
|
48
|
+
|
|
49
|
+
### HID - HID入力デバイス
|
|
50
|
+
|
|
51
|
+
- **HidCodeReader**: キーボード入力によるバーコード読み取り(プレフィックス・サフィックスにより対応)
|
|
52
|
+
|
|
53
|
+
### Tcp - TCP/IPサーバー
|
|
54
|
+
|
|
55
|
+
- **TcpServerBase**: 非同期マルチクライアント対応のTCPサーバー基底クラス
|
|
56
|
+
- **ModbusTcpSlave**: Modbusプロトコル対応のTCPスレーブ実装
|
|
57
|
+
|
|
58
|
+
### Lookup - CSV参照
|
|
59
|
+
|
|
60
|
+
- [CsvLookup](doc/Lookup.md): CSVファイルを使用したキー・バリュー検索
|
|
61
|
+
|
|
62
|
+
### QrCode - QRコード生成
|
|
63
|
+
|
|
64
|
+
- **QrCodeBase**: 文字列からPIL画像形式のQRコード生成
|
|
65
|
+
|
|
66
|
+
### Setting - 設定管理
|
|
67
|
+
|
|
68
|
+
- **XmlSettingBase**: XML設定ファイルの読み込み・管理
|
|
69
|
+
- **SettingEditor**: Tkinterベースの設定編集GUI
|
|
70
|
+
|
|
71
|
+
### FileSelector - ファイル選択
|
|
72
|
+
|
|
73
|
+
- [CsvLookup](doc/FileSelector.md): ファイル選択ダイアログを
|
|
74
|
+
|
|
75
|
+
## 主な機能
|
|
76
|
+
|
|
77
|
+
- 🔌 **非同期ネットワーク通信**: asyncio を活用したマルチクライアント対応
|
|
78
|
+
- 📡 **シリアル通信**: 自動接続・再接続機能付き
|
|
79
|
+
- ⚙️ **設定管理**: XML形式による柔軟な設定の読み込み・編集
|
|
80
|
+
- 🏷️ **バーコード処理**: QRコード生成、シリアル/HID入力対応
|
|
81
|
+
- 📊 **Modbus対応**: Modbus TCP レジスタ操作
|
|
82
|
+
|
|
83
|
+
## インストール
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install ikkToolKit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## リンク
|
|
90
|
+
|
|
91
|
+
- [公式ページ](https://insightkk.net/oss/ikkToolKit)
|
|
92
|
+
- [APIドキュメント](https://insightkk.net/oss/ikkToolKit/api/index.html)
|
|
93
|
+
- [GitHub リポジトリ](https://github.com/Insight-kk/ikkToolKit)
|
|
94
|
+
- [Issues](https://github.com/Insight-kk/ikkToolKit/issues)
|
|
95
|
+
|
|
96
|
+
## ライセンス
|
|
97
|
+
|
|
98
|
+
MIT License
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ikkToolKit
|
|
2
|
+
|
|
3
|
+
Insight.k.k. による Insight Production開発向けPython ネットワーク・デバイス通信ツールキット
|
|
4
|
+
|
|
5
|
+
## 概要
|
|
6
|
+
|
|
7
|
+
UT装置の周辺デバイスやハードウェア制御を伴うアプリケーション開発を効率化するための統合ツールキット。シリアル通信、TCP/IP、HID入力、QRコード生成、XML設定管理など、組み込みシステム開発に必要な機能を提供します。
|
|
8
|
+
|
|
9
|
+
**Python 3.10 以上対応**
|
|
10
|
+
|
|
11
|
+
## 主なモジュール
|
|
12
|
+
|
|
13
|
+
### ComPort - シリアル通信
|
|
14
|
+
|
|
15
|
+
- **SerialBase**: シリアルポート通信の基本クラス
|
|
16
|
+
- **SerialReceiver**: バーコードリーダー対応の受信特化クラス(接続監視・自動再接続機能)
|
|
17
|
+
|
|
18
|
+
### HID - HID入力デバイス
|
|
19
|
+
|
|
20
|
+
- **HidCodeReader**: キーボード入力によるバーコード読み取り(プレフィックス・サフィックスにより対応)
|
|
21
|
+
|
|
22
|
+
### Tcp - TCP/IPサーバー
|
|
23
|
+
|
|
24
|
+
- **TcpServerBase**: 非同期マルチクライアント対応のTCPサーバー基底クラス
|
|
25
|
+
- **ModbusTcpSlave**: Modbusプロトコル対応のTCPスレーブ実装
|
|
26
|
+
|
|
27
|
+
### Lookup - CSV参照
|
|
28
|
+
|
|
29
|
+
- [CsvLookup](doc/Lookup.md): CSVファイルを使用したキー・バリュー検索
|
|
30
|
+
|
|
31
|
+
### QrCode - QRコード生成
|
|
32
|
+
|
|
33
|
+
- **QrCodeBase**: 文字列からPIL画像形式のQRコード生成
|
|
34
|
+
|
|
35
|
+
### Setting - 設定管理
|
|
36
|
+
|
|
37
|
+
- **XmlSettingBase**: XML設定ファイルの読み込み・管理
|
|
38
|
+
- **SettingEditor**: Tkinterベースの設定編集GUI
|
|
39
|
+
|
|
40
|
+
### FileSelector - ファイル選択
|
|
41
|
+
|
|
42
|
+
- [CsvLookup](doc/FileSelector.md): ファイル選択ダイアログを
|
|
43
|
+
|
|
44
|
+
## 主な機能
|
|
45
|
+
|
|
46
|
+
- 🔌 **非同期ネットワーク通信**: asyncio を活用したマルチクライアント対応
|
|
47
|
+
- 📡 **シリアル通信**: 自動接続・再接続機能付き
|
|
48
|
+
- ⚙️ **設定管理**: XML形式による柔軟な設定の読み込み・編集
|
|
49
|
+
- 🏷️ **バーコード処理**: QRコード生成、シリアル/HID入力対応
|
|
50
|
+
- 📊 **Modbus対応**: Modbus TCP レジスタ操作
|
|
51
|
+
|
|
52
|
+
## インストール
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install ikkToolKit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## リンク
|
|
59
|
+
|
|
60
|
+
- [公式ページ](https://insightkk.net/oss/ikkToolKit)
|
|
61
|
+
- [APIドキュメント](https://insightkk.net/oss/ikkToolKit/api/index.html)
|
|
62
|
+
- [GitHub リポジトリ](https://github.com/Insight-kk/ikkToolKit)
|
|
63
|
+
- [Issues](https://github.com/Insight-kk/ikkToolKit/issues)
|
|
64
|
+
|
|
65
|
+
## ライセンス
|
|
66
|
+
|
|
67
|
+
MIT License
|
|
@@ -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)
|