Skip to content

Commit d1ded37

Browse files
authored
Update Alicanto Features (#16)
* adding alicanto co-provider feature * initial documentation, will need more * fix headers * working on bug * first shot add logic * add logic * fix tag * see if threads can be cleaned * close sockets * looks like context term did the trick * wait a sec * typo fix * working on closing zmq context * maybe working * add some error handling * add a sleep * move split into try * get rid of sleeps * work on getting seperate exec good * fix logic import * add parser * change tag * maybe a fix * fix point split not working at start * fix binary bug * fix client.py weirdness * allow user to provide false to client.py * comment out startup sleep * undo client change * try out new digiital write * actual fix with eval * remove unneeded provider files * fix readme * clean up comments --------- Co-authored-by: jarwils <[email protected]>
1 parent 991f1e5 commit d1ded37

File tree

5 files changed

+190
-456
lines changed

5 files changed

+190
-456
lines changed

src/pybennu/pybennu/executables/pybennu_alicanto.py

+188-45
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,38 @@
1414
import sys
1515
import time
1616
import math
17+
from distutils.util import strtobool
18+
from py_expression_eval import Parser
1719

1820
from pybennu.distributed.subscriber import Subscriber
1921
from pybennu.distributed.client import Client
2022
import pybennu.distributed.swig._Endpoint as E
2123

24+
#Adding a timeout helper to cause client objects not to feeze program
25+
import signal
26+
from contextlib import contextmanager
27+
28+
29+
@contextmanager
30+
def timeout(time):
31+
# Register a function to raise a TimeoutError on the signal.
32+
signal.signal(signal.SIGALRM, raise_timeout)
33+
# Schedule the signal to be sent after ``time``
34+
signal.alarm(time)
35+
36+
try:
37+
yield
38+
except TimeoutError:
39+
pass
40+
finally:
41+
# Unregister the signal so it won't be triggered
42+
# if the timeout is not reached.
43+
signal.signal(signal.SIGALRM, signal.SIG_IGN)
44+
45+
46+
def raise_timeout(signum, frame):
47+
raise TimeoutError
48+
2249
logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(levelname)s - %(message)s')
2350
logger = logging.getLogger('alicanto')
2451
#logger.addHandler(logging.StreamHandler())
@@ -34,24 +61,31 @@ class alicantoClient(Client):
3461
def __init__(self, end_dest):
3562
new_endpoint_dest = E.new_Endpoint()
3663
E.Endpoint_str_set(new_endpoint_dest, 'tcp://'+str(end_dest))
37-
self.endpointName = 'tcp://'+str(end_dest)
3864
Client.__init__(self, new_endpoint_dest)
39-
65+
4066
def send(self, message):
4167
""" Send message to Provider
4268
"""
43-
# send update
44-
self._Client__socket.send_string(message+'\0') # must include null byte
45-
# get response
46-
msg = self._Client__socket.recv_string()
47-
reply = msg.split('=')
48-
status = reply[0]
49-
data = reply[1]
69+
with timeout(10):
70+
self.connect()
71+
# send update
72+
self._Client__socket.send_string(message+'\0') # must include null byte
73+
# get response
74+
msg = self._Client__socket.recv_string()
75+
reply = msg.split('=')
76+
status = reply[0]
77+
data = reply[1]
78+
79+
if status == self._Client__kACK:
80+
print("I: ACK: "+data)
81+
#self.reply_handler(data)
82+
else:
83+
print("I: ERR -- %s" % msg)
5084

51-
if status != self._Client__kACK:
52-
logger.error(msg)
85+
self._Client__socket.close()
86+
self._Client__context.term()
5387

54-
return reply
88+
return reply
5589

5690
class alicanto():
5791
def __init__(self, config, debug=False, exit_handler=None):
@@ -72,6 +106,9 @@ def __init__(self, config, debug=False, exit_handler=None):
72106
self.dests = {}
73107
# Tag=>type map
74108
self.types = {}
109+
self.logic = {}
110+
# Expression parser for logic
111+
self.parser = Parser()
75112
# Set of all tags
76113
self.tags = {}
77114

@@ -103,7 +140,7 @@ def __init__(self, config, debug=False, exit_handler=None):
103140
end_name = endpoint["name"]
104141
end_destination = endpoint["destination"]
105142
end_type = endpoint["type"]
106-
logger.debug(f"Registered endpoint ---> end_name: {end_name} ---> end_destination: {end_destination}")
143+
logger.info(f"Registered endpoint ---> end_name: {end_name} ---> end_destination: {end_destination}")
107144
self.tags.update({end_destination : 0})
108145
self.end_dests.append(end_destination)
109146
self.dests[end_name] = end_destination
@@ -124,24 +161,23 @@ def __init__(self, config, debug=False, exit_handler=None):
124161
sub_info = self.subid[i]["info"] # stores logic for interdependencies
125162
except:
126163
sub_info = None
127-
logger.debug(f"Registered subscription ---> sub_name: {sub_name} ---> sub_type: {sub_type} ---> sub_info: {sub_info}")
128-
#sub_name = sub_name.split('/')[1] if '/' in sub_name else sub_name
164+
logger.info(f"Registered subscription ---> sub_name: {sub_name} ---> sub_type: {sub_type} ---> sub_info: {sub_info}")
129165
self.tags.update({sub_name : 0 })
130166
self.types[sub_name] = sub_type
131167
if sub_info:
132-
logger.debug(f"********** LOGIC **********")
168+
logger.info(f"********** LOGIC **********")
133169
for exp in sub_info.split(';'):
134170
lhs, rhs = exp.split('=')
135171
self.logic[lhs.strip()] = rhs.strip()
136-
logger.debug(f'{exp.strip()}')
172+
logger.info(f'{exp.strip()}')
137173
#make sub_sources elements unique
138174
self.sub_sources = list(set(self.sub_sources))
139175

140176
for tag in self.tags:
141177
self.state[tag] = False if self.get_type(tag) == 'bool' else 0
142178

143179
for sub_source in self.sub_sources:
144-
logger.debug(f"Launching Subscriber Thread ---> subscription: udp://{sub_source}")
180+
logger.info(f"Launching Subscriber Thread ---> subscription: udp://{sub_source}")
145181
subber = alicantoSubscriber(sub_source)
146182
subber.subscription_handler = self._subscription_handler
147183
self.__sub_thread = threading.Thread(target=subber.run)
@@ -153,17 +189,45 @@ def __init__(self, config, debug=False, exit_handler=None):
153189
for end_dest in self.end_dests:
154190
# Initialize bennu Client
155191
end_dest = end_dest.split('/')[0]
156-
self.end_clients[end_dest] = alicantoClient(end_dest)
192+
try:
193+
self.end_clients[end_dest] = alicantoClient(end_dest)
194+
except:
195+
logger.error(f"\tError Initializing Client: {self.end_clients}")
157196
for key in list(self.end_clients.keys()):
158-
logger.debug(f"End_client: {key}")
197+
logger.info(f"End_client: {key}")
159198

160199
def run(self):
161200
############## Entering Execution Mode ##############################
162201
logger.info("Entered alicanto execution mode")
202+
# Endpoint initial values to alicanto
203+
for i in range(self.end_count):
204+
full_end_name = self.endid[i]["name"]
205+
end_name = (full_end_name.split('/')[1]
206+
if '/' in full_end_name
207+
else full_end_name)
208+
full_end_dest = self.endid[i]["destination"]
209+
end_dest = (full_end_dest.split('/')[0]
210+
if '/' in full_end_dest
211+
else full_end_dest)
212+
end_dest_tag = (full_end_dest.split('/')[1]
213+
if '/' in full_end_dest
214+
else full_end_dest)
215+
try:
216+
self.end_clients[end_dest] = alicantoClient(end_dest)
217+
reply = self.end_clients[end_dest].send("READ="+end_dest_tag)
218+
value = reply[1].rstrip('\x00')
219+
self.endid[i]["value"] = value
220+
self.tag(full_end_dest, value)
221+
logger.debug(f"Initial Endpoints {end_name} / {end_dest}:{value} ")
222+
223+
except:
224+
logger.error(f"\tError Initializing Client: {self.end_clients}")
225+
continue
163226

164227
########## Main co-simulation loop ####################################
165228
while True:
166229
self.publish_state()
230+
time.sleep(0.1)
167231
for key, value in self.endid.items():
168232
full_end_name = value["name"]
169233
end_name = (full_end_name.split('/')[1]
@@ -177,25 +241,95 @@ def run(self):
177241
if '/' in full_end_dest
178242
else full_end_dest)
179243

180-
# !!need to add something to handle binary points
181244
if self.types[full_end_name] == 'float' or self.types[full_end_name] == 'double':
182245
if not math.isclose(float(self.tag(full_end_name)), float(self.tag(full_end_dest))):
183-
self.end_clients[end_dest].write_analog_point(end_dest_tag, self.tag(full_end_name))
184-
reply = self.end_clients[end_dest].send("READ="+end_name)
185-
value = reply[1].rstrip('\x00')
186-
self.tag(full_end_dest, value)
246+
#Handle Logic
247+
if self.logic[full_end_dest] is not None:
248+
expr = self.parser.parse(self.logic[full_end_dest])
249+
'''
250+
# Assign variables
251+
vars = {}
252+
for var in expr.variables():
253+
vars[var] = self.tag(var)
254+
'''
255+
i = 0
256+
# Assign vars not working, so assign token manually
257+
for token in expr.tokens:
258+
for search_tag in self.tags:
259+
if token.toString() == search_tag:
260+
expr.tokens[i].number_ = self.tag(token.toString())
261+
i += 1
262+
# Evaluate expression
263+
value = expr.evaluate(vars)
264+
value = str(value).lower()
265+
if value != self.tag(full_end_dest):
266+
logger.debug(f"\tLOGIC: {full_end_dest.strip()}={self.logic[full_end_dest]} ----> {value}")
267+
# Assign new tag value
268+
self.tag(full_end_dest, value)
269+
# Skip if value is unchanged
270+
elif value == self.tag(full_end_dest):
271+
continue
272+
273+
try:
274+
self.end_clients[end_dest] = alicantoClient(end_dest)
275+
if self.logic[full_end_dest] is not None:
276+
self.end_clients[end_dest].write_analog_point(end_dest_tag, self.tag(full_end_dest))
277+
else:
278+
self.end_clients[end_dest].write_analog_point(end_dest_tag, self.tag(full_end_name))
279+
time.sleep(0.5)
280+
reply = self.end_clients[end_dest].send("READ="+end_dest_tag)
281+
value = reply[1].rstrip('\x00')
282+
self.tag(full_end_dest, value)
283+
except:
284+
logger.error(f"\tError Initializing Client: {self.end_clients}")
285+
continue
187286
elif self.types[full_end_name] == 'bool':
188287
if str(self.tag(full_end_name)).lower() != str(self.tag(full_end_dest)).lower():
189-
self.end_clients[end_dest].write_digital_point(end_dest_tag, self.tag(full_end_name))
190-
reply = self.end_clients[end_dest].send("READ="+end_name)
191-
value = reply[1].rstrip('\x00')
192-
self.tag(full_end_dest, value)
288+
#Handle Logic
289+
if self.logic[full_end_dest] is not None:
290+
expr = self.parser.parse(self.logic[full_end_dest])
291+
'''
292+
# Assign variables
293+
vars = {}
294+
for var in expr.variables():
295+
vars[var] = self.tag(var)
296+
'''
297+
i = 0
298+
# Assign vars not working, so assign token manually
299+
for token in expr.tokens:
300+
for search_tag in self.tags:
301+
if token.toString() == search_tag:
302+
expr.tokens[i].number_ = bool(self.tag(token.toString()))
303+
i += 1
304+
# Evaluate expression
305+
value = expr.evaluate(vars)
306+
value = str(value)
307+
if value != self.tag(full_end_dest):
308+
logger.debug(f"\tLOGIC: {full_end_dest.strip()}={self.logic[full_end_dest]} ----> {value}")
309+
# Assign new tag value
310+
self.tag(full_end_dest, value)
311+
# Skip if value is unchanged
312+
elif value == self.tag(full_end_dest):
313+
continue
314+
try:
315+
self.end_clients[end_dest] = alicantoClient(end_dest)
316+
if self.logic[full_end_dest] is not None:
317+
self.end_clients[end_dest].write_digital_point(end_dest_tag, eval(self.tag(full_end_dest)))
318+
else:
319+
self.end_clients[end_dest].write_digital_point(end_dest_tag, eval(self.tag(full_end_name)))
320+
time.sleep(0.5)
321+
reply = self.end_clients[end_dest].send("READ="+end_dest_tag)
322+
value = reply[1].rstrip('\x00')
323+
self.tag(full_end_dest, value)
324+
except:
325+
logger.error(f"\tError Initializing Client: {self.end_clients}")
326+
continue
193327

194328
def publish_state(self):
195-
logger.debug("=================== DATA ===================")
329+
logger.info("=================== DATA ===================")
196330
for tag in self.tags:
197-
logger.debug(f"{tag:<30} --- {self.tag(tag):}")
198-
logger.debug("============================================")
331+
logger.info(f"{tag:<30} --- {self.tag(tag):}")
332+
logger.info("============================================")
199333

200334
def get_type(self, tag):
201335
return self.types[tag]
@@ -221,6 +355,7 @@ def _subscription_handler(self, message):
221355
message (str): published zmq message as a string
222356
"""
223357
points = message.split(',')
358+
points = points[:-1] # remove last element since it might be empty
224359
sub_source = threading.current_thread().name
225360

226361
for point in points:
@@ -229,33 +364,41 @@ def _subscription_handler(self, message):
229364
if point == "":
230365
continue
231366

232-
tag = point.split(':')[0]
233-
full_tag = sub_source + '/' + tag
234-
value = point.split(':')[1]
367+
try:
368+
tag = point.split(':')[0]
369+
full_tag = sub_source + '/' + tag
370+
value = point.split(':')[1]
371+
except:
372+
continue
235373

236374
if full_tag not in self.tags:
237375
continue
238376

239-
if value.lower() == 'false':
240-
value = False
241-
field = 'status'
242-
elif value.lower() == 'true':
243-
value = True
244-
field = 'status'
377+
if self.types[full_tag] == 'bool':
378+
if value.lower() == 'false' or value == '0':
379+
value = False
380+
field = 'status'
381+
elif value.lower() == 'true' or value == '1':
382+
value = True
383+
field = 'status'
245384
else:
246385
value = float(value)
247386
field = 'value'
248387

249388
if field == 'value':
250389
if not math.isclose(float(self.tag(full_tag)), value):
251390
self.tag(full_tag, value)
252-
logger.info("UPDATE NOW: "+full_tag)
253-
logger.info("New value: "+str(value))
391+
logger.debug("UPDATE NOW: "+full_tag)
392+
logger.debug("New value: "+str(value))
254393
else:
255394
continue
256395
elif field == 'status':
257-
logger.info("Cannot handle binary points")
258-
continue
396+
if self.tag(full_tag) != value:
397+
self.tag(full_tag, value)
398+
logger.debug("UPDATE NOW: "+full_tag)
399+
logger.debug("New value: "+str(value))
400+
else:
401+
continue
259402
else:
260403
continue
261404

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
Alicanto is a new feature made to be a more simple co-simulation tool than HELICS.
22

33
The code is similar to the bennu HELICS code but stripped down.
4-
Alicanto runs as a Subscriber and Client object. It takes in a configuration file (which points to a json) which defines which points Alicanto cares about
5-
JSON format
4+
Alicanto runs as a Subscriber and Client object. It takes in a json file which defines which points Alicanto cares about.
65
- Subscriptions tell Alicanto which publish point (udp) to subscrie to and which point to keep track of
76
- Endpoints tell Alicanto where to corelate that subscribed point to a server-endpoint
87

98
Usage:
10-
`pybennu-power-solver -c config.ini -v start`
9+
`pybennu-alicanto -c alicanto.json -d DEBUG`
1110

1211
Please update this README as Alicanto is used more

src/pybennu/pybennu/providers/alicanto/__init__.py

-1
This file was deleted.

0 commit comments

Comments
 (0)