Skip to content

Commit b86337a

Browse files
committed
Start autosave thread when in dispatcher init
1 parent 60d6742 commit b86337a

File tree

5 files changed

+82
-29
lines changed

5 files changed

+82
-29
lines changed

softioc/asyncio_dispatcher.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import threading
55
import atexit
66
import signal
7+
from . import autosave
78

89
class AsyncioDispatcher:
910
def __init__(self, loop=None, debug=False):
@@ -41,6 +42,13 @@ def __init__(self, loop=None, debug=False):
4142
raise ValueError("Provided asyncio event loop is not running")
4243
else:
4344
self.loop = loop
45+
# set up autosave thread
46+
autosaver = autosave.Autosave()
47+
self.__autosave_worker = threading.Thread(
48+
target=autosaver.loop,
49+
)
50+
self.__autosave_worker.daemon = True
51+
self.__autosave_worker.start()
4452

4553
def close(self):
4654
if self.__atexit is not None:

softioc/autosave.py

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,66 @@
11
import json
22
from pathlib import Path
33
from typing import Dict, List, Optional
4-
from softioc.pythonSoftIoc import RecordWrapper
54
from datetime import datetime
65
import shutil
7-
from softioc import builder
6+
from softioc.device_core import LookupRecordList
7+
import time
8+
import threading
89

910
SAV_SUFFIX = "softsav"
1011
SAVB_SUFFIX = "softsavB"
1112

13+
def configure(device=None, directory=None, save_period=None, poll_period=None):
14+
Autosave.poll_period = poll_period or Autosave.poll_period
15+
Autosave.save_period = save_period or Autosave.save_period
16+
if device is None and Autosave.device_name is None:
17+
from .builder import GetRecordNames
18+
Autosave.device_name = GetRecordNames().prefix[0]
19+
else:
20+
Autosave.device_name = device
21+
if directory is None and Autosave.directory is None:
22+
raise RuntimeError("Autosave directory is not known, "
23+
"call autosave.configure() with directory keyword argument")
24+
else:
25+
Autosave.directory = Path(directory)
1226

1327
class Autosave:
28+
_instance = None
29+
poll_period = 1.0
30+
save_period = 30.0
31+
device_name = None
32+
directory = None
33+
enabled = True
34+
backup_on_restart = True
35+
1436
def __init__(
1537
self,
16-
directory: str,
17-
pvs: List[RecordWrapper],
18-
save_period: float = 30.0,
19-
enabled: bool = True,
20-
backup_on_restart: bool = True
2138
):
22-
self._device_name: str = builder.GetRecordNames().prefix[0]
23-
self._directory: Path = Path(directory) # cast string to Path
39+
if not self.directory:
40+
raise RuntimeError("Autosave directory is not known, "
41+
"call autosave.configure() with directory keyword argument")
42+
if not self.device_name:
43+
raise RuntimeError("Device name is not known to autosave thread, "
44+
"call autosave.configure() with device keyword argument")
2445
self._last_saved_time = datetime.now()
25-
if not self._directory.is_dir():
46+
if not self.directory.is_dir():
2647
raise RuntimeError(f"{directory} is not a valid autosave directory")
27-
if backup_on_restart:
48+
if self.backup_on_restart:
2849
self.backup_sav_file()
29-
self._enabled = enabled
30-
self._save_period = save_period
31-
self._pvs = {pv.name: pv for pv in pvs}
32-
self._state: Dict[str, RecordWrapper] = {}
50+
self.get_autosave_pvs()
51+
self._state = {}
3352
self._last_saved_state = {}
53+
self._started = False
54+
if self.enabled:
55+
self.load() # load at startup if enabled
56+
57+
def get_autosave_pvs(self):
58+
self._pvs = {name: pv for name, pv in LookupRecordList() if pv.autosave}
3459

3560
def _change_directory(self, directory: str):
3661
dir_path = Path(directory)
3762
if dir_path.is_dir():
38-
self._directory = dir_path
63+
self.directory = dir_path
3964
else:
4065
raise ValueError(f"{directory} is not a valid autosave directory")
4166

@@ -44,27 +69,29 @@ def backup_sav_file(self):
4469
if sav_path.is_file():
4570
shutil.copy2(sav_path, self._get_timestamped_backup_sav_path())
4671

47-
def add_pv(self, pv: RecordWrapper):
72+
def add_pv(self, pv):
73+
pv.autosave = True
4874
self._pvs[pv.name] = pv
4975

50-
def remove_pv(self, pv: RecordWrapper):
76+
def remove_pv(self, pv):
77+
pv.autosave = False
5178
self._pvs.pop(pv.name, None)
5279

5380
def _get_state_from_device(self):
5481
for name, pv in self._pvs.items():
5582
self._state[name] = pv.get()
5683

57-
def _get_timestamped_backup_sav_path(self) -> Path:
84+
def _get_timestamped_backup_sav_path(self):
5885
sav_path = self._get_current_sav_path()
5986
return sav_path.parent / (
6087
sav_path.name + self._last_saved_time.strftime("_%y%m%d-%H%M%S")
6188
)
6289

63-
def _get_backup_save_path(self) -> Path:
64-
return self._directory / f"{self._device_name}.{SAVB_SUFFIX}"
90+
def _get_backup_save_path(self):
91+
return self.directory / f"{self.device_name}.{SAVB_SUFFIX}"
6592

66-
def _get_current_sav_path(self) -> Path:
67-
return self._directory / f"{self._device_name}.{SAV_SUFFIX}"
93+
def _get_current_sav_path(self):
94+
return self.directory / f"{self.device_name}.{SAV_SUFFIX}"
6895

6996
def _update_last_saved(self):
7097
self._last_saved_state = self._state.copy()
@@ -80,19 +107,19 @@ def _save(self):
80107
print(f"Could not save state to file: {e}")
81108

82109
def save(self):
83-
if not self._enabled:
110+
if not self.enabled:
84111
print("Not saving to file as autosave adapter disabled")
85112
return
86113
timenow = datetime.now()
87114
self._get_state_from_device()
88115
if (
89-
(timenow - self._last_saved_time).total_seconds() > self._save_period
116+
(timenow - self._last_saved_time).total_seconds() > self.save_period
90117
and self._state != self._last_saved_state # only save if changed
91118
):
92119
self._save()
93120

94-
def load(self, path: Optional[str] = None):
95-
if not self._enabled:
121+
def load(self, path = None):
122+
if not self.enabled:
96123
print("Not loading from file as autosave adapter disabled")
97124
return
98125
sav_path = path or self._get_current_sav_path()
@@ -108,3 +135,10 @@ def load(self, path: Optional[str] = None):
108135
continue
109136
pv.set(value)
110137
self._get_state_from_device()
138+
139+
def loop(self):
140+
if not self._pvs:
141+
return # end thread if no PVs to save
142+
while True:
143+
time.sleep(self.poll_period)
144+
self.save()

softioc/cothread_dispatcher.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
1+
import threading
2+
from . import autosave
23
class CothreadDispatcher:
34
def __init__(self, dispatcher = None):
45
"""A dispatcher for `cothread` based IOCs, suitable to be passed to
@@ -20,6 +21,13 @@ def __init__(self, dispatcher = None):
2021
self.__dispatcher = dispatcher
2122

2223
self.wait_for_quit = cothread.WaitForQuit
24+
# set up autosave thread
25+
autosaver = autosave.Autosave()
26+
self.__autosave_worker = threading.Thread(
27+
target=autosaver.loop,
28+
)
29+
self.__autosave_worker.daemon = True
30+
self.__autosave_worker.start()
2331

2432
def __call__(
2533
self,

softioc/device.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class ProcessDeviceSupportIn(ProcessDeviceSupportCore):
109109
_link_ = 'INP'
110110

111111
def __init__(self, name, **kargs):
112+
self.autosave = kargs.pop("autosave", False)
112113
if 'initial_value' in kargs:
113114
value = self._value_to_epics(kargs.pop('initial_value'))
114115
else:
@@ -159,6 +160,7 @@ class ProcessDeviceSupportOut(ProcessDeviceSupportCore):
159160
_link_ = 'OUT'
160161

161162
def __init__(self, name, **kargs):
163+
self.autosave = kargs.pop('autosave', False)
162164
on_update = kargs.pop('on_update', None)
163165
on_update_name = kargs.pop('on_update_name', None)
164166
# At most one of on_update and on_update_name can be specified
@@ -432,6 +434,7 @@ class WaveformBase(ProcessDeviceSupportCore):
432434

433435

434436
def __init__(self, name, _wf_nelm, _wf_dtype, **kargs):
437+
self.autosave = kargs.pop("autosave", False)
435438
self._dtype = _wf_dtype
436439
self._nelm = _wf_nelm
437440
self.__super.__init__(name, **kargs)

softioc/pythonSoftIoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(self, builder, device, name, **fields):
2424
# have to maintain this separately from the corresponding device list.
2525
DeviceKeywords = [
2626
'on_update', 'on_update_name', 'validate', 'always_update',
27-
'initial_value', '_wf_nelm', '_wf_dtype', 'blocking']
27+
'initial_value', '_wf_nelm', '_wf_dtype', 'blocking', 'autosave']
2828
device_kargs = {}
2929
for keyword in DeviceKeywords:
3030
if keyword in fields:

0 commit comments

Comments
 (0)