pymscada 0.2.0__py3-none-any.whl → 0.2.6b9__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.
Files changed (39) hide show
  1. pymscada/__init__.py +8 -2
  2. pymscada/alarms.py +179 -60
  3. pymscada/bus_client.py +12 -2
  4. pymscada/bus_server.py +18 -9
  5. pymscada/callout.py +198 -101
  6. pymscada/config.py +20 -1
  7. pymscada/console.py +19 -6
  8. pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
  9. pymscada/demo/callout.yaml +13 -4
  10. pymscada/demo/files.yaml +3 -2
  11. pymscada/demo/openweather.yaml +3 -11
  12. pymscada/demo/piapi.yaml +15 -0
  13. pymscada/demo/pymscada-io-piapi.service +15 -0
  14. pymscada/demo/pymscada-io-sms.service +18 -0
  15. pymscada/demo/sms.yaml +11 -0
  16. pymscada/demo/tags.yaml +3 -0
  17. pymscada/demo/witsapi.yaml +6 -8
  18. pymscada/demo/wwwserver.yaml +15 -0
  19. pymscada/files.py +1 -0
  20. pymscada/history.py +4 -5
  21. pymscada/iodrivers/logix_map.py +1 -1
  22. pymscada/iodrivers/modbus_client.py +189 -21
  23. pymscada/iodrivers/modbus_map.py +17 -2
  24. pymscada/iodrivers/piapi.py +133 -0
  25. pymscada/iodrivers/sms.py +212 -0
  26. pymscada/iodrivers/witsapi.py +26 -35
  27. pymscada/module_config.py +24 -18
  28. pymscada/opnotes.py +38 -16
  29. pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
  30. pymscada/tag.py +6 -7
  31. pymscada/tools/get_history.py +147 -0
  32. pymscada/www_server.py +2 -1
  33. {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/METADATA +2 -2
  34. {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/RECORD +38 -32
  35. pymscada/validate.py +0 -451
  36. {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/WHEEL +0 -0
  37. {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/entry_points.txt +0 -0
  38. {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/licenses/LICENSE +0 -0
  39. {pymscada-0.2.0.dist-info → pymscada-0.2.6b9.dist-info}/top_level.txt +0 -0
pymscada/__init__.py CHANGED
@@ -2,25 +2,31 @@
2
2
  from pymscada.bus_client import BusClient
3
3
  from pymscada.bus_server import BusServer
4
4
  from pymscada.config import Config
5
+ from pymscada.callout import Callout, ALM
5
6
  from pymscada.iodrivers.accuweather import AccuWeatherClient
6
7
  from pymscada.iodrivers.logix_client import LogixClient
7
8
  from pymscada.iodrivers.modbus_client import ModbusClient
8
9
  from pymscada.iodrivers.modbus_server import ModbusServer
10
+ from pymscada.iodrivers.piapi import PIWebAPIClient
11
+ from pymscada.iodrivers.sms import SMS
12
+ from pymscada.iodrivers.witsapi import WitsAPIClient
9
13
  from pymscada.misc import find_nodes, ramp
10
14
  from pymscada.periodic import Periodic
11
15
  from pymscada.tag import Tag
12
- from pymscada.validate import validate
13
16
 
14
17
  __all__ = [
15
18
  'BusClient',
16
19
  'BusServer',
17
20
  'Config',
21
+ 'Callout', 'ALM',
18
22
  'AccuWeatherClient',
19
23
  'LogixClient',
20
24
  'ModbusClient',
21
25
  'ModbusServer',
26
+ 'PIWebAPIClient',
27
+ 'SMS',
28
+ 'WitsAPIClient',
22
29
  'find_nodes', 'ramp',
23
30
  'Periodic',
24
31
  'Tag',
25
- 'validate',
26
32
  ]
pymscada/alarms.py CHANGED
@@ -11,12 +11,14 @@ ALM = 0
11
11
  RTN = 1
12
12
  ACT = 2
13
13
  INF = 3
14
+ TIMING = 4
14
15
  KIND = {
15
16
  ALM: 'ALM',
16
17
  RTN: 'RTN',
17
18
  ACT: 'ACT',
18
19
  INF: 'INF'
19
20
  }
21
+
20
22
  NORMAL = 0
21
23
  ALARM = 1
22
24
 
@@ -74,12 +76,11 @@ def standardise_tag_info(tagname: str, tag: dict):
74
76
  def split_operator(alarm: str) -> dict:
75
77
  """Split alarm string into operator and value."""
76
78
  tokens = alarm.split(' ')
77
- alm_dict = {'for': 0}
78
79
  if len(tokens) not in (2, 4):
79
80
  raise ValueError(f"Invalid alarm {alarm}")
80
81
  if tokens[0] not in ['>', '<', '==', '>=', '<=']:
81
82
  raise ValueError(f"Invalid alarm {alarm}")
82
- alm_dict['operator'] = tokens[0]
83
+ alm_dict = {'for': 0, 'operator': tokens[0], 'value': None}
83
84
  try:
84
85
  alm_dict['value'] = float(tokens[1])
85
86
  except ValueError:
@@ -102,10 +103,11 @@ class Alarm():
102
103
  conditions, each combination of tag and condition is a separate Alarm.
103
104
 
104
105
  Monitors tag value through the Tag callback. Tracks in alarm state.
105
- Generates the ALM and RTN messages for Alarms to publish via rta_tag.
106
+ Notifies Alarms of state changes via state callback.
106
107
  """
107
108
 
108
- def __init__(self, tagname: str, tag: dict, alarm: str, group: str, rta_cb, alarms) -> None:
109
+ def __init__(self, tagname: str, tag: dict, alarm: str, group: str,
110
+ state_cb) -> None:
109
111
  """Initialize alarm with tag and condition(s)."""
110
112
  self.alarm_id = f'{tagname} {alarm}'
111
113
  self.tag = Tag(tagname, tag['type'])
@@ -114,18 +116,18 @@ class Alarm():
114
116
  self.tag.units = tag['units']
115
117
  self.tag.add_callback(self.callback)
116
118
  self.group = group
117
- self.rta_cb = rta_cb
118
- self.alarms = alarms
119
+ self.state_cb = state_cb
120
+ self.timing_value = None
121
+ self.timing_us = None
119
122
  self.alarm = split_operator(alarm)
120
123
  self.in_alarm = False
121
- self.checking = False
124
+ self.disabled_until = 0
122
125
 
123
126
  def callback(self, tag: Tag):
124
127
  """Handle tag value changes and generate ALM/RTN messages."""
125
- if tag.value is None:
128
+ if tag.value is None or tag.time_us < self.disabled_until:
126
129
  return
127
130
  value = float(tag.value)
128
- time_us = tag.time_us
129
131
  new_in_alarm = False
130
132
  op = self.alarm['operator']
131
133
  if op == '>':
@@ -140,39 +142,25 @@ class Alarm():
140
142
  new_in_alarm = value <= self.alarm['value']
141
143
  if new_in_alarm == self.in_alarm:
142
144
  return
143
- self.in_alarm = new_in_alarm
144
- if self.in_alarm:
145
+ if new_in_alarm:
145
146
  if self.alarm['for'] > 0:
146
- if not self.checking:
147
- self.checking = True
148
- self.alarms.checking_alarms.append(self)
147
+ self.state_cb(self, TIMING)
148
+ self.timing_us = tag.time_us
149
+ self.timing_value = value
149
150
  else:
150
- self.generate_alarm(ALM, time_us, value)
151
+ self.state_cb(self, ALM)
151
152
  else:
152
- if self.checking:
153
- self.checking = False
154
- self.alarms.checking_alarms.remove(self)
155
- self.generate_alarm(RTN, time_us, value)
156
-
157
- def generate_alarm(self, kind: int, time_us: int, value: float):
158
- """Generate alarm message."""
159
- logging.warning(f'Alarm {self.alarm_id} {value} {KIND[kind]}')
160
- self.rta_cb({
161
- 'action': 'ADD',
162
- 'date_ms': int(time_us / 1000),
163
- 'alarm_string': self.alarm_id,
164
- 'kind': kind,
165
- 'desc': f'{self.tag.desc} {value:.{self.tag.dp}f}'
166
- f' {self.tag.units}',
167
- 'group': self.group
168
- })
153
+ if self.timing_us is not None:
154
+ if tag.time_us - self.timing_us >= self.alarm['for'] * 1000000:
155
+ self.state_cb(self, ALM)
156
+ self.timing_us = None
157
+ self.state_cb(self, RTN)
158
+ self.in_alarm = new_in_alarm
169
159
 
170
160
  def check_duration(self, current_time_us: int):
171
161
  """Check if alarm condition has been met for required duration."""
172
162
  if current_time_us - self.tag.time_us >= self.alarm['for'] * 1000000:
173
- self.generate_alarm(ALM, current_time_us, self.tag.value)
174
- self.checking = False
175
- self.alarms.checking_alarms.remove(self)
163
+ self.state_cb(self, ALM)
176
164
 
177
165
 
178
166
  class Alarms:
@@ -218,19 +206,20 @@ class Alarms:
218
206
  logging.warning(f'Alarms {bus_ip} {bus_port} {db} {rta_tag}')
219
207
  self.alarms: list[Alarm] = []
220
208
  self.checking_alarms: list[Alarm] = []
209
+ self.in_alarm: list[Alarm] = []
221
210
  for tagname, tag in tag_info.items():
222
211
  standardise_tag_info(tagname, tag)
223
212
  if 'alarm' not in tag or tag['type'] not in (int, float):
224
213
  continue
225
214
  group = tag['group']
226
215
  for alarm in tag['alarm']:
227
- new_alarm = Alarm(tagname, tag, alarm, group, self.rta_cb,
228
- self)
216
+ new_alarm = Alarm(tagname, tag, alarm, group, self.state_cb)
229
217
  self.alarms.append(new_alarm)
230
218
  self.busclient = BusClient(bus_ip, bus_port, module='Alarms')
231
219
  self.rta = Tag(rta_tag, dict)
232
- self.rta.value = {}
220
+ self.rta.value = {'__rta_id__': 0}
233
221
  self.busclient.add_callback_rta(rta_tag, self.rta_cb)
222
+ self.busclient.add_tag(self.rta)
234
223
  self._init_db(db, table)
235
224
  self.periodic = Periodic(self.periodic_cb, 1.0)
236
225
 
@@ -239,6 +228,14 @@ class Alarms:
239
228
  self.connection = sqlite3.connect(db)
240
229
  self.table = table
241
230
  self.cursor = self.connection.cursor()
231
+ sqlite_version = sqlite3.sqlite_version_info
232
+ self.has_returning = sqlite_version >= (3, 35, 0)
233
+ if not self.has_returning:
234
+ logging.warning(
235
+ f'SQLite {sqlite3.sqlite_version} is older than 3.35.0. '
236
+ f'RETURNING clause not supported, using fallback method. '
237
+ f'Consider upgrading SQLite for better performance.'
238
+ )
242
239
  query = (
243
240
  'CREATE TABLE IF NOT EXISTS ' + self.table + ' '
244
241
  '(id INTEGER PRIMARY KEY ASC, '
@@ -250,7 +247,6 @@ class Alarms:
250
247
  )
251
248
  self.cursor.execute(query)
252
249
  self.connection.commit()
253
-
254
250
  startup_record = {
255
251
  'action': 'ADD',
256
252
  'date_ms': int(time.time() * 1000),
@@ -267,6 +263,39 @@ class Alarms:
267
263
  for alarm in self.checking_alarms[:]:
268
264
  alarm.check_duration(current_time_us)
269
265
 
266
+ def state_cb(self, alarm: Alarm, state: int):
267
+ """Handle alarm state changes."""
268
+ if state == TIMING:
269
+ self.checking_alarms.append(alarm)
270
+ elif state == ALM:
271
+ if alarm in self.checking_alarms:
272
+ self.checking_alarms.remove(alarm)
273
+ self.in_alarm.append(alarm)
274
+ self.generate_alarm(alarm, ALM)
275
+ elif state == RTN:
276
+ if alarm in self.checking_alarms:
277
+ self.checking_alarms.remove(alarm)
278
+ if alarm in self.in_alarm:
279
+ self.in_alarm.remove(alarm)
280
+ self.generate_alarm(alarm, RTN)
281
+
282
+ def generate_alarm(self, alarm: Alarm, kind: int):
283
+ """Generate alarm message."""
284
+ value = alarm.tag.value
285
+ if alarm.timing_us is not None:
286
+ value = alarm.timing_value
287
+ time_us = alarm.tag.time_us
288
+ logging.warning(f'Alarm {alarm.alarm_id} {value} {KIND[kind]}')
289
+ self.rta_cb({
290
+ 'action': 'ADD',
291
+ 'date_ms': int(time_us / 1000),
292
+ 'alarm_string': alarm.alarm_id,
293
+ 'kind': kind,
294
+ 'desc': f'{alarm.tag.desc} {value:.{alarm.tag.dp}f}'
295
+ f' {alarm.tag.units}',
296
+ 'group': alarm.group
297
+ })
298
+
270
299
  def rta_cb(self, request):
271
300
  """Respond to Request to Author and publish on rta_tag as needed."""
272
301
  if 'action' not in request:
@@ -275,14 +304,27 @@ class Alarms:
275
304
  try:
276
305
  logging.info(f'add {request}')
277
306
  with self.connection:
278
- self.cursor.execute(
279
- f'INSERT INTO {self.table} '
280
- '(date_ms, alarm_string, kind, desc, "group") '
281
- 'VALUES(:date_ms, :alarm_string, :kind, :desc, :group) '
282
- 'RETURNING *;',
283
- request)
284
- res = self.cursor.fetchone()
307
+ if self.has_returning:
308
+ self.cursor.execute(
309
+ f'INSERT INTO {self.table} '
310
+ '(date_ms, alarm_string, kind, desc, "group") '
311
+ 'VALUES(:date_ms, :alarm_string, :kind, :desc, :group) '
312
+ 'RETURNING *;',
313
+ request)
314
+ res = self.cursor.fetchone()
315
+ else:
316
+ self.cursor.execute(
317
+ f'INSERT INTO {self.table} '
318
+ '(date_ms, alarm_string, kind, desc, "group") '
319
+ 'VALUES(:date_ms, :alarm_string, :kind, :desc, :group);',
320
+ request)
321
+ row_id = self.cursor.lastrowid
322
+ self.cursor.execute(
323
+ f'SELECT * FROM {self.table} WHERE id = ?;',
324
+ (row_id,))
325
+ res = self.cursor.fetchone()
285
326
  self.rta.value = {
327
+ '__rta_id__': 0,
286
328
  'id': res[0],
287
329
  'date_ms': res[1],
288
330
  'alarm_string': res[2],
@@ -296,20 +338,42 @@ class Alarms:
296
338
  try:
297
339
  logging.info(f'update {request}')
298
340
  with self.connection:
299
- self.cursor.execute(
300
- f'UPDATE {self.table} SET in_alm = :in_alm '
301
- 'WHERE id = :id RETURNING *;',
302
- request)
303
- res = self.cursor.fetchone()
304
- if res:
305
- self.rta.value = {
306
- 'id': res[0],
307
- 'date_ms': res[1],
308
- 'alarm_string': res[2],
309
- 'kind': res[3],
310
- 'desc': res[4],
311
- 'group': res[5]
312
- }
341
+ if self.has_returning:
342
+ self.cursor.execute(
343
+ f'UPDATE {self.table} SET in_alm = :in_alm '
344
+ 'WHERE id = :id RETURNING *;',
345
+ request)
346
+ res = self.cursor.fetchone()
347
+ if res:
348
+ self.rta.value = {
349
+ '__rta_id__': 0,
350
+ 'id': res[0],
351
+ 'date_ms': res[1],
352
+ 'alarm_string': res[2],
353
+ 'kind': res[3],
354
+ 'desc': res[4],
355
+ 'group': res[5]
356
+ }
357
+ else:
358
+ self.cursor.execute(
359
+ f'UPDATE {self.table} SET in_alm = :in_alm '
360
+ 'WHERE id = :id;',
361
+ request)
362
+ if self.cursor.rowcount > 0:
363
+ self.cursor.execute(
364
+ f'SELECT * FROM {self.table} WHERE id = :id;',
365
+ request)
366
+ res = self.cursor.fetchone()
367
+ if res:
368
+ self.rta.value = {
369
+ '__rta_id__': 0,
370
+ 'id': res[0],
371
+ 'date_ms': res[1],
372
+ 'alarm_string': res[2],
373
+ 'kind': res[3],
374
+ 'desc': res[4],
375
+ 'group': res[5]
376
+ }
313
377
  except sqlite3.IntegrityError as error:
314
378
  logging.warning(f'Alarms rta_cb update {error}')
315
379
  elif request['action'] == 'HISTORY':
@@ -346,6 +410,61 @@ class Alarms:
346
410
  elif request['action'] == 'IN ALARM':
347
411
  self.rta.value = {'__rta_id__': request['__rta_id__'],
348
412
  'data': {'in_alarm': list(self.in_alarm)}}
413
+ elif request['action'] == 'ENABLE':
414
+ time_us = int(time.time() * 1000000)
415
+ local_time = time.localtime(time_us / 1000000)
416
+ for alarm in self.alarms:
417
+ if alarm.alarm_id == request['alarm id']:
418
+ enable = request['enable']
419
+ if enable == 'Enable':
420
+ disabled_until_us = 0
421
+ else:
422
+ if enable == 'Disable until 8am':
423
+ target_hour = 8
424
+ target_day_offset = 0
425
+ next_offset = 1
426
+ elif enable == 'Disable until 4pm':
427
+ target_hour = 16
428
+ target_day_offset = 0
429
+ next_offset = 1
430
+ elif enable == 'Disable until Monday 8am':
431
+ target_hour = 8
432
+ target_day_offset = (0 - local_time.tm_wday) % 7
433
+ next_offset = 7
434
+ else:
435
+ disabled_until_us = 0
436
+ break
437
+ target_s = time.mktime((
438
+ local_time.tm_year, local_time.tm_mon,
439
+ local_time.tm_mday + target_day_offset,
440
+ target_hour, 0, 0, 0, 0, -1
441
+ ))
442
+ if target_s * 1000000 <= time_us:
443
+ target_s = time.mktime((
444
+ local_time.tm_year, local_time.tm_mon,
445
+ local_time.tm_mday + next_offset,
446
+ target_hour, 0, 0, 0, 0, -1
447
+ ))
448
+ disabled_until_us = int(target_s * 1000000)
449
+ alarm.disabled_until = disabled_until_us
450
+ ts = time.strftime(
451
+ "%Y-%m-%d %H:%M:%S",
452
+ time.localtime(disabled_until_us / 1000000)
453
+ )
454
+ if disabled_until_us == 0:
455
+ desc = 'Enable'
456
+ else:
457
+ desc = f'Disable until {ts}'
458
+ self.rta_cb({
459
+ 'action': 'ADD',
460
+ 'date_ms': int(time_us / 1000),
461
+ 'alarm_string': alarm.alarm_id,
462
+ 'kind': ACT,
463
+ 'desc': desc,
464
+ 'group': alarm.group
465
+ })
466
+ break
467
+
349
468
 
350
469
  async def start(self):
351
470
  """Async startup."""
pymscada/bus_client.py CHANGED
@@ -84,6 +84,12 @@ class BusClient:
84
84
  """Write a message."""
85
85
  if data is None:
86
86
  data = b''
87
+ try:
88
+ size_total = len(data)
89
+ except Exception:
90
+ size_total = 0
91
+ logging.info(f'{self.module}: write cmd={command} tag_id={tag_id} '
92
+ f'size_total={size_total}')
87
93
  for i in range(0, len(data) + 1, pc.MAX_LEN):
88
94
  snip = data[i:i+pc.MAX_LEN]
89
95
  size = len(snip)
@@ -94,7 +100,9 @@ class BusClient:
94
100
  except (asyncio.IncompleteReadError, ConnectionResetError):
95
101
  self.read_task.cancel()
96
102
  except AttributeError:
97
- logging.warning('Attribute Error, TO DO, fix in test')
103
+ logging.warning(f'{self.module}: write AttributeError '
104
+ f'cmd={command} '
105
+ f'tag_id={tag_id} size={size}')
98
106
 
99
107
  def add_tag(self, tag: Tag):
100
108
  """Add the new tag and get the tag's bus ID."""
@@ -209,10 +217,12 @@ class BusClient:
209
217
  data = struct.unpack_from(f'!{len(value) - 1}s', value, offset=1
210
218
  )[0].decode()
211
219
  data = json.loads(data)
220
+ logging.info(f'{self.module}: RTA received {tag.name} {data} '
221
+ f'from tag_id {tag_id}')
212
222
  try:
213
223
  self.rta_handlers[tag.name](data)
214
224
  except KeyError:
215
- logging.warning(f'unhandled RTA for {tag.name} {data}')
225
+ logging.warning(f'{self.module}: unhandled RTA for {tag.name} {data}')
216
226
  else:
217
227
  raise SystemExit(f'Invalid message {cmd}')
218
228
 
pymscada/bus_server.py CHANGED
@@ -18,8 +18,7 @@ class BusTags(type):
18
18
  """Return existing tag if tagname already exists."""
19
19
  if tagname in cls._tag_by_name:
20
20
  return cls._tag_by_name[tagname]
21
- tag: 'BusTag' = cls.__new__(cls, tagname)
22
- tag.__init__(tagname)
21
+ tag: 'BusTag' = super().__call__(tagname)
23
22
  tag.id = cls._next_id
24
23
  cls._next_id += 1
25
24
  cls._tag_by_name[tagname] = tag
@@ -35,10 +34,10 @@ class BusTag(metaclass=BusTags):
35
34
  def __init__(self, name: bytes):
36
35
  """Name and id must be unique."""
37
36
  self.name = name
38
- self.id = None
37
+ self.id = 0
39
38
  self.time_us: int = 0
40
39
  self.value: bytes = b''
41
- self.from_bus: 'BusConnection' = None
40
+ self.from_bus: int = 0
42
41
  self.pub = []
43
42
 
44
43
  def add_callback(self, callback, bus_id):
@@ -51,7 +50,7 @@ class BusTag(metaclass=BusTags):
51
50
  if (callback, bus_id) in self.pub:
52
51
  self.pub.remove((callback, bus_id))
53
52
 
54
- def update(self, data: bytes, time_us: int, from_bus: 'BusConnection'):
53
+ def update(self, data: bytes, time_us: int, from_bus: int):
55
54
  """Assign value and update subscribers."""
56
55
  self.value = data
57
56
  self.time_us = time_us
@@ -111,7 +110,8 @@ class BusConnection():
111
110
  head = await self.reader.readexactly(14)
112
111
  _, cmd, tag_id, size, time_us = unpack('!BBHHQ', head)
113
112
  except (ConnectionResetError, asyncio.IncompleteReadError,
114
- asyncio.CancelledError):
113
+ asyncio.CancelledError) as e:
114
+ logging.warning(f'{self.addr} read error: {e}')
115
115
  break
116
116
  # if the command packet indicates data, get that too
117
117
  if size == 0:
@@ -121,7 +121,8 @@ class BusConnection():
121
121
  payload = await self.reader.readexactly(size)
122
122
  data = unpack(f'!{size}s', payload)[0]
123
123
  except (ConnectionResetError, asyncio.IncompleteReadError,
124
- asyncio.CancelledError):
124
+ asyncio.CancelledError) as e:
125
+ logging.warning(f'{self.addr} read payload error: {e}')
125
126
  break
126
127
  # if MAX_LEN then a continuation packet is required
127
128
  if size == pc.MAX_LEN:
@@ -201,15 +202,21 @@ class BusServer:
201
202
  try:
202
203
  tag = BusTags._tag_by_id[tag_id]
203
204
  except KeyError:
205
+ logging.warning(f'RTA KeyError {tag_id}')
204
206
  self.connections[bus_id].write(
205
207
  pc.COMMAND.ERR, tag_id, time_us,
206
208
  f"RTA KeyError {tag_id}".encode())
209
+ return
207
210
  try:
211
+ logging.info(f'RTA forwarding {tag.name} from_bus={tag.from_bus} '
212
+ f'to bus_id={tag.from_bus}')
208
213
  self.connections[tag.from_bus].write(
209
214
  pc.COMMAND.RTA, tag_id, tag.time_us, data)
210
215
  except KeyError:
211
- logging.warning(f'likely busclient for {tag.name} is gone')
216
+ logging.warning(f'RTA forwarding failed: busclient for '
217
+ f'{tag.name} (from_bus={tag.from_bus}) is gone')
212
218
  except Exception as e:
219
+ logging.warning(f'RTA forwarding error {tag.name}: {e}')
213
220
  self.connections[bus_id].write(
214
221
  pc.COMMAND.ERR, tag_id, time_us,
215
222
  f"RTA {tag_id} {e}".encode())
@@ -283,11 +290,13 @@ class BusServer:
283
290
  self.bus_tag.value = f'\x03{client_addr}: {log_msg}'.encode()
284
291
  self.bus_tag.time_us = int(time.time() * 1e6)
285
292
  else: # consider disconnecting
286
- logging.warn(f'invalid message {cmd}')
293
+ logging.warning(f'invalid message {cmd}')
287
294
 
288
295
  def read_callback(self, command):
289
296
  """Process read messages, delete broken connections."""
290
297
  bus_id, cmd, tag_id, time_us, data = command
298
+ logging.info(f'recv cmd={cmd} tag_id={tag_id} bus_id={bus_id} '
299
+ f'size={(0 if data is None else len(data))}')
291
300
  if cmd is None:
292
301
  # Clean up tag subscriptions before deleting it
293
302
  for tag in BusTags._tag_by_id.values():