pm2cd 0.1.1__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.
- pm2cd/__init__.py +3 -0
- pm2cd/core/__init__.py +12 -0
- pm2cd/core/communication_tcp.py +416 -0
- pm2cd/core/controller_status.py +35 -0
- pm2cd/core/data.py +32 -0
- pm2cd/core/errors.py +10 -0
- pm2cd/core/home_status.py +31 -0
- pm2cd/core/limit_status.py +71 -0
- pm2cd/core/limit_switch_setting.py +163 -0
- pm2cd/core/motor_setting.py +39 -0
- pm2cd/core/motor_status.py +120 -0
- pm2cd/core/stop_mode.py +27 -0
- pm2cd/core/version_info.py +129 -0
- pm2cd/pm2cd_ctrl.py +1355 -0
- pm2cd/pm2cd_device.py +2287 -0
- pm2cd/pm2cd_single.py +2105 -0
- pm2cd/server_fastapi/__init__.py +24 -0
- pm2cd/server_fastapi/__init__.pyi +15 -0
- pm2cd/server_fastapi/client_async.py +27 -0
- pm2cd/server_fastapi/client_async.pyi +108 -0
- pm2cd/server_fastapi/client_sync.py +27 -0
- pm2cd/server_fastapi/client_sync.pyi +107 -0
- pm2cd/server_fastapi/make_pyi.py +42 -0
- pm2cd/server_fastapi/server.py +62 -0
- pm2cd-0.1.1.dist-info/METADATA +1454 -0
- pm2cd-0.1.1.dist-info/RECORD +28 -0
- pm2cd-0.1.1.dist-info/WHEEL +5 -0
- pm2cd-0.1.1.dist-info/top_level.txt +1 -0
pm2cd/__init__.py
ADDED
pm2cd/core/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .communication_tcp import Communication
|
|
2
|
+
from .errors import CommunicationError, MotorControlError, ProtocolError
|
|
3
|
+
from .motor_status import MotorStatus
|
|
4
|
+
from .version_info import VersionInfo
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Communication",
|
|
8
|
+
"CommunicationError",
|
|
9
|
+
"MotorControlError",
|
|
10
|
+
"ProtocolError",
|
|
11
|
+
"MotorStatus",
|
|
12
|
+
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pm2cd.core.errors import CommunicationError
|
|
7
|
+
|
|
8
|
+
# TCP通信層。PM2CD 固有のコマンド解釈は行わず、送受信だけを担当する。
|
|
9
|
+
# 通信異常時は接続状態と受信バッファを破棄し、次回通信へ異常応答を持ち越さない。
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Communication:
|
|
13
|
+
"""PMシリーズ用のTCP通信層。
|
|
14
|
+
|
|
15
|
+
TCPソケットの生成、接続、CR+LF単位の送受信、切断までをこのクラス内で
|
|
16
|
+
完結させる。
|
|
17
|
+
|
|
18
|
+
通常運転中に発生し得る通信失敗はログへ原因を記録し、``False`` または
|
|
19
|
+
``None`` を返す。コンストラクタ引数の不正など、戻り値では表現できない
|
|
20
|
+
呼び出し側の誤りは例外とする。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
ip_address: str,
|
|
26
|
+
port: int = 7777,
|
|
27
|
+
timeout_sec: float = 3.0,
|
|
28
|
+
encoding: str = "ascii",
|
|
29
|
+
logger=None,
|
|
30
|
+
log_level: str = "INFO",
|
|
31
|
+
):
|
|
32
|
+
"""インスタンスを初期化する。
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
ip_address: PM2CD の IP アドレス。
|
|
36
|
+
port: PM2CD の TCP ポート番号。
|
|
37
|
+
timeout_sec: TCP 通信および待機処理のタイムアウト秒数。
|
|
38
|
+
encoding: encoding。
|
|
39
|
+
logger: 使用する logger。None の場合はこのモジュールで生成する。
|
|
40
|
+
log_level: logger を生成する場合のログレベル。
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
TypeError: 引数の型が不正な場合。
|
|
44
|
+
ValueError: 引数の値が不正な場合。
|
|
45
|
+
"""
|
|
46
|
+
if not logger:
|
|
47
|
+
logging.basicConfig(level=(log_level or "INFO").upper())
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
self._logger = logger
|
|
51
|
+
|
|
52
|
+
if not isinstance(ip_address, str):
|
|
53
|
+
raise TypeError("ip_addressはstrで指定してください")
|
|
54
|
+
|
|
55
|
+
if not ip_address:
|
|
56
|
+
raise ValueError("ip_addressは空にできません")
|
|
57
|
+
|
|
58
|
+
if not isinstance(port, int) or isinstance(port, bool):
|
|
59
|
+
raise TypeError("portはintで指定してください")
|
|
60
|
+
|
|
61
|
+
if port < 1 or port > 65535:
|
|
62
|
+
raise ValueError("portは1から65535の範囲で指定してください")
|
|
63
|
+
|
|
64
|
+
if not isinstance(timeout_sec, (int, float)) or isinstance(timeout_sec, bool):
|
|
65
|
+
raise TypeError("timeout_secは数値で指定してください")
|
|
66
|
+
|
|
67
|
+
if timeout_sec <= 0:
|
|
68
|
+
raise ValueError("timeout_secは0より大きい値を指定してください")
|
|
69
|
+
|
|
70
|
+
if not isinstance(encoding, str):
|
|
71
|
+
raise TypeError("encodingはstrで指定してください")
|
|
72
|
+
|
|
73
|
+
self._ip_address = ip_address
|
|
74
|
+
self._port = port
|
|
75
|
+
self._timeout_sec = timeout_sec
|
|
76
|
+
self._encoding = encoding
|
|
77
|
+
|
|
78
|
+
self._socket = None # type: Optional[socket.socket]
|
|
79
|
+
self._receive_buffer = bytearray()
|
|
80
|
+
self._delimiter = b"\r\n"
|
|
81
|
+
self._lock = threading.RLock()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def ip_address(
|
|
85
|
+
self,
|
|
86
|
+
):
|
|
87
|
+
"""ip_address を返す。"""
|
|
88
|
+
return self._ip_address
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def port(
|
|
92
|
+
self,
|
|
93
|
+
):
|
|
94
|
+
"""port を返す。"""
|
|
95
|
+
return self._port
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def timeout_sec(
|
|
99
|
+
self,
|
|
100
|
+
):
|
|
101
|
+
"""timeout_sec を返す。"""
|
|
102
|
+
return self._timeout_sec
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def encoding(
|
|
106
|
+
self,
|
|
107
|
+
):
|
|
108
|
+
"""encoding を返す。"""
|
|
109
|
+
return self._encoding
|
|
110
|
+
|
|
111
|
+
def is_connected(
|
|
112
|
+
self,
|
|
113
|
+
) -> bool:
|
|
114
|
+
"""TCP 接続状態を取得する。
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
接続中の場合は True、それ以外は False。
|
|
118
|
+
"""
|
|
119
|
+
if self._socket is None:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def connect(
|
|
125
|
+
self,
|
|
126
|
+
) -> bool:
|
|
127
|
+
"""TCP接続する。
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
接続済み、または接続に成功した場合は ``True``。
|
|
131
|
+
接続に失敗した場合は ``False``。
|
|
132
|
+
"""
|
|
133
|
+
self._logger.info(f"== connect()")
|
|
134
|
+
|
|
135
|
+
# 接続状態の変更を送受信と同じロックで直列化する。
|
|
136
|
+
with self._lock:
|
|
137
|
+
if self._socket is not None:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
sock = None # type: Optional[socket.socket]
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
sock = socket.create_connection(
|
|
144
|
+
(self._ip_address, self._port), timeout=self._timeout_sec
|
|
145
|
+
)
|
|
146
|
+
sock.settimeout(self._timeout_sec)
|
|
147
|
+
except OSError as exc:
|
|
148
|
+
if sock is not None:
|
|
149
|
+
try:
|
|
150
|
+
sock.close()
|
|
151
|
+
except OSError as close_exc:
|
|
152
|
+
self._logger.error(
|
|
153
|
+
f"接続失敗後のTCPソケット破棄にも失敗しました: {close_exc}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
message = (
|
|
157
|
+
f"TCP接続に失敗しました: {self._ip_address}:{self._port}: {exc}"
|
|
158
|
+
)
|
|
159
|
+
self._logger.error(message)
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
# 新しい TCP セッションへ以前の受信断片を持ち越さない。
|
|
163
|
+
self._socket = sock
|
|
164
|
+
self._receive_buffer.clear()
|
|
165
|
+
|
|
166
|
+
self._logger.info(f"TCP接続しました: {self._ip_address}:{self._port}")
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
def disconnect(
|
|
170
|
+
self,
|
|
171
|
+
) -> bool:
|
|
172
|
+
"""TCP接続を切断する。"""
|
|
173
|
+
self._logger.info(f"== disconnect()")
|
|
174
|
+
|
|
175
|
+
with self._lock:
|
|
176
|
+
# 切断処理中に別経路から古いソケットを使用しないよう、先に内部状態を切り離す。
|
|
177
|
+
sock = self._socket
|
|
178
|
+
self._socket = None
|
|
179
|
+
self._receive_buffer.clear()
|
|
180
|
+
|
|
181
|
+
if sock is None:
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
186
|
+
except OSError:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
sock.close()
|
|
191
|
+
except OSError as exc:
|
|
192
|
+
self._logger.error(f"TCP切断に失敗しました: {exc}")
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
self._logger.info(f"TCP切断しました")
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
def send(
|
|
199
|
+
self,
|
|
200
|
+
command: str,
|
|
201
|
+
) -> bool:
|
|
202
|
+
"""CR+LFを付加してコマンドを送信する。"""
|
|
203
|
+
self._logger.info(f"== send(): {command!r}")
|
|
204
|
+
|
|
205
|
+
if not isinstance(command, str):
|
|
206
|
+
raise TypeError("commandはstrで指定してください")
|
|
207
|
+
|
|
208
|
+
if "\r" in command or "\n" in command:
|
|
209
|
+
raise ValueError("commandに改行文字を含めることはできません")
|
|
210
|
+
|
|
211
|
+
with self._lock:
|
|
212
|
+
return self._send(command)
|
|
213
|
+
|
|
214
|
+
def receive(
|
|
215
|
+
self,
|
|
216
|
+
) -> Optional[str]:
|
|
217
|
+
"""CR+LF単位で装置応答を1件受信する。"""
|
|
218
|
+
self._logger.info(f"== receive()")
|
|
219
|
+
|
|
220
|
+
with self._lock:
|
|
221
|
+
return self._receive()
|
|
222
|
+
|
|
223
|
+
def query(
|
|
224
|
+
self,
|
|
225
|
+
command: str,
|
|
226
|
+
) -> Optional[str]:
|
|
227
|
+
"""コマンドを送信し、その応答を1件受信する。"""
|
|
228
|
+
self._logger.debug(f"== query(): {command!r}")
|
|
229
|
+
|
|
230
|
+
if not isinstance(command, str):
|
|
231
|
+
raise TypeError("commandはstrで指定してください")
|
|
232
|
+
|
|
233
|
+
if "\r" in command or "\n" in command:
|
|
234
|
+
raise ValueError("commandに改行文字を含めることはできません")
|
|
235
|
+
|
|
236
|
+
with self._lock:
|
|
237
|
+
# query は同一ロック内で send -> receive を完結させ、別コマンドの応答が
|
|
238
|
+
# 間へ入り込まないようにする。
|
|
239
|
+
if not self._send(command):
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
return self._receive()
|
|
243
|
+
|
|
244
|
+
def close(
|
|
245
|
+
self,
|
|
246
|
+
) -> bool:
|
|
247
|
+
"""通信を終了する。"""
|
|
248
|
+
return self.disconnect()
|
|
249
|
+
|
|
250
|
+
def __enter__(
|
|
251
|
+
self,
|
|
252
|
+
) -> "Communication":
|
|
253
|
+
"""context manager 開始時に接続を確立する。
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
'Communication' 型の結果。
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
CommunicationError: PM2CD への接続を確立できない場合。
|
|
260
|
+
"""
|
|
261
|
+
if not self.connect():
|
|
262
|
+
raise CommunicationError(
|
|
263
|
+
f"TCP接続を開始できません: {self._ip_address}:{self._port}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return self
|
|
267
|
+
|
|
268
|
+
def __exit__(
|
|
269
|
+
self,
|
|
270
|
+
exc_type,
|
|
271
|
+
exc_value,
|
|
272
|
+
traceback,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""context manager 終了時に接続を切断する。
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
exc_type: with ブロック内で発生した例外の型。
|
|
278
|
+
exc_value: with ブロック内で発生した例外。
|
|
279
|
+
traceback: with ブロック内で発生した例外の traceback。
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
None。
|
|
283
|
+
"""
|
|
284
|
+
self.disconnect()
|
|
285
|
+
|
|
286
|
+
def _send(
|
|
287
|
+
self,
|
|
288
|
+
command: str,
|
|
289
|
+
) -> bool:
|
|
290
|
+
"""ロック取得済みの状態でコマンドを送信する。"""
|
|
291
|
+
sock = self._require_socket("送信")
|
|
292
|
+
if sock is None:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
data = command.encode(self._encoding) + self._delimiter
|
|
297
|
+
except (UnicodeEncodeError, LookupError) as exc:
|
|
298
|
+
self._logger.error(
|
|
299
|
+
f"コマンドのエンコードに失敗しました: {command!r}: {exc}"
|
|
300
|
+
)
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
sock.sendall(data)
|
|
305
|
+
except (OSError, socket.timeout) as exc:
|
|
306
|
+
self._logger.error(f"コマンド送信に失敗しました: {command!r}: {exc}")
|
|
307
|
+
self._close_after_error()
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def _receive(
|
|
313
|
+
self,
|
|
314
|
+
) -> Optional[str]:
|
|
315
|
+
"""ロック取得済みの状態で応答を1件受信する。"""
|
|
316
|
+
while True:
|
|
317
|
+
# 受信済みバッファからCR+LF終端の1応答だけを切り出す。
|
|
318
|
+
delimiter_index = self._receive_buffer.find(self._delimiter)
|
|
319
|
+
|
|
320
|
+
if delimiter_index >= 0:
|
|
321
|
+
raw = bytes(self._receive_buffer[:delimiter_index])
|
|
322
|
+
del self._receive_buffer[: delimiter_index + len(self._delimiter)]
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
response = raw.decode(self._encoding)
|
|
326
|
+
except (UnicodeDecodeError, LookupError) as exc:
|
|
327
|
+
self._logger.error(f"装置応答のデコードに失敗しました: {exc}")
|
|
328
|
+
self._close_after_error()
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
return response
|
|
332
|
+
|
|
333
|
+
sock = self._require_socket("受信")
|
|
334
|
+
if sock is None:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
chunk = sock.recv(4096)
|
|
339
|
+
except socket.timeout as exc:
|
|
340
|
+
self._logger.error(f"装置応答の受信がタイムアウトしました: {exc}")
|
|
341
|
+
|
|
342
|
+
# タイムアウト後に遅延応答が到着すると、次のqueryが古い応答を
|
|
343
|
+
# 誤って受信する可能性があるため、接続を破棄して同期を取り直す。
|
|
344
|
+
self._close_after_error()
|
|
345
|
+
return None
|
|
346
|
+
except OSError as exc:
|
|
347
|
+
self._logger.error(f"装置応答の受信に失敗しました: {exc}")
|
|
348
|
+
self._close_after_error()
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
if not chunk:
|
|
352
|
+
self._logger.error(f"装置側からTCP接続が切断されました")
|
|
353
|
+
self._close_after_error()
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
self._receive_buffer.extend(chunk)
|
|
357
|
+
|
|
358
|
+
def _require_socket(
|
|
359
|
+
self,
|
|
360
|
+
operation: str,
|
|
361
|
+
) -> Optional[socket.socket]:
|
|
362
|
+
"""接続済みソケットを返す。"""
|
|
363
|
+
if self._socket is None:
|
|
364
|
+
self._logger.error(f"TCP接続されていないため{operation}できません")
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
return self._socket
|
|
368
|
+
|
|
369
|
+
def _close_after_error(
|
|
370
|
+
self,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""通信異常後にソケットと受信バッファを破棄する。"""
|
|
373
|
+
# エラー発生後はソケット参照を先に無効化し、再利用を防止する。
|
|
374
|
+
sock = self._socket
|
|
375
|
+
self._socket = None
|
|
376
|
+
self._receive_buffer.clear()
|
|
377
|
+
|
|
378
|
+
if sock is None:
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
sock.close()
|
|
383
|
+
except OSError as exc:
|
|
384
|
+
self._logger.error(f"通信異常後のTCPソケット破棄に失敗しました: {exc}")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main() -> None:
|
|
388
|
+
"""Communication の基本的な送受信例を実行する。
|
|
389
|
+
|
|
390
|
+
実機の IP アドレスに変更してから、パッケージのモジュールとして実行する。
|
|
391
|
+
``VER?`` を送信し、CR+LF で区切られた装置応答を 1 件受信する。
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
None。
|
|
395
|
+
"""
|
|
396
|
+
ip_address = "10.170.10.62"
|
|
397
|
+
port = 7777
|
|
398
|
+
|
|
399
|
+
communication = Communication(ip_address=ip_address, port=port)
|
|
400
|
+
|
|
401
|
+
if not communication.connect():
|
|
402
|
+
print("PM2CD へ接続できませんでした")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
response = communication.query("VER?")
|
|
407
|
+
if response is None:
|
|
408
|
+
print("VER? の取得に失敗しました")
|
|
409
|
+
else:
|
|
410
|
+
print(f"VER?: {response}")
|
|
411
|
+
finally:
|
|
412
|
+
communication.disconnect()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
if __name__ == "__main__":
|
|
416
|
+
main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class ControllerStatus:
|
|
7
|
+
"""``STQ?`` の REMOTE/LOCAL と停止可能数を保持する。"""
|
|
8
|
+
|
|
9
|
+
raw: str
|
|
10
|
+
remote_local: str
|
|
11
|
+
stopped_motor_count: int
|
|
12
|
+
is_remote: bool
|
|
13
|
+
can_start: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_controller_status(raw: str) -> Optional[ControllerStatus]:
|
|
17
|
+
"""``STQ?`` 応答を解析する。"""
|
|
18
|
+
if not isinstance(raw, str):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
text = raw.strip().upper()
|
|
22
|
+
if len(text) != 2 or text[0] not in ('R', 'L') or not text[1].isdigit():
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
count = int(text[1])
|
|
26
|
+
if count < 0 or count > 4:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
return ControllerStatus(
|
|
30
|
+
raw=text,
|
|
31
|
+
remote_local=text[0],
|
|
32
|
+
stopped_motor_count=count,
|
|
33
|
+
is_remote=text[0] == 'R',
|
|
34
|
+
can_start=count > 0,
|
|
35
|
+
)
|
pm2cd/core/data.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""PM2CD / PM2C-06A 固有データ。"""
|
|
2
|
+
|
|
3
|
+
# PM4C-06A series manual TABLE 2 RATE DATA (No. 000-025)
|
|
4
|
+
# Unit: ms / 1000 PPS
|
|
5
|
+
RATE_TABLE = [
|
|
6
|
+
1000.0,
|
|
7
|
+
800.0,
|
|
8
|
+
600.0,
|
|
9
|
+
500.0,
|
|
10
|
+
400.0,
|
|
11
|
+
300.0,
|
|
12
|
+
200.0,
|
|
13
|
+
150.0,
|
|
14
|
+
125.0,
|
|
15
|
+
100.0,
|
|
16
|
+
75.0,
|
|
17
|
+
50.0,
|
|
18
|
+
30.0,
|
|
19
|
+
20.0,
|
|
20
|
+
15.0,
|
|
21
|
+
10.0,
|
|
22
|
+
7.5,
|
|
23
|
+
5.0,
|
|
24
|
+
4.0,
|
|
25
|
+
2.0,
|
|
26
|
+
1.5,
|
|
27
|
+
1.0,
|
|
28
|
+
0.5,
|
|
29
|
+
0.3,
|
|
30
|
+
0.2,
|
|
31
|
+
0.1,
|
|
32
|
+
]
|
pm2cd/core/errors.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class HomeStatus:
|
|
7
|
+
"""``SETHP?x`` の ``0XYZ`` 応答を保持する。"""
|
|
8
|
+
|
|
9
|
+
raw: str
|
|
10
|
+
found: bool
|
|
11
|
+
found_direction: str
|
|
12
|
+
auto_start_direction: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_home_status(raw: str) -> Optional[HomeStatus]:
|
|
16
|
+
"""``SETHP?x`` 応答を解析する。"""
|
|
17
|
+
if not isinstance(raw, str):
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
text = raw.strip()
|
|
21
|
+
if len(text) != 4 or text[0] != '0':
|
|
22
|
+
return None
|
|
23
|
+
if any(value not in ('0', '1') for value in text[1:]):
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
return HomeStatus(
|
|
27
|
+
raw=text,
|
|
28
|
+
found=text[1] == '1',
|
|
29
|
+
found_direction='cw' if text[2] == '0' else 'ccw',
|
|
30
|
+
auto_start_direction='cw' if text[3] == '0' else 'ccw',
|
|
31
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class LimitStatus:
|
|
7
|
+
"""``HDSTLS?`` 応答中の1軸分のリミット状態を保持する。"""
|
|
8
|
+
|
|
9
|
+
motor: int
|
|
10
|
+
channel: int
|
|
11
|
+
raw: str
|
|
12
|
+
hard_status: int
|
|
13
|
+
soft_status: int
|
|
14
|
+
hard_cw: bool
|
|
15
|
+
hard_ccw: bool
|
|
16
|
+
hard_hp: bool
|
|
17
|
+
soft_cw: bool
|
|
18
|
+
soft_ccw: bool
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def hard(self) -> List[bool]:
|
|
22
|
+
"""ハードウェアLS現在状態を ``[CW, CCW, HP]`` で返す。"""
|
|
23
|
+
return [self.hard_cw, self.hard_ccw, self.hard_hp]
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def soft(self) -> List[bool]:
|
|
27
|
+
"""ソフトウェアLS現在状態を ``[CW, CCW]`` で返す。"""
|
|
28
|
+
return [self.soft_cw, self.soft_ccw]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_limit_status_all(raw: str) -> Optional[List[LimitStatus]]:
|
|
32
|
+
"""PM2CD の ``HDSTLS?`` 2ch応答 ``abHHSS`` を解析する。"""
|
|
33
|
+
if not isinstance(raw, str):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
text = raw.strip()
|
|
37
|
+
if len(text) != 6:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
channels = [int(value, 16) for value in text[0:2]]
|
|
42
|
+
hard_values = [int(value, 16) for value in text[2:4]]
|
|
43
|
+
soft_values = [int(value, 16) for value in text[4:6]]
|
|
44
|
+
except ValueError:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
values = []
|
|
48
|
+
for index in range(2):
|
|
49
|
+
hard_status = hard_values[index]
|
|
50
|
+
soft_status = soft_values[index]
|
|
51
|
+
if hard_status & ~0x07:
|
|
52
|
+
return None
|
|
53
|
+
if soft_status & ~0x03:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
values.append(
|
|
57
|
+
LimitStatus(
|
|
58
|
+
motor=index,
|
|
59
|
+
channel=channels[index],
|
|
60
|
+
raw=text,
|
|
61
|
+
hard_status=hard_status,
|
|
62
|
+
soft_status=soft_status,
|
|
63
|
+
hard_cw=bool(hard_status & 0x01),
|
|
64
|
+
hard_ccw=bool(hard_status & 0x02),
|
|
65
|
+
hard_hp=bool(hard_status & 0x04),
|
|
66
|
+
soft_cw=bool(soft_status & 0x01),
|
|
67
|
+
soft_ccw=bool(soft_status & 0x02),
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return values
|