From d9f8225feb1c187ea64c1de2a997bda4f0ae0f3d Mon Sep 17 00:00:00 2001 From: shinedday <shinedday@gmail.com> Date: Wed, 8 Jun 2022 08:57:34 +0200 Subject: [PATCH] Version : IoTAMAK-0.0.7 Major change : * completly revamp the core structure Add Async classes (Amas, Env, Agent) that can live without a scheduler --- dist/iotAmak-0.0.7-py3-none-any.whl | Bin 0 -> 31645 bytes iotAmak/{tool => agent}/__init__.py | 0 iotAmak/agent/agent.py | 68 +++++++++++ iotAmak/agent/async_agent.py | 57 +++++++++ iotAmak/agent/async_communicating_agent.py | 16 +++ iotAmak/{agent.py => agent/base_agent.py} | 111 ++++------------- .../base_communicating_agent.py} | 15 +-- iotAmak/agent/communicating_agent.py | 16 +++ iotAmak/{tool => agent}/mail.py | 0 iotAmak/amas/__init__.py | 0 iotAmak/amas/amas.py | 79 ++++++++++++ iotAmak/amas/async_amas.py | 114 ++++++++++++++++++ iotAmak/{amas.py => amas/base_amas.py} | 110 +++++------------ iotAmak/base/__init__.py | 0 iotAmak/base/async_controlable.py | 92 ++++++++++++++ iotAmak/{tool => base}/mqtt_client.py | 0 iotAmak/{tool => base}/schedulable.py | 5 +- iotAmak/env/__init__.py | 0 iotAmak/env/async_env.py | 36 ++++++ iotAmak/env/base_env.py | 13 ++ iotAmak/{ => env}/environment.py | 19 +-- iotAmak/ihm/__init__.py | 0 iotAmak/{tool => ihm}/confi_reader.py | 2 +- iotAmak/{ => ihm}/ihm.py | 12 +- iotAmak/scheduler/__init__.py | 0 iotAmak/{ => scheduler}/scheduler.py | 5 +- iotAmak/ssh_module/__init__.py | 0 iotAmak/{tool => ssh_module}/remote_client.py | 0 iotAmak/{tool => ssh_module}/ssh_client.py | 14 +-- setup.py | 2 +- 30 files changed, 572 insertions(+), 214 deletions(-) create mode 100644 dist/iotAmak-0.0.7-py3-none-any.whl rename iotAmak/{tool => agent}/__init__.py (100%) create mode 100644 iotAmak/agent/agent.py create mode 100644 iotAmak/agent/async_agent.py create mode 100644 iotAmak/agent/async_communicating_agent.py rename iotAmak/{agent.py => agent/base_agent.py} (52%) rename iotAmak/{communicating_agent.py => agent/base_communicating_agent.py} (86%) create mode 100644 iotAmak/agent/communicating_agent.py rename iotAmak/{tool => agent}/mail.py (100%) create mode 100644 iotAmak/amas/__init__.py create mode 100644 iotAmak/amas/amas.py create mode 100644 iotAmak/amas/async_amas.py rename iotAmak/{amas.py => amas/base_amas.py} (63%) create mode 100644 iotAmak/base/__init__.py create mode 100644 iotAmak/base/async_controlable.py rename iotAmak/{tool => base}/mqtt_client.py (100%) rename iotAmak/{tool => base}/schedulable.py (89%) create mode 100644 iotAmak/env/__init__.py create mode 100644 iotAmak/env/async_env.py create mode 100644 iotAmak/env/base_env.py rename iotAmak/{ => env}/environment.py (83%) create mode 100644 iotAmak/ihm/__init__.py rename iotAmak/{tool => ihm}/confi_reader.py (90%) rename iotAmak/{ => ihm}/ihm.py (93%) create mode 100644 iotAmak/scheduler/__init__.py rename iotAmak/{ => scheduler}/scheduler.py (97%) create mode 100644 iotAmak/ssh_module/__init__.py rename iotAmak/{tool => ssh_module}/remote_client.py (100%) rename iotAmak/{tool => ssh_module}/ssh_client.py (91%) diff --git a/dist/iotAmak-0.0.7-py3-none-any.whl b/dist/iotAmak-0.0.7-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..6b8729469c70282fe1274145a88acef11cfe9f1a GIT binary patch literal 31645 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 JU}(z&@c@6#pRxb| literal 0 HcmV?d00001 diff --git a/iotAmak/tool/__init__.py b/iotAmak/agent/__init__.py similarity index 100% rename from iotAmak/tool/__init__.py rename to iotAmak/agent/__init__.py diff --git a/iotAmak/agent/agent.py b/iotAmak/agent/agent.py new file mode 100644 index 0000000..4af7fb9 --- /dev/null +++ b/iotAmak/agent/agent.py @@ -0,0 +1,68 @@ +""" +Agent class file +""" +import json +import sys +import pathlib +from typing import Dict + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.base.schedulable import Schedulable +from iotAmak.agent.base_agent import BaseAgent + + +class Agent(Schedulable, BaseAgent): + """ + base class for agent + """ + + def __init__(self, arguments: str) -> None: + + arguments: Dict = json.loads(arguments) + + broker_ip: str = arguments.get("broker_ip") + identifier: int = int(arguments.get("identifier")) + seed: int = int(arguments.get("seed")) + broker_username: str = str(arguments.get("broker_username")) + broker_password: str = str(arguments.get("broker_password")) + + Schedulable.__init__(self, broker_ip, "Agent" + str(identifier), broker_username, broker_password) + BaseAgent.__init__(self, identifier, seed) + + self.subscribe("scheduler/agent/wakeup", self.wake_up) + + self.on_initialization() + + self.publish("cycle_done", "") + + def publish(self, topic: str, message) -> None: + """ + publish a message on the topic + :param topic: str + :param message: content of the message + """ + self.client.publish("agent/" + str(self.id) + "/" + topic, message) + + def run(self) -> None: + """ + Main method of the agent + """ + while not self.exit_bool: + + self.wait() + if self.exit_bool: + return + + self.on_cycle_begin() + + self.on_perceive() + + self.on_decide() + + self.on_act() + + self.on_cycle_end() + + self.publish("metric", str(self.send_metric())) + self.publish("cycle_done", "") + self.nbr_cycle += 1 diff --git a/iotAmak/agent/async_agent.py b/iotAmak/agent/async_agent.py new file mode 100644 index 0000000..a2653d5 --- /dev/null +++ b/iotAmak/agent/async_agent.py @@ -0,0 +1,57 @@ +""" +AsyncAgent class file +""" +import pathlib +import sys +import json +from typing import Dict + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.base.async_controlable import AsyncControlable +from iotAmak.agent.base_agent import BaseAgent + + +class AsyncAgent(AsyncControlable, BaseAgent): + """ + Async class for agent + """ + + def __init__(self, arguments: str) -> None: + + arguments: Dict = json.loads(arguments) + + broker_ip: str = arguments.get("broker_ip") + identifier: int = int(arguments.get("identifier")) + seed: int = int(arguments.get("seed")) + broker_username: str = str(arguments.get("broker_username")) + broker_password: str = str(arguments.get("broker_password")) + wait_delay: float = float(arguments.get("wait_delay")) + + AsyncControlable.__init__( + self, + broker_ip, + "Agent" + str(identifier), + broker_username, + broker_password, + wait_delay + ) + BaseAgent.__init__(self, identifier, seed) + + self.on_initialization() + + def publish(self, topic: str, message) -> None: + """ + publish a message on the topic + :param topic: str + :param message: content of the message + """ + self.client.publish("agent/" + str(self.id) + "/" + topic, message) + + def behaviour(self) -> None: + self.on_cycle_begin() + self.on_perceive() + self.on_decide() + self.on_act() + self.on_cycle_end() + self.publish("metric", str(self.send_metric())) + diff --git a/iotAmak/agent/async_communicating_agent.py b/iotAmak/agent/async_communicating_agent.py new file mode 100644 index 0000000..fb221b4 --- /dev/null +++ b/iotAmak/agent/async_communicating_agent.py @@ -0,0 +1,16 @@ +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.agent.async_agent import AsyncAgent +from iotAmak.agent.base_communicating_agent import BaseCommunicatingAgent + + +class AsyncCommunicatingAgent(AsyncAgent, BaseCommunicatingAgent): + """ + Agent class that can communicate + """ + + def __init__(self, arguments: str) -> None: + AsyncAgent.__init__(self, arguments) + BaseCommunicatingAgent.__init__(self) diff --git a/iotAmak/agent.py b/iotAmak/agent/base_agent.py similarity index 52% rename from iotAmak/agent.py rename to iotAmak/agent/base_agent.py index 8ccb58e..4d31dd2 100644 --- a/iotAmak/agent.py +++ b/iotAmak/agent/base_agent.py @@ -1,140 +1,81 @@ """ -Agent class file +Base Agent class file """ -import json import random from ast import literal_eval from typing import Dict, List -import sys -import pathlib -sys.path.insert(0, str(pathlib.Path(__file__).parent)) - -from iotAmak.tool.schedulable import Schedulable - - -class Agent(Schedulable): +class BaseAgent: """ base class for agent """ - def __init__(self, arguments: str) -> None: - - arguments: Dict = json.loads(arguments) - - broker_ip: str = arguments.get("broker_ip") - identifier: int = int(arguments.get("identifier")) - seed: int = int(arguments.get("seed")) - broker_username: str = str(arguments.get("broker_username")) - broker_password: str = str(arguments.get("broker_password")) - + def __init__(self, identifier: int, seed: int) -> None: self.id: int = identifier - random.seed(seed + 10 + self.id) - Schedulable.__init__(self, broker_ip, "Agent" + str(self.id), broker_username, broker_password) - - self.subscribe("scheduler/agent/wakeup", self.wake_up) - self.neighbors: List[Dict] = [] self.next_neighbors: List[Dict] = [] self.subscribe("amas/agent/" + str(self.id) + "/neighbor", self.add_neighbor) - self.on_initialization() - - self.publish("cycle_done", "") - print("init done") - def on_initialization(self) -> None: """ This method will be executed at the end of __init__() """ pass - def add_neighbor(self, client, userdata, message) -> None: - """ - Called when the agent, receive metrics - put the metric in a list that will be rad during on_perceive - param message: metric (dict) of the neighbor - """ - result: Dict = literal_eval(message.payload.decode("utf-8")) - self.next_neighbors.append(result) - - def log(self, message: str) -> None: - """ - Convenient method to log things (will be printed in the amas's stdout - :param message: - """ - self.client.publish( - "agent/" + str(self.id) + "/log", - "[AGENT] " + str(self.id) + " : " + message - ) - - def publish(self, topic: str, message) -> None: - """ - publish a message on the topic - :param topic: str - :param message: content of the message - """ - self.client.publish("agent/" + str(self.id) + "/" + topic, message) - def on_cycle_begin(self) -> None: """ This method will be executed at the start of each cycle """ - self.log("on_cycle_begin") + pass def on_perceive(self) -> None: """ Method that should be used to open the neighbor metrics and use them """ - self.log("on_perceive") + pass def on_decide(self) -> None: """ Should be override """ - self.log("on_decide") + pass def on_act(self) -> None: """ Should be override """ - self.log("on_act") + pass def on_cycle_end(self) -> None: """ This method will be executed at the end of each cycle """ - self.log("on_cycle_end") + pass - def send_metric(self) -> Dict: + def add_neighbor(self, client, userdata, message) -> None: """ - Should be override if the neighbor need to be aware of any other info, should be a dict + Called when the agent, receive metrics + put the metric in a list that will be rad during on_perceive + param message: metric (dict) of the neighbor """ - return {"id": self.id} + result: Dict = literal_eval(message.payload.decode("utf-8")) + self.next_neighbors.append(result) - def run(self) -> None: + def log(self, message: str) -> None: """ - Main method of the agent + Convenient method to log things (will be printed in the amas's stdout + :param message: """ - while not self.exit_bool: - - self.wait() - if self.exit_bool: - return - - self.on_cycle_begin() - - self.on_perceive() - - self.on_decide() - - self.on_act() - - self.on_cycle_end() + self.client.publish( + "agent/" + str(self.id) + "/log", + "[AGENT] " + str(self.id) + " : " + message + ) - self.publish("metric", str(self.send_metric())) - self.publish("cycle_done", "") - self.nbr_cycle += 1 + def send_metric(self) -> Dict: + """ + Should be override if the neighbor need to be aware of any other info, should be a dict + """ + return {"id": self.id} diff --git a/iotAmak/communicating_agent.py b/iotAmak/agent/base_communicating_agent.py similarity index 86% rename from iotAmak/communicating_agent.py rename to iotAmak/agent/base_communicating_agent.py index 0600ea9..81cc8d1 100644 --- a/iotAmak/communicating_agent.py +++ b/iotAmak/agent/base_communicating_agent.py @@ -4,20 +4,17 @@ import sys from ast import literal_eval from typing import List, Any -sys.path.insert(0, str(pathlib.Path(__file__).parent)) +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.agent.mail import Mail -from iotAmak.agent import Agent -from iotAmak.tool.mail import Mail - -class CommunicatingAgent(Agent): +class BaseCommunicatingAgent: """ Agent class that can communicate """ - def __init__(self, arguments: str) -> None: + def __init__(self) -> None: self.mailbox: List[Mail] = [] - Agent.__init__(self, arguments) self.subscribe("agent/" + str(self.id) + "/mail", self.receive_mail) def receive_mail(self, client, userdata, message) -> None: @@ -57,7 +54,3 @@ class CommunicatingAgent(Agent): put the mail in the front of the mailbox """ self.mailbox = [mail] + self.mailbox - - - - diff --git a/iotAmak/agent/communicating_agent.py b/iotAmak/agent/communicating_agent.py new file mode 100644 index 0000000..8cebdfa --- /dev/null +++ b/iotAmak/agent/communicating_agent.py @@ -0,0 +1,16 @@ +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.agent.agent import Agent +from iotAmak.agent.base_communicating_agent import BaseCommunicatingAgent + + +class CommunicatingAgent(Agent, BaseCommunicatingAgent): + """ + Agent class that can communicate + """ + + def __init__(self, arguments: str) -> None: + Agent.__init__(self, arguments) + BaseCommunicatingAgent.__init__(self) diff --git a/iotAmak/tool/mail.py b/iotAmak/agent/mail.py similarity index 100% rename from iotAmak/tool/mail.py rename to iotAmak/agent/mail.py diff --git a/iotAmak/amas/__init__.py b/iotAmak/amas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotAmak/amas/amas.py b/iotAmak/amas/amas.py new file mode 100644 index 0000000..391bdbb --- /dev/null +++ b/iotAmak/amas/amas.py @@ -0,0 +1,79 @@ +""" +Amas class +""" +import json +from ast import literal_eval + +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.ssh_module.remote_client import RemoteClient +from iotAmak.base.schedulable import Schedulable +from iotAmak.amas.base_amas import BaseAmas + + +class Amas(Schedulable, BaseAmas): + """ + Amas class + """ + + def __init__(self, arguments: str) -> None: + + arguments = json.loads(arguments) + + broker_ip: str = arguments.get("broker_ip") + clients: str = arguments.get("clients") + seed: int = int(arguments.get("seed")) + broker_username: str = str(arguments.get("broker_username")) + broker_password: str = str(arguments.get("broker_password")) + iot_path: str = str(arguments.get("iot_path")) + true_client = [RemoteClient(i.get("hostname"), i.get("user"), i.get("password")) for i in literal_eval(clients)] + + Schedulable.__init__(self, broker_ip, "Amas", broker_username, broker_password) + self.subscribe("scheduler/schedulable/wakeup", self.wake_up) + BaseAmas.__init__(self, + broker_ip, + broker_username, + broker_password, + seed, + iot_path, + true_client) + + self.client.publish("amas/action_done", "") + + def on_cycle_begin(self) -> None: + """ + This method will be executed at the start of each cycle + """ + pass + + def on_cycle_end(self) -> None: + """ + This method will be executed at the end of each cycle + """ + pass + + def run(self) -> None: + """ + Main function of the amas class + """ + self.push_agent() + + while not self.exit_bool: + + self.wait() + if self.exit_bool: + return + + self.publish("amas/all_metric", str(self.agents_metric)) + self.on_cycle_begin() + self.client.publish("amas/action_done", "") + + # agent cycle + + self.wait() + self.on_cycle_end() + self.client.publish("amas/action_done", "") + + self.nbr_cycle += 1 diff --git a/iotAmak/amas/async_amas.py b/iotAmak/amas/async_amas.py new file mode 100644 index 0000000..34abb21 --- /dev/null +++ b/iotAmak/amas/async_amas.py @@ -0,0 +1,114 @@ +import pathlib +import sys +import json +from ast import literal_eval +from typing import List + + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.amas.base_amas import BaseAmas +from iotAmak.base.async_controlable import AsyncControlable +from iotAmak.ssh_module.remote_client import RemoteClient +from iotAmak.ssh_module.ssh_client import Cmd + + +class AsyncAmas(AsyncControlable, BaseAmas): + """ + Amas class + """ + + def __init__(self, arguments: str) -> None: + + arguments = json.loads(arguments) + + broker_ip: str = arguments.get("broker_ip") + clients: str = arguments.get("clients") + seed: int = int(arguments.get("seed")) + broker_username: str = str(arguments.get("broker_username")) + broker_password: str = str(arguments.get("broker_password")) + iot_path: str = str(arguments.get("iot_path")) + wait_delay: float = float(arguments.get("wait_delay")) + + true_client = [RemoteClient(i.get("hostname"), i.get("user"), i.get("password")) for i in literal_eval(clients)] + + AsyncControlable.__init__( + self, + broker_ip, + "Amas", + broker_username, + broker_password, + wait_delay + ) + BaseAmas.__init__(self, + broker_ip, + broker_username, + broker_password, + seed, + iot_path, + true_client) + + self.push_agent() + + def on_metric(self) -> None: + """ + This method will be executed everytime a metric is send + """ + pass + + def add_agent( + self, + experience_name: str, + client_ip: str = None, + agent_name: str = "agent.py", + args: List = None + ) -> None: + """ + Function that need to be called to create a new agent + :param experience_name: name of the experience folder + :param client_ip: if the agent should be created in a specific device, you can specify an ip address, + otherwise the Amas will try to share the work between the devices + :param agent_name: if using multiple kind of agent, you can specify the relative path in the experiment + directory to the agent file to use + :param args: if any argument is needed to initiate the new agent + :return: None + """ + if args is None: + args = [] + + arg_dict = { + "broker_ip": str(self.broker_ip), + "seed": self.seed, + "identifier": self.next_id, + "broker_username": self.broker_username, + "broker_password": self.broker_password, + "wait_delay": self.wait_delay + } + + command = "nohup python " + command += "\'" + self.iot_path + experience_name + "/" + agent_name + "\' \'" + command += json.dumps(arg_dict) + "\' " + for arg in args: + command += str(arg) + " " + command += "&" + + if client_ip is None: + # find the most suitable pi + i_min = 0 + for elem in range(len(self.clients)): + if len(self.agents_cmd[i_min]) > len(self.agents_cmd[elem]): + i_min = elem + self.agents_cmd[i_min].append(Cmd(command)) + else: + have_found = False + for i_client in range(len(self.clients)): + if self.clients[i_client].hostname == client_ip: + self.agents_cmd[i_client].append(Cmd(command)) + have_found = True + break + if not have_found: + self.agents_cmd[0].append(Cmd(command)) + + self.subscribe("agent/" + str(self.next_id) + "/metric", self.agent_metric) + + self.client.publish("amas/agent/new", self.next_id) + self.next_id += 1 \ No newline at end of file diff --git a/iotAmak/amas.py b/iotAmak/amas/base_amas.py similarity index 63% rename from iotAmak/amas.py rename to iotAmak/amas/base_amas.py index d335985..c3da4e0 100644 --- a/iotAmak/amas.py +++ b/iotAmak/amas/base_amas.py @@ -1,56 +1,48 @@ -""" -Amas class -""" import json -from ast import literal_eval -from typing import List, Dict - +import random import sys import pathlib -import random - -sys.path.insert(0, str(pathlib.Path(__file__).parent)) - -from iotAmak.tool.remote_client import RemoteClient -from iotAmak.tool.schedulable import Schedulable -from iotAmak.tool.ssh_client import SSHClient, Cmd +from ast import literal_eval +from typing import List, Dict +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.ssh_module.ssh_client import SSHClient, Cmd +from iotAmak.ssh_module.remote_client import RemoteClient -class Amas(Schedulable, SSHClient): - """ - Amas class - """ - def __init__(self, arguments: str) -> None: +class BaseAmas(SSHClient): - arguments = json.loads(arguments) + def __init__( + self, + broker_ip: str, + broker_username: str, + broker_password: str, + seed: int, + iot_path: str, + clients: List[RemoteClient] + ): + self.broker_ip: str = broker_ip + self.broker_username: str = broker_username + self.broker_password: str = broker_password - self.broker_ip: str = arguments.get("broker_ip") - clients: str = arguments.get("clients") - self.seed: int = int(arguments.get("seed")) - self.broker_username: str = str(arguments.get("broker_username")) - self.broker_password: str = str(arguments.get("broker_password")) - iot_path: str = str(arguments.get("iot_path")) + self.next_id: int = 0 + self.seed: int = seed random.seed(self.seed) - Schedulable.__init__(self, self.broker_ip, "Amas", self.broker_username, self.broker_password) + self.agents_cmd: List[List[Cmd]] = [[] for _ in range(len(clients))] - true_client = [RemoteClient(i.get("hostname"), i.get("user"), i.get("password")) for i in literal_eval(clients)] - - SSHClient.__init__(self, true_client, iot_path) - - self.subscribe("scheduler/schedulable/wakeup", self.wake_up) - - self.next_id: int = 0 - - self.agents_cmd: List[List[Cmd]] = [[] for _ in range(len(self.clients))] + SSHClient.__init__(self, clients, iot_path) self.on_initialization() self.on_initial_agents_creation() self.agents_metric: List[Dict] = [{} for _ in range(self.next_id)] - self.client.publish("amas/action_done", "") + def on_initialization(self) -> None: + """ + This method will be executed at the end of __init__() + """ + pass def on_initial_agents_creation(self) -> None: """ @@ -83,11 +75,11 @@ class Amas(Schedulable, SSHClient): "seed": self.seed, "identifier": self.next_id, "broker_username": self.broker_username, - "broker_password": self.broker_password + "broker_password": self.broker_password } command = "nohup python " - command += "\'" + self.iot_path + experience_name + "/"+agent_name+"\' \'" + command += "\'" + self.iot_path + experience_name + "/" + agent_name + "\' \'" command += json.dumps(arg_dict) + "\' " for arg in args: command += str(arg) + " " @@ -138,45 +130,3 @@ class Amas(Schedulable, SSHClient): result = literal_eval(message.payload.decode("utf-8")) agent_id = result.get("id") self.agents_metric[agent_id] = result - - def on_initialization(self) -> None: - """ - This method will be executed at the end of __init__() - """ - pass - - def on_cycle_begin(self) -> None: - """ - This method will be executed at the start of each cycle - """ - pass - - def on_cycle_end(self) -> None: - """ - This method will be executed at the end of each cycle - """ - pass - - def run(self) -> None: - """ - Main function of the amas class - """ - self.push_agent() - - while not self.exit_bool: - - self.wait() - if self.exit_bool: - return - - self.publish("amas/all_metric", str(self.agents_metric)) - self.on_cycle_begin() - self.client.publish("amas/action_done", "") - - # agent cycle - - self.wait() - self.on_cycle_end() - self.client.publish("amas/action_done", "") - - self.nbr_cycle += 1 diff --git a/iotAmak/base/__init__.py b/iotAmak/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotAmak/base/async_controlable.py b/iotAmak/base/async_controlable.py new file mode 100644 index 0000000..c0c71b8 --- /dev/null +++ b/iotAmak/base/async_controlable.py @@ -0,0 +1,92 @@ +""" +MQTT client class file +""" +import sys +import pathlib +from threading import Semaphore +from time import sleep + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.base.mqtt_client import MqttClient + + +class AsyncControlable(MqttClient): + """ + Base class to any instance that need to interact with the broker + """ + + def __init__( + self, + broker_ip: str, + client_id: str, + broker_username: str, + broker_password: str, + wait_delay: float + ): + MqttClient.__init__(self, broker_ip, client_id, broker_username, broker_password) + + # exit + self.exit_bool: bool = False + self.subscribe("ihm/exit", self.exit_procedure) + + # pause + self.paused: bool = True + self.pause_semaphore = Semaphore(0) + self.subscribe("ihm/pause", self.pause) + self.subscribe("ihm/unpause", self.unpause) + + # time to wait + self.wait_delay: float = wait_delay + + self.nbr_cycle: int = 0 + + def exit_procedure(self, client, userdata, message) -> None: + """ + Called by the Ihm to exit as soon as possible + """ + self.exit_bool = True + + def pause(self, client, userdata, message) -> None: + """ + Function called when the IHM pause the scheduler + """ + self.paused = True + + def unpause(self, client, userdata, message) -> None: + """ + Function called when the IHM unpause the scheduler + """ + self.paused = False + self.pause_semaphore.release() + + def wait(self) -> None: + """ + If the element is paused, wait until unpause was received + """ + if not self.paused: + return + self.pause_semaphore.acquire() + + def behaviour(self) -> None: + """ + method to override + """ + return + + def run(self) -> None: + """ + Main method of the client + """ + + while not self.exit_bool: + # wait to be unpause + self.wait() + + # check the need to exit + if self.exit_bool: + return + + self.behaviour() + + sleep(self.wait_delay) + self.nbr_cycle += 1 diff --git a/iotAmak/tool/mqtt_client.py b/iotAmak/base/mqtt_client.py similarity index 100% rename from iotAmak/tool/mqtt_client.py rename to iotAmak/base/mqtt_client.py diff --git a/iotAmak/tool/schedulable.py b/iotAmak/base/schedulable.py similarity index 89% rename from iotAmak/tool/schedulable.py rename to iotAmak/base/schedulable.py index 0a27304..33ec1ff 100644 --- a/iotAmak/tool/schedulable.py +++ b/iotAmak/base/schedulable.py @@ -8,7 +8,7 @@ import threading sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) -from iotAmak.tool.mqtt_client import MqttClient +from iotAmak.base.mqtt_client import MqttClient class Schedulable(MqttClient): @@ -31,15 +31,12 @@ class Schedulable(MqttClient): Called by the scheduler to wake up the schedulable """ self.semaphore.release() - # print("Waked up") def wait(self) -> None: """ Basic wait method """ - # print("Waiting") self.semaphore.acquire() - # print("End wait") def exit_procedure(self, client, userdata, message) -> None: """ diff --git a/iotAmak/env/__init__.py b/iotAmak/env/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotAmak/env/async_env.py b/iotAmak/env/async_env.py new file mode 100644 index 0000000..6d0fced --- /dev/null +++ b/iotAmak/env/async_env.py @@ -0,0 +1,36 @@ +""" +Environment class +""" +import json +import sys +import pathlib + +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.env.base_env import BaseEnv +from iotAmak.base.async_controlable import AsyncControlable + + +class AsyncEnvironment(BaseEnv, AsyncControlable): + """ + Environment class + """ + + def __init__(self, arguments: str) -> None: + + arguments = json.loads(arguments) + + broker_ip: str = arguments.get("broker_ip") + seed: int = int(arguments.get("seed")) + broker_username: str = str(arguments.get("broker_username")) + broker_password: str = str(arguments.get("broker_password")) + wait_delay: float = float(arguments.get("wait_delay")) + + AsyncControlable.__init__( + self, + broker_ip, + "Env", + broker_username, + broker_password, + wait_delay + ) + BaseEnv.__init__(self, seed) diff --git a/iotAmak/env/base_env.py b/iotAmak/env/base_env.py new file mode 100644 index 0000000..905f8fe --- /dev/null +++ b/iotAmak/env/base_env.py @@ -0,0 +1,13 @@ +import random + + +class BaseEnv: + def __init__(self, seed: int) -> None: + random.seed(seed + 1) + self.on_initialization() + + def on_initialization(self) -> None: + """ + This method will be executed at the end of __init__() + """ + pass diff --git a/iotAmak/environment.py b/iotAmak/env/environment.py similarity index 83% rename from iotAmak/environment.py rename to iotAmak/env/environment.py index b054cbc..68bbbc1 100644 --- a/iotAmak/environment.py +++ b/iotAmak/env/environment.py @@ -7,11 +7,11 @@ import sys import pathlib sys.path.insert(0, str(pathlib.Path(__file__).parent)) +from iotAmak.env.base_env import BaseEnv +from iotAmak.base.schedulable import Schedulable -from iotAmak.tool.schedulable import Schedulable - -class Environment(Schedulable): +class Environment(Schedulable, BaseEnv): """ Environment class """ @@ -25,22 +25,11 @@ class Environment(Schedulable): broker_username: str = str(arguments.get("broker_username")) broker_password: str = str(arguments.get("broker_password")) - random.seed(seed + 1) - Schedulable.__init__(self, broker_ip, "Env", broker_username, broker_password) - self.subscribe("scheduler/schedulable/wakeup", self.wake_up) - - self.on_initialization() - + BaseEnv.__init__(self, seed) self.client.publish("env/action_done", "") - def on_initialization(self) -> None: - """ - This method will be executed at the end of __init__() - """ - pass - def on_cycle_begin(self) -> None: """ This method will be executed at the start of each cycle diff --git a/iotAmak/ihm/__init__.py b/iotAmak/ihm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotAmak/tool/confi_reader.py b/iotAmak/ihm/confi_reader.py similarity index 90% rename from iotAmak/tool/confi_reader.py rename to iotAmak/ihm/confi_reader.py index 5070707..f761a9c 100644 --- a/iotAmak/tool/confi_reader.py +++ b/iotAmak/ihm/confi_reader.py @@ -4,7 +4,7 @@ import pathlib sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) -from iotAmak.tool.remote_client import RemoteClient +from iotAmak.ssh_module.remote_client import RemoteClient def read_ssh(path): diff --git a/iotAmak/ihm.py b/iotAmak/ihm/ihm.py similarity index 93% rename from iotAmak/ihm.py rename to iotAmak/ihm/ihm.py index 11b4f19..5032f41 100644 --- a/iotAmak/ihm.py +++ b/iotAmak/ihm/ihm.py @@ -9,9 +9,9 @@ import pathlib sys.path.insert(0, str(pathlib.Path(__file__).parent)) -from iotAmak.tool.confi_reader import read_ssh, read_broker -from iotAmak.tool.mqtt_client import MqttClient -from iotAmak.tool.ssh_client import SSHClient, Cmd +from iotAmak.ihm.confi_reader import read_ssh, read_broker +from iotAmak.base.mqtt_client import MqttClient +from iotAmak.ssh_module.ssh_client import SSHClient, Cmd class Ihm(MqttClient, SSHClient): @@ -29,10 +29,10 @@ class Ihm(MqttClient, SSHClient): def loading(self): print("[LOADING]") print("Check experiment:") - print(" | -> agent.py : ", path.exists("./agent.py")) - print(" | -> amas.py : ", path.exists("./amas.py")) + print(" | -> agent.py : ", path.exists("../agent/agent.py")) + print(" | -> amas.py : ", path.exists("../amas/amas.py")) print(" | -> env.py : ", path.exists("./env.py")) - if path.exists("./agent.py") and path.exists("./amas.py") and path.exists("./env.py"): + if path.exists("../agent/agent.py") and path.exists("../amas/amas.py") and path.exists("./env.py"): self.experiment_loaded = True print("Experiment loaded") else: diff --git a/iotAmak/scheduler/__init__.py b/iotAmak/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotAmak/scheduler.py b/iotAmak/scheduler/scheduler.py similarity index 97% rename from iotAmak/scheduler.py rename to iotAmak/scheduler/scheduler.py index 19e21ea..d5f2237 100644 --- a/iotAmak/scheduler.py +++ b/iotAmak/scheduler/scheduler.py @@ -7,9 +7,8 @@ from time import sleep, time import sys import pathlib -sys.path.insert(0, str(pathlib.Path(__file__).parent)) - -from iotAmak.tool.schedulable import Schedulable +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) +from iotAmak.base.schedulable import Schedulable class Scheduler(Schedulable): diff --git a/iotAmak/ssh_module/__init__.py b/iotAmak/ssh_module/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotAmak/tool/remote_client.py b/iotAmak/ssh_module/remote_client.py similarity index 100% rename from iotAmak/tool/remote_client.py rename to iotAmak/ssh_module/remote_client.py diff --git a/iotAmak/tool/ssh_client.py b/iotAmak/ssh_module/ssh_client.py similarity index 91% rename from iotAmak/tool/ssh_client.py rename to iotAmak/ssh_module/ssh_client.py index 858df6d..a208519 100644 --- a/iotAmak/tool/ssh_client.py +++ b/iotAmak/ssh_module/ssh_client.py @@ -9,8 +9,7 @@ from pexpect import pxssh sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) -from iotAmak.tool.remote_client import RemoteClient - +from iotAmak.ssh_module.remote_client import RemoteClient class Cmd: @@ -19,15 +18,14 @@ class Cmd: self.do_print = do_print self.prefix = prefix - class SSHClient: def __init__(self, clients: List[RemoteClient], iot_path: str): - self.clients = clients - self.iot_path = iot_path + self.clients: List[RemoteClient] = clients + self.iot_path: str = iot_path - def run_cmd(self, client: int, cmd: list, repeat: bool = False) -> list[str]: - ret = [] + def run_cmd(self, client: int, cmd: list, repeat: bool = False) -> List[str]: + ret: List[str] = [] try: s = pxssh.pxssh() dest = self.clients[client] @@ -48,7 +46,7 @@ class SSHClient: self.run_cmd(client, cmd, True) return ret - def update(self, experiment_name, path_to_experiment): + def update(self, experiment_name: str, path_to_experiment: str): for client in self.clients: transport = paramiko.Transport((client.hostname, 22)) transport.connect(username=client.user, password=client.password) diff --git a/setup.py b/setup.py index c884117..fc4ebef 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup( name='iotAmak', packages=find_packages(), - version='0.0.6', + version='0.0.7', description='AmakFramework in python', author='SMAC - GOYON Sebastien', install_requires=[ -- GitLab