aandd-reader 0.2.15__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.
- aandd_reader/__init__.py +20 -0
- aandd_reader/aandd_reader.py +765 -0
- aandd_reader/config.py +105 -0
- aandd_reader/server.py +13 -0
- aandd_reader/server_fastapi/__init__.py +47 -0
- aandd_reader/server_fastapi/__init__.pyi +15 -0
- aandd_reader/server_fastapi/clients/__init__.py +6 -0
- aandd_reader/server_fastapi/clients/async_aandd_reader_client.py +104 -0
- aandd_reader/server_fastapi/clients/async_aandd_reader_client.pyi +86 -0
- aandd_reader/server_fastapi/clients/sync_aandd_reader_client.py +53 -0
- aandd_reader/server_fastapi/clients/sync_aandd_reader_client.pyi +80 -0
- aandd_reader/server_fastapi/make.py +345 -0
- aandd_reader/server_fastapi/make_device_proxy.py +35 -0
- aandd_reader/server_fastapi/make_old.py +164 -0
- aandd_reader/server_fastapi/models/__init__.py +0 -0
- aandd_reader/server_fastapi/models/comman.py +17 -0
- aandd_reader/server_fastapi/routers/__init__.py +3 -0
- aandd_reader/server_fastapi/routers/aandd_reader_router.py +36 -0
- aandd_reader/server_fastapi/routers/aandd_reader_router.pyi +34 -0
- aandd_reader/server_fastapi/spec_ctrl.py +82 -0
- aandd_reader/server_grpc/__init__.py +8 -0
- aandd_reader/server_grpc/aandd_reader_grpc_client.py +128 -0
- aandd_reader/server_grpc/aandd_reader_grpc_client.pyi +23 -0
- aandd_reader/server_grpc/aandd_reader_grpc_server.py +107 -0
- aandd_reader/server_grpc/async_aandd_reader_grpc_client.py +104 -0
- aandd_reader/server_grpc/async_aandd_reader_grpc_client.pyi +23 -0
- aandd_reader/server_grpc/make_pyi.py +56 -0
- aandd_reader/try/try01.py +28 -0
- aandd_reader/try/try02.py +108 -0
- aandd_reader/try/try03.py +113 -0
- aandd_reader-0.2.15.dist-info/METADATA +104 -0
- aandd_reader-0.2.15.dist-info/RECORD +35 -0
- aandd_reader-0.2.15.dist-info/WHEEL +5 -0
- aandd_reader-0.2.15.dist-info/licenses/LICENSE +201 -0
- aandd_reader-0.2.15.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
"""
|
|
2
|
+
K.NAKADA, kengo.nakada@gmail.com/kengo.nakada@mat.shimane-u.ac.jp
|
|
3
|
+
|
|
4
|
+
返信メッセージの形式
|
|
5
|
+
|
|
6
|
+
- ACK なし(出荷時)
|
|
7
|
+
- ACK あり(現在)
|
|
8
|
+
|
|
9
|
+
PRT モードとデーター取得について
|
|
10
|
+
|
|
11
|
+
PRT は **「データ出力(Serial Print)の自動出力条件」**
|
|
12
|
+
天秤が“自分の判断で”出力を送るタイミングの設定であり、
|
|
13
|
+
Q/S/SIR などの“コマンド要求”とは別軸となる
|
|
14
|
+
|
|
15
|
+
1 Auto print A(基準=ゼロ) 安定+条件成立で自動出力
|
|
16
|
+
2 Auto print B(基準=前回の安定値) 安定+条件成立で自動出力
|
|
17
|
+
3 Stream mode 連続出力(表示更新周期に依存)
|
|
18
|
+
SIR と実施的には同じである
|
|
19
|
+
SIR はコマンドでスタートをキックできる PTR3 は開始したら動きっぱなし
|
|
20
|
+
どちらも readline で値を読み続けることができる
|
|
21
|
+
*) 測定可能なサイクルは 5pd で、 5/10/20 回/秒設定が可能
|
|
22
|
+
SAMPLE-key --> ba5fnc
|
|
23
|
+
4 Key mode B PRINTキーを押すと出力(安定不要)
|
|
24
|
+
5 Key mode C PRINTキーで出力(安定なら即、非安定なら待つ)
|
|
25
|
+
6 Interval mode 一定間隔で周期出力( mode3 との違いに注意)
|
|
26
|
+
|
|
27
|
+
--- 上記は結果をシリアルに流すので、コマンドで値要求(Q/S/SIR)と
|
|
28
|
+
混ざるとどの応答をシリアルで捉えているのか分かりにくい
|
|
29
|
+
STREAM MODE の測定周期は天秤側に設定する必要がある
|
|
30
|
+
|
|
31
|
+
コマンド出力(Q/S/SIR)
|
|
32
|
+
PCからのコマンドに対して天秤が返す出力
|
|
33
|
+
SIR は readline のストリームに流す。
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from typing import Any, Dict, List, Optional, Union, Tuple, Callable, Sequence
|
|
38
|
+
import serial
|
|
39
|
+
import time
|
|
40
|
+
import threading
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AanddReader:
|
|
45
|
+
"""
|
|
46
|
+
通信速度
|
|
47
|
+
天秤の 内部設定
|
|
48
|
+
bPS (ボーレート) 19200bps
|
|
49
|
+
|
|
50
|
+
全てのコマンドに対しての何らかの応答(ACK)を返すモードであるのが前提
|
|
51
|
+
天秤の 内部設定
|
|
52
|
+
SF(シリアルインターフェース)
|
|
53
|
+
ErCd 1 (出力する AK,コード)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
port: str,
|
|
59
|
+
baud_rate: Optional[int] = None,
|
|
60
|
+
timeout: Optional[float] = None,
|
|
61
|
+
logger: Optional[Any] = None,
|
|
62
|
+
log_level: Optional[str] = None,
|
|
63
|
+
):
|
|
64
|
+
# XLogger が存在しない時に仕方がないのでデフォルトの logging を使う
|
|
65
|
+
if logger is None:
|
|
66
|
+
import logging
|
|
67
|
+
|
|
68
|
+
log_level = log_level or "INFO"
|
|
69
|
+
logging.basicConfig(level=log_level.upper())
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
self._logger = logger
|
|
73
|
+
|
|
74
|
+
self._port = port or "COM3"
|
|
75
|
+
self._baud_rate = baud_rate or 19200
|
|
76
|
+
self._timeout = timeout or 0.5
|
|
77
|
+
self._meas_mode = "Q"
|
|
78
|
+
|
|
79
|
+
self._status: Optional[str] = None
|
|
80
|
+
self._unit: Optional[str] = None
|
|
81
|
+
self._weight: Optional[float] = None
|
|
82
|
+
|
|
83
|
+
self._logger.info(f"Create AandD Device (Serial)")
|
|
84
|
+
self._logger.info(f"timeout = {self._timeout}")
|
|
85
|
+
self._logger.info(f"baud_rate = {self._baud_rate}")
|
|
86
|
+
self._logger.info(f"port = {self._port}")
|
|
87
|
+
try:
|
|
88
|
+
self._ser = serial.Serial(
|
|
89
|
+
port=self._port,
|
|
90
|
+
baudrate=self._baud_rate,
|
|
91
|
+
parity=serial.PARITY_EVEN,
|
|
92
|
+
bytesize=serial.SEVENBITS,
|
|
93
|
+
timeout=self._timeout,
|
|
94
|
+
)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self._logger.error(f"**Error**: {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
# ----------------------------------
|
|
100
|
+
# stream 測定スレッド制御
|
|
101
|
+
self._stream_thread = None
|
|
102
|
+
self._stream_stop_event = threading.Event()
|
|
103
|
+
# ----------------------------------
|
|
104
|
+
|
|
105
|
+
# ----------------------------------
|
|
106
|
+
# gRPC 利用を意識
|
|
107
|
+
# _stream で得た値が式意図を超えたときの結果によってevent発生
|
|
108
|
+
# Event / stream related
|
|
109
|
+
self._lock = threading.Lock()
|
|
110
|
+
|
|
111
|
+
# ----------------------------------
|
|
112
|
+
# 外部通知先 (gRPC Event)
|
|
113
|
+
#
|
|
114
|
+
# 測定クライアント側で
|
|
115
|
+
# AanddReader生成
|
|
116
|
+
# → EventBus生成
|
|
117
|
+
# → reader._set_event_bus(event_bus)
|
|
118
|
+
# → stream_start()
|
|
119
|
+
# → event_arm()
|
|
120
|
+
self._event_bus: Optional[Any] = None
|
|
121
|
+
#
|
|
122
|
+
# eventを使わない場合
|
|
123
|
+
# (1) _event_sub は None
|
|
124
|
+
# (2) stream_start() は呼ばれない
|
|
125
|
+
# (3) event_arm() は呼ばれない
|
|
126
|
+
|
|
127
|
+
# ----------------------------------
|
|
128
|
+
# イベント条件・状態
|
|
129
|
+
# (gRPCに依存してないが実質的にはgRPC-Eventで用いる)
|
|
130
|
+
self._event_topic: Optional[str] = None
|
|
131
|
+
self._event_threshold_g: Optional[float] = None
|
|
132
|
+
self._event_once: bool = True
|
|
133
|
+
self._event_armed: bool = False
|
|
134
|
+
|
|
135
|
+
self._event_above: bool = True
|
|
136
|
+
# ----------------------------------
|
|
137
|
+
|
|
138
|
+
# ----------------------------------
|
|
139
|
+
# トリガ判定用(streamイベント用)
|
|
140
|
+
self._trigger_mode: str = "raw" # raw / mean / median / ema
|
|
141
|
+
self._trigger_window: int = 3
|
|
142
|
+
self._trigger_min_samples: int = 1
|
|
143
|
+
self._trigger_alpha: float = 0.3
|
|
144
|
+
self._trigger_buffer: List[float] = []
|
|
145
|
+
self._trigger_ema: Optional[float] = None
|
|
146
|
+
self._trigger_stable_only: bool = False
|
|
147
|
+
# ----------------------------------
|
|
148
|
+
|
|
149
|
+
def __del__(self):
|
|
150
|
+
try:
|
|
151
|
+
self._close()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
def _close(self) -> None:
|
|
156
|
+
"""
|
|
157
|
+
ストリーム停止とシリアルクローズを確実に行う
|
|
158
|
+
gRPC からもし使うときに gRPC側の close() とかぶるので名前を変えておく
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
self.stream_stop()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
self._ser.close()
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
# ---------------------------
|
|
171
|
+
# Device Info API
|
|
172
|
+
# ---------------------------
|
|
173
|
+
|
|
174
|
+
def status(self) -> Optional[str]:
|
|
175
|
+
return self._status
|
|
176
|
+
|
|
177
|
+
def get_status(self) -> Optional[str]:
|
|
178
|
+
return self._status
|
|
179
|
+
|
|
180
|
+
def unit(self) -> Optional[str]:
|
|
181
|
+
return self._unit
|
|
182
|
+
|
|
183
|
+
def get_unit(self) -> Optional[str]:
|
|
184
|
+
return self._unit
|
|
185
|
+
|
|
186
|
+
def get_weight(self) -> Optional[float]:
|
|
187
|
+
return self._weight
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def is_ack(data: bytes) -> bool:
|
|
191
|
+
return data == b"\x06"
|
|
192
|
+
|
|
193
|
+
def get_serial_number(self):
|
|
194
|
+
"""
|
|
195
|
+
?SN
|
|
196
|
+
マニュアルには \r でも OK とあるが、\r\n が必須
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
self._logger.info(f"[Get Serial Number]")
|
|
202
|
+
self._ser.reset_input_buffer()
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
self._ser.write(b"?SN\r\n")
|
|
206
|
+
line = self._ser.readline()
|
|
207
|
+
self._logger.info(f"[ID raw] {line!r}")
|
|
208
|
+
|
|
209
|
+
# return line.decode(errors="ignore").strip()
|
|
210
|
+
serial_number = ""
|
|
211
|
+
if len(line) == 13 and line != b"":
|
|
212
|
+
line = line.rstrip()
|
|
213
|
+
try:
|
|
214
|
+
serial_number = line[3:11].decode()
|
|
215
|
+
except Exception as e:
|
|
216
|
+
self._logger.error(f"**Error**: {e}")
|
|
217
|
+
return False
|
|
218
|
+
else:
|
|
219
|
+
self._logger.error(line)
|
|
220
|
+
return False
|
|
221
|
+
return serial_number
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
self._logger.error(f"**Error**: {e}")
|
|
225
|
+
return ""
|
|
226
|
+
|
|
227
|
+
def set_meas_mode(self, val: str = "Q"):
|
|
228
|
+
self._meas_mode: str = val
|
|
229
|
+
|
|
230
|
+
def get_meas_mode(self) -> str:
|
|
231
|
+
return self._meas_mode
|
|
232
|
+
|
|
233
|
+
# ---------------------------------
|
|
234
|
+
# Device Ctrl API
|
|
235
|
+
# ---------------------------------
|
|
236
|
+
|
|
237
|
+
def reset(self) -> Union[bool, str]:
|
|
238
|
+
"""reset
|
|
239
|
+
reset 後、測定するには 0.5 s くらい待たないとダメ
|
|
240
|
+
"""
|
|
241
|
+
return self._r()
|
|
242
|
+
|
|
243
|
+
def meas(
|
|
244
|
+
self,
|
|
245
|
+
mode: Optional[str] = None,
|
|
246
|
+
) -> Union[bool, float, str, None]:
|
|
247
|
+
"""
|
|
248
|
+
Q
|
|
249
|
+
即時計測する
|
|
250
|
+
Returns:
|
|
251
|
+
"""
|
|
252
|
+
# --- ストリームが動いているなら、必ず最新のストリーム値を返す ---
|
|
253
|
+
thread_obj = self._stream_thread
|
|
254
|
+
if thread_obj is not None and thread_obj.is_alive():
|
|
255
|
+
# 初回値がまだなら短時間だけ待つ
|
|
256
|
+
wait_sec = self._timeout if self._timeout is not None else 0.5
|
|
257
|
+
t0 = time.time()
|
|
258
|
+
while self._weight is None and (time.time() - t0) < wait_sec:
|
|
259
|
+
time.sleep(0.01)
|
|
260
|
+
return self._weight
|
|
261
|
+
|
|
262
|
+
# --- 通常の Q/S 測定 ---
|
|
263
|
+
mode_val = mode
|
|
264
|
+
if mode_val is None:
|
|
265
|
+
mode_val = self._meas_mode
|
|
266
|
+
|
|
267
|
+
if mode_val.upper() == "Q":
|
|
268
|
+
return self._q()
|
|
269
|
+
|
|
270
|
+
if mode_val.upper() == "S":
|
|
271
|
+
return self._s()
|
|
272
|
+
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
# ---------------------------------
|
|
276
|
+
# Device Ctrl Driver
|
|
277
|
+
# ---------------------------------
|
|
278
|
+
|
|
279
|
+
def _r(
|
|
280
|
+
self,
|
|
281
|
+
) -> Union[str, bool]:
|
|
282
|
+
"""reset
|
|
283
|
+
ACK前提
|
|
284
|
+
|
|
285
|
+
reset 後、測定するには 0.5 s くらい待たないとダメ
|
|
286
|
+
"""
|
|
287
|
+
self._logger.debug(f"[Serial] Reset(): _r")
|
|
288
|
+
if self._ser is None:
|
|
289
|
+
self._logger.error("Serial port is not opened.")
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
self._ser.reset_input_buffer()
|
|
293
|
+
self._ser.write(b"R\r\n")
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
line = self._ser.readline()
|
|
297
|
+
except Exception as e:
|
|
298
|
+
self._logger.error(f"**Error**: {e}")
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
if len(line) == 0:
|
|
302
|
+
self._logger.error("**Error**: No data received.")
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
if line[0] == 0x06:
|
|
306
|
+
return True
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def _s(
|
|
310
|
+
self,
|
|
311
|
+
) -> Union[bool, float]:
|
|
312
|
+
"""
|
|
313
|
+
ACKモード前提
|
|
314
|
+
|
|
315
|
+
Q, 安定後、データ取得
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
"""
|
|
319
|
+
if self._ser is None:
|
|
320
|
+
self._logger.error("Serial port is not opened.")
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
self._ser.reset_input_buffer()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
self._ser.write(b"S\r\n")
|
|
327
|
+
line = self._ser.readline()
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self._logger.error(f"**Error**: {e}")
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
parsed = self._parse_measure_line(line)
|
|
333
|
+
if parsed is None:
|
|
334
|
+
self._logger.error(line)
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
status, weight, unit = parsed
|
|
338
|
+
self._status = status
|
|
339
|
+
self._weight = weight
|
|
340
|
+
self._unit = unit
|
|
341
|
+
return float(weight)
|
|
342
|
+
|
|
343
|
+
def _q(
|
|
344
|
+
self,
|
|
345
|
+
) -> Union[bool, float]:
|
|
346
|
+
"""
|
|
347
|
+
ACKモード前提
|
|
348
|
+
|
|
349
|
+
Q, 即時
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
"""
|
|
353
|
+
if self._ser is None:
|
|
354
|
+
self._logger.error("Serial port is not opened.")
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
self._ser.reset_input_buffer()
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
self._ser.write(b"Q\r\n")
|
|
361
|
+
line = self._ser.readline()
|
|
362
|
+
except Exception as e:
|
|
363
|
+
self._logger.error(f"**Error(readline)**: {e}")
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
if b"EC,E02" in line:
|
|
367
|
+
self._logger.error(f"**Error(EC, E02)**: {line}")
|
|
368
|
+
self._logger.error(
|
|
369
|
+
"計測時間が短くて前回の命令動作を本体足している可能性があります"
|
|
370
|
+
)
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
parsed = self._parse_measure_line(line)
|
|
374
|
+
if parsed is None:
|
|
375
|
+
self._logger.error(f"**Error(len(line)!=17)**: {line}")
|
|
376
|
+
self._logger.error(
|
|
377
|
+
"返信メッセージが正しくない可能性があります。たとえば「空」の可能性です"
|
|
378
|
+
)
|
|
379
|
+
self._logger.error("計測モードなどのパラメーターを疑ってください")
|
|
380
|
+
self._logger.error(f"line: {line}")
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
status, weight, unit = parsed
|
|
384
|
+
self._status = status
|
|
385
|
+
self._weight = weight
|
|
386
|
+
self._unit = unit
|
|
387
|
+
return float(weight)
|
|
388
|
+
|
|
389
|
+
def _stream(self) -> Union[bool, float]:
|
|
390
|
+
"""
|
|
391
|
+
ストリームデータを1行読む
|
|
392
|
+
Q/S や SIR もしくは PRT の各モードも readline に値を書き出す
|
|
393
|
+
ので、どれがどれに対応しているのあわからなくなるので
|
|
394
|
+
|
|
395
|
+
Q/S で使うとき
|
|
396
|
+
SIR で readline を loop で撮り続ける
|
|
397
|
+
|
|
398
|
+
をきちんと区別して使い分けないとおかしいことになる。
|
|
399
|
+
また、PRT の各モードは上記のコマンドベースにキックされて readline に
|
|
400
|
+
流れる以外にPRTの各機能でそれぞれの周期やボタンで readline にデーターを
|
|
401
|
+
流すので Q/S/SIR を使う時は PRT を 0 にしておかないと訳がわからなくなる。
|
|
402
|
+
"""
|
|
403
|
+
if self._ser is None:
|
|
404
|
+
self._logger.error("Serial port is not opened.")
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
line = self._ser.readline()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
self._logger.error(f"**Error**: {e}")
|
|
411
|
+
return False
|
|
412
|
+
|
|
413
|
+
# SIR だとタイムアウトで空行は起こり得るので無視
|
|
414
|
+
if not line:
|
|
415
|
+
# self._logger.error("No data received.")
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
# SIRの失敗は EC,Exx が返る
|
|
419
|
+
if line.startswith(b"EC,"):
|
|
420
|
+
self._logger.error(f"**Error(EC)**: {line!r}")
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
# erCd=1 の場合、各コマンドに ACK(0x06) が返るため無視
|
|
424
|
+
if line == b"\x06":
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
parsed = self._parse_measure_line(line)
|
|
428
|
+
if parsed is None:
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
status, weight, unit = parsed
|
|
432
|
+
self._status = status
|
|
433
|
+
self._weight = weight
|
|
434
|
+
self._unit = unit
|
|
435
|
+
return float(weight)
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _parse_measure_line(
|
|
439
|
+
line: bytes,
|
|
440
|
+
) -> Optional[Tuple[str, float, str]]:
|
|
441
|
+
""""""
|
|
442
|
+
if line is None:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
raw = line.rstrip()
|
|
446
|
+
|
|
447
|
+
# 例: b'ST,+0000.000 g\r\n' を想定
|
|
448
|
+
# 固定長の厳密チェックはしないが、最低限の slicing が成立する長さは必要
|
|
449
|
+
if len(raw) < 15:
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
status = raw[0:2].decode()
|
|
454
|
+
weight = float(raw[3:12].decode())
|
|
455
|
+
unit = raw[12:15].decode().strip()
|
|
456
|
+
except Exception:
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
return status, weight, unit
|
|
460
|
+
|
|
461
|
+
# ----------------------------
|
|
462
|
+
# for Event (gRPC and thread)
|
|
463
|
+
# ----------------------------
|
|
464
|
+
|
|
465
|
+
def _set_event_bus(self, event_bus: Any) -> None:
|
|
466
|
+
"""
|
|
467
|
+
gRPC の event による制御が回るように gRPC のイベントを挿入
|
|
468
|
+
"""
|
|
469
|
+
with self._lock:
|
|
470
|
+
self._event_bus = event_bus
|
|
471
|
+
|
|
472
|
+
def event_set_config(
|
|
473
|
+
self,
|
|
474
|
+
*,
|
|
475
|
+
topic: str,
|
|
476
|
+
threshold_g: float,
|
|
477
|
+
above: bool = True,
|
|
478
|
+
) -> bool:
|
|
479
|
+
"""
|
|
480
|
+
Event 条件の設定だけを行う。
|
|
481
|
+
設定後、event_arm() で armed を有効化する。
|
|
482
|
+
"""
|
|
483
|
+
with self._lock:
|
|
484
|
+
self._event_topic = topic
|
|
485
|
+
self._event_threshold_g = float(threshold_g)
|
|
486
|
+
self._event_above = bool(above)
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
def event_arm(
|
|
490
|
+
self,
|
|
491
|
+
*,
|
|
492
|
+
once: bool = True,
|
|
493
|
+
) -> bool:
|
|
494
|
+
"""
|
|
495
|
+
Event の armed フラグを立てる。
|
|
496
|
+
topic/threshold は event_set_config() で先に設定する。
|
|
497
|
+
|
|
498
|
+
armed を立てていると gRPC のイベントモードになるのでチェックする
|
|
499
|
+
そのときに onece を入れていると、一回だけ閾値超え判定がある。
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
once (bool): イベントは一回のみ
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
bool:
|
|
506
|
+
|
|
507
|
+
"""
|
|
508
|
+
with self._lock:
|
|
509
|
+
if self._event_topic is None:
|
|
510
|
+
return False
|
|
511
|
+
if self._event_threshold_g is None:
|
|
512
|
+
return False
|
|
513
|
+
self._event_once = bool(once)
|
|
514
|
+
self._event_armed = True
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
def event_disarm(self) -> bool:
|
|
518
|
+
with self._lock:
|
|
519
|
+
self._event_armed = False
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
def set_trigger_mode(
|
|
523
|
+
self,
|
|
524
|
+
*,
|
|
525
|
+
mode: str = "raw",
|
|
526
|
+
window: int = 3,
|
|
527
|
+
min_samples: Optional[int] = None,
|
|
528
|
+
alpha: float = 0.3,
|
|
529
|
+
stable_only: bool = False,
|
|
530
|
+
) -> None:
|
|
531
|
+
"""
|
|
532
|
+
streamイベントのトリガ値算出方法を設定する。
|
|
533
|
+
|
|
534
|
+
mode=raw, stable_only=True で値が stable のものだけ閾値チェック
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
mode: raw / mean / median / ema
|
|
538
|
+
window: 平均・中央値で使う窓サイズ
|
|
539
|
+
min_samples: 判定に必要な最小サンプル数(Noneならwindow)
|
|
540
|
+
alpha: EMA係数(0-1)
|
|
541
|
+
stable_only: TrueならST以外は無視
|
|
542
|
+
"""
|
|
543
|
+
with self._lock:
|
|
544
|
+
self._trigger_mode = str(mode).lower()
|
|
545
|
+
w = max(1, int(window))
|
|
546
|
+
self._trigger_window = w
|
|
547
|
+
if min_samples is None:
|
|
548
|
+
self._trigger_min_samples = w
|
|
549
|
+
else:
|
|
550
|
+
self._trigger_min_samples = max(1, int(min_samples))
|
|
551
|
+
self._trigger_alpha = float(alpha)
|
|
552
|
+
self._trigger_buffer = []
|
|
553
|
+
self._trigger_ema = None
|
|
554
|
+
self._trigger_stable_only = bool(stable_only)
|
|
555
|
+
|
|
556
|
+
def _calc_trigger_value(self, weight_val: float) -> Optional[float]:
|
|
557
|
+
"""
|
|
558
|
+
トリガ判定に使う値を計算する。
|
|
559
|
+
None を返した場合は判定をスキップする。
|
|
560
|
+
"""
|
|
561
|
+
# 安定値だけ使う場合
|
|
562
|
+
if self._trigger_stable_only:
|
|
563
|
+
if self._status != "ST":
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
buf = self._trigger_buffer
|
|
567
|
+
buf.append(weight_val)
|
|
568
|
+
if len(buf) > self._trigger_window:
|
|
569
|
+
del buf[0]
|
|
570
|
+
|
|
571
|
+
if len(buf) < self._trigger_min_samples:
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
mode = self._trigger_mode
|
|
575
|
+
|
|
576
|
+
if mode == "raw":
|
|
577
|
+
return weight_val
|
|
578
|
+
|
|
579
|
+
if mode == "mean":
|
|
580
|
+
return sum(buf) / float(len(buf))
|
|
581
|
+
|
|
582
|
+
if mode == "median":
|
|
583
|
+
b = sorted(buf)
|
|
584
|
+
return b[len(b) // 2]
|
|
585
|
+
|
|
586
|
+
if mode == "ema":
|
|
587
|
+
if self._trigger_ema is None:
|
|
588
|
+
self._trigger_ema = weight_val
|
|
589
|
+
else:
|
|
590
|
+
a = self._trigger_alpha
|
|
591
|
+
self._trigger_ema = (a * weight_val) + ((1.0 - a) * self._trigger_ema)
|
|
592
|
+
return self._trigger_ema
|
|
593
|
+
|
|
594
|
+
# 不明なモードは raw と同じ扱い
|
|
595
|
+
return weight_val
|
|
596
|
+
|
|
597
|
+
def stream_start(self) -> bool:
|
|
598
|
+
with self._lock:
|
|
599
|
+
thread_obj = self._stream_thread
|
|
600
|
+
if thread_obj is not None:
|
|
601
|
+
if thread_obj.is_alive():
|
|
602
|
+
return True
|
|
603
|
+
|
|
604
|
+
self._stream_stop_event.clear()
|
|
605
|
+
|
|
606
|
+
self._stream_thread = threading.Thread(
|
|
607
|
+
target=self._stream_loop,
|
|
608
|
+
name="AanddReaderStream",
|
|
609
|
+
daemon=True,
|
|
610
|
+
)
|
|
611
|
+
self._stream_thread.start()
|
|
612
|
+
return True
|
|
613
|
+
|
|
614
|
+
def stream_stop(self) -> bool:
|
|
615
|
+
""""""
|
|
616
|
+
# stream_loop での thread停止をするために thread-event set する
|
|
617
|
+
with self._lock:
|
|
618
|
+
self._stream_stop_event.set()
|
|
619
|
+
|
|
620
|
+
thread_obj: Optional[threading.Thread] = None
|
|
621
|
+
with self._lock:
|
|
622
|
+
thread_obj = self._stream_thread
|
|
623
|
+
|
|
624
|
+
# 念の為 SIRの停止コマンド
|
|
625
|
+
try:
|
|
626
|
+
if self._ser is not None:
|
|
627
|
+
self._ser.write(b"C\r\n")
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
# loop で readline 取得を止める
|
|
632
|
+
if thread_obj is not None:
|
|
633
|
+
try:
|
|
634
|
+
thread_obj.join(timeout=2.0)
|
|
635
|
+
except Exception:
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
with self._lock:
|
|
639
|
+
self._stream_thread = None
|
|
640
|
+
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
def _stream_loop(self) -> None:
|
|
644
|
+
"""
|
|
645
|
+
1. stream_start() でスレッド起動
|
|
646
|
+
2. _stream_loop の先頭で SIR を送る
|
|
647
|
+
3. readline() で連続取得
|
|
648
|
+
4. stream_stop() で停止フラグ
|
|
649
|
+
5. _stream_loop 側で C を送って終了
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
|
|
653
|
+
"""
|
|
654
|
+
# --- SIR 開始 ---
|
|
655
|
+
if self._ser is None:
|
|
656
|
+
self._logger.error("Serial port is not opened.")
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
try:
|
|
660
|
+
self._ser.reset_input_buffer()
|
|
661
|
+
self._ser.write(b"SIR\r\n") # 連続データ要求
|
|
662
|
+
except Exception as e:
|
|
663
|
+
self._logger.error(f"**Error(SIR start)**: {e}")
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
# SIR開始直後の1行目はゴミになることがあるので捨てる
|
|
667
|
+
try:
|
|
668
|
+
_ = self._ser.readline()
|
|
669
|
+
except Exception:
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
while True:
|
|
673
|
+
if self._stream_stop_event.is_set():
|
|
674
|
+
try:
|
|
675
|
+
self._ser.write(b"C\r\n") # SIR 停止
|
|
676
|
+
except Exception:
|
|
677
|
+
pass
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
stream_val = self._stream()
|
|
681
|
+
if isinstance(stream_val, bool):
|
|
682
|
+
time.sleep(0.01)
|
|
683
|
+
continue
|
|
684
|
+
|
|
685
|
+
weight_val = float(stream_val)
|
|
686
|
+
|
|
687
|
+
event_bus: Optional[Any] = None
|
|
688
|
+
topic: Optional[str] = None
|
|
689
|
+
threshold_g: Optional[float] = None
|
|
690
|
+
armed = False
|
|
691
|
+
once = True
|
|
692
|
+
|
|
693
|
+
###################################################
|
|
694
|
+
# トリガーとして raw 値ではなくて計算する
|
|
695
|
+
trigger_val = self._calc_trigger_value(weight_val)
|
|
696
|
+
if trigger_val is None:
|
|
697
|
+
continue
|
|
698
|
+
weight_val = trigger_val
|
|
699
|
+
###################################################
|
|
700
|
+
|
|
701
|
+
with self._lock:
|
|
702
|
+
above = bool(self._event_above)
|
|
703
|
+
event_bus = self._event_bus
|
|
704
|
+
topic = self._event_topic
|
|
705
|
+
threshold_g = self._event_threshold_g
|
|
706
|
+
armed = bool(self._event_armed)
|
|
707
|
+
once = bool(self._event_once)
|
|
708
|
+
|
|
709
|
+
if event_bus is None:
|
|
710
|
+
continue
|
|
711
|
+
if topic is None:
|
|
712
|
+
continue
|
|
713
|
+
if threshold_g is None:
|
|
714
|
+
continue
|
|
715
|
+
|
|
716
|
+
###################################################
|
|
717
|
+
# 閾値チェックを入れるか入れないか?
|
|
718
|
+
# gRPC-event 入れるか?入れないか?
|
|
719
|
+
if armed is False:
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
# 閾値より上と下を選べる
|
|
723
|
+
# if weight_val >= float(threshold_g):
|
|
724
|
+
if (above and weight_val >= float(threshold_g)) or (
|
|
725
|
+
(not above) and weight_val <= float(threshold_g)
|
|
726
|
+
):
|
|
727
|
+
###################################################
|
|
728
|
+
payload: Dict[str, Any] = {
|
|
729
|
+
"weight_g": weight_val,
|
|
730
|
+
"threshold_g": float(threshold_g),
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
event_bus.publish(topic, payload)
|
|
735
|
+
except Exception:
|
|
736
|
+
pass
|
|
737
|
+
|
|
738
|
+
# 閾値判定は一回だけ発火(発火後のみどす)
|
|
739
|
+
if once:
|
|
740
|
+
with self._lock:
|
|
741
|
+
self._event_armed = False
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
if __name__ == "__main__":
|
|
745
|
+
from x_logger import XLogger
|
|
746
|
+
|
|
747
|
+
# log_level_ = "info"
|
|
748
|
+
log_level_ = "debug"
|
|
749
|
+
logger_ = XLogger(
|
|
750
|
+
log_level=log_level_,
|
|
751
|
+
logger_name="AandDReader",
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
aandd_reader = AanddReader(
|
|
755
|
+
port="COM3",
|
|
756
|
+
# port="COM4",
|
|
757
|
+
logger=logger_,
|
|
758
|
+
log_level=log_level_,
|
|
759
|
+
)
|
|
760
|
+
# aandd_reader.cal()
|
|
761
|
+
logger_.info(f"SN: {aandd_reader.get_serial_number()}")
|
|
762
|
+
time.sleep(0.5)
|
|
763
|
+
logger_.info(f"{aandd_reader.meas()}")
|
|
764
|
+
# print(aandd_reader.status())
|
|
765
|
+
# print(aandd_reader.unit())
|