From 36c3ea5bbc12c6b7e58cd36bc04fbbc5e6bbd994 Mon Sep 17 00:00:00 2001 From: Simone Margaritelli Date: Wed, 23 Oct 2019 18:19:43 +0200 Subject: [PATCH] new: new grateful status that can override sad/bored/lonely if units with a strong bond are nearby --- pwnagotchi/agent.py | 100 +++------------------------------ pwnagotchi/ai/train.py | 2 - pwnagotchi/automata.py | 118 +++++++++++++++++++++++++++++++++++++++ pwnagotchi/defaults.yml | 4 ++ pwnagotchi/mesh/utils.py | 3 + pwnagotchi/ui/faces.py | 1 + pwnagotchi/ui/view.py | 5 ++ pwnagotchi/voice.py | 5 ++ 8 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 pwnagotchi/automata.py diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 16fa722..b3dd8cf 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -8,6 +8,7 @@ import _thread import pwnagotchi import pwnagotchi.utils as utils import pwnagotchi.plugins as plugins +from pwnagotchi.automata import Automata from pwnagotchi.log import LastSession from pwnagotchi.bettercap import Client from pwnagotchi.mesh.utils import AsyncAdvertiser @@ -16,13 +17,14 @@ from pwnagotchi.ai.train import AsyncTrainer RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery' -class Agent(Client, AsyncAdvertiser, AsyncTrainer): +class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer): def __init__(self, view, config, keypair): Client.__init__(self, config['bettercap']['hostname'], config['bettercap']['scheme'], config['bettercap']['port'], config['bettercap']['username'], config['bettercap']['password']) + Automata.__init__(self, config, view) AsyncAdvertiser.__init__(self, config, view, keypair) AsyncTrainer.__init__(self, config) @@ -49,36 +51,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): def supported_channels(self): return self._supported_channels - def set_starting(self): - self._view.on_starting() - - def set_ready(self): - plugins.on('ready', self) - - def set_free_channel(self, channel): - self._view.on_free_channel(channel) - plugins.on('free_channel', self, channel) - - def set_bored(self): - self._view.on_bored() - plugins.on('bored', self) - - def set_sad(self): - self._view.on_sad() - plugins.on('sad', self) - - def set_excited(self): - self._view.on_excited() - plugins.on('excited', self) - - def set_lonely(self): - self._view.on_lonely() - plugins.on('lonely', self) - - def set_rebooting(self): - self._view.on_rebooting() - plugins.on('rebooting', self) - def setup_events(self): logging.info("connecting to %s ..." % self.url) @@ -155,11 +127,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): self.next_epoch() self.set_ready() - def wait_for(self, t, sleeping=True): - plugins.on('sleep' if sleeping else 'wait', self, t) - self._view.wait(t, sleeping) - self._epoch.track(sleep=True, inc=t) - def recon(self): recon_time = self._config['personality']['recon_time'] max_inactive = self._config['personality']['max_inactive_scale'] @@ -277,6 +244,11 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): def _update_peers(self): self._view.set_closest_peer(self._closest_peer, len(self._peers)) + def _reboot(self): + self.set_rebooting() + self._save_recovery_data() + pwnagotchi.reboot() + def _save_recovery_data(self): logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE) with open(RECOVERY_DATA_FILE, 'w') as fp: @@ -391,21 +363,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): return self._history[who] < self._config['personality']['max_interactions'] - def _on_miss(self, who): - logging.info("it looks like %s is not in range anymore :/" % who) - self._epoch.track(miss=True) - self._view.on_miss(who) - - def _on_error(self, who, e): - error = "%s" % e - # when we're trying to associate or deauth something that is not in range anymore - # (if we are moving), we get the following error from bettercap: - # error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list. - if 'is an unknown BSSID' in error: - self._on_miss(who) - else: - logging.error("%s" % e) - def associate(self, ap, throttle=0): if self.is_stale(): logging.debug("recon is stale, skipping assoc(%s)" % ap['mac']) @@ -482,44 +439,3 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): except Exception as e: logging.error("error: %s" % e) - - def is_stale(self): - return self._epoch.num_missed > self._config['personality']['max_misses_for_recon'] - - def any_activity(self): - return self._epoch.any_activity - - def _reboot(self): - self.set_rebooting() - self._save_recovery_data() - pwnagotchi.reboot() - - def next_epoch(self): - was_stale = self.is_stale() - did_miss = self._epoch.num_missed - - self._epoch.next() - - # after X misses during an epoch, set the status to lonely - if was_stale: - logging.warning("agent missed %d interactions -> lonely" % did_miss) - self.set_lonely() - # after X times being bored, the status is set to sad - elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']: - logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for) - self.set_sad() - # after X times being inactive, the status is set to bored - elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']: - logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for) - self.set_bored() - # after X times being active, the status is set to happy / excited - elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']: - logging.warning("%d epochs with activity -> excited" % self._epoch.active_for) - self.set_excited() - - plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data()) - - if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']: - logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for) - self._reboot() - self._epoch.blind_for = 0 diff --git a/pwnagotchi/ai/train.py b/pwnagotchi/ai/train.py index 96e6789..ed644d6 100644 --- a/pwnagotchi/ai/train.py +++ b/pwnagotchi/ai/train.py @@ -8,7 +8,6 @@ import logging import pwnagotchi.plugins as plugins import pwnagotchi.ai as ai -from pwnagotchi.ai.epoch import Epoch class Stats(object): @@ -88,7 +87,6 @@ class AsyncTrainer(object): def __init__(self, config): self._config = config self._model = None - self._epoch = Epoch(config) self._is_training = False self._training_epochs = 0 self._nn_path = self._config['ai']['path'] diff --git a/pwnagotchi/automata.py b/pwnagotchi/automata.py new file mode 100644 index 0000000..b0ae92f --- /dev/null +++ b/pwnagotchi/automata.py @@ -0,0 +1,118 @@ +import logging + +import pwnagotchi.plugins as plugins +from pwnagotchi.ai.epoch import Epoch + + +# basic mood system +class Automata(object): + def __init__(self, config, view): + self._config = config + self._view = view + self._epoch = Epoch(config) + + def _on_miss(self, who): + logging.info("it looks like %s is not in range anymore :/" % who) + self._epoch.track(miss=True) + self._view.on_miss(who) + + def _on_error(self, who, e): + error = "%s" % e + # when we're trying to associate or deauth something that is not in range anymore + # (if we are moving), we get the following error from bettercap: + # error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list. + if 'is an unknown BSSID' in error: + self._on_miss(who) + else: + logging.error("%s" % e) + + def set_starting(self): + self._view.on_starting() + + def set_ready(self): + plugins.on('ready', self) + + def _has_support_network_for(self, factor): + bond_factor = self._config['personality']['bond_encounters_factor'] + total_encounters = sum(peer.encounters for _, peer in self._peers.items()) + support_factor = total_encounters / bond_factor + return support_factor >= factor + + # triggered when it's a sad/bad day but you have good friends around ^_^ + def set_grateful(self): + self._view.on_grateful() + plugins.on('grateful', self) + + def set_lonely(self): + if not self._has_support_network_for(1.0): + self._view.on_lonely() + plugins.on('lonely', self) + else: + self.set_grateful() + + def set_bored(self): + factor = self._epoch.inactive_for / self._config['personality']['bored_num_epochs'] + if not self._has_support_network_for(factor): + logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for) + self._view.on_bored() + plugins.on('bored', self) + else: + self.set_grateful() + + def set_sad(self): + factor = self._epoch.inactive_for / self._config['personality']['sad_num_epochs'] + if not self._has_support_network_for(factor): + logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for) + self._view.on_sad() + plugins.on('sad', self) + else: + self.set_grateful() + + def set_excited(self): + logging.warning("%d epochs with activity -> excited" % self._epoch.active_for) + self._view.on_excited() + plugins.on('excited', self) + + def set_rebooting(self): + self._view.on_rebooting() + plugins.on('rebooting', self) + + def wait_for(self, t, sleeping=True): + plugins.on('sleep' if sleeping else 'wait', self, t) + self._view.wait(t, sleeping) + self._epoch.track(sleep=True, inc=t) + + def is_stale(self): + return self._epoch.num_missed > self._config['personality']['max_misses_for_recon'] + + def any_activity(self): + return self._epoch.any_activity + + def next_epoch(self): + was_stale = self.is_stale() + did_miss = self._epoch.num_missed + + self._epoch.next() + + # after X misses during an epoch, set the status to lonely + if was_stale: + logging.warning("agent missed %d interactions -> lonely" % did_miss) + self.set_lonely() + # after X times being bored, the status is set to sad + elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']: + self.set_sad() + # after X times being inactive, the status is set to bored + elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']: + self.set_bored() + # after X times being active, the status is set to happy / excited + elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']: + self.set_excited() + elif self._has_support_network_for(1.0): + self.set_grateful() + + plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data()) + + if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']: + logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for) + self._reboot() + self._epoch.blind_for = 0 diff --git a/pwnagotchi/defaults.yml b/pwnagotchi/defaults.yml index 6e91965..a22874c 100644 --- a/pwnagotchi/defaults.yml +++ b/pwnagotchi/defaults.yml @@ -161,6 +161,9 @@ personality: bored_num_epochs: 15 # number of inactive epochs that triggers the sad state sad_num_epochs: 25 + # number of encounters (times met on a channel) with another unit before considering it a friend and bond + # also used for cumulative bonding score of nearby units + bond_encounters_factor: 5000 # ui configuration ui: @@ -176,6 +179,7 @@ ui: cool: '(⌐■_■)' happy: '(•‿‿•)' excited: '(ᵔ◡◡ᵔ)' + grateful: '(^‿‿^)' motivated: '(☼‿‿☼)' demotivated: '(≖__≖)' smart: '(✜‿‿✜)' diff --git a/pwnagotchi/mesh/utils.py b/pwnagotchi/mesh/utils.py index 8591f4b..9a67f38 100644 --- a/pwnagotchi/mesh/utils.py +++ b/pwnagotchi/mesh/utils.py @@ -53,6 +53,9 @@ class AsyncAdvertiser(object): self._advertisement['face'] = new grid.set_advertisement_data(self._advertisement) + def cumulative_encounters(self): + return sum(peer.encounters for _, peer in self._peers.items()) + def _on_new_peer(self, peer): self._view.on_new_peer(peer) plugins.on('peer_detected', self, peer) diff --git a/pwnagotchi/ui/faces.py b/pwnagotchi/ui/faces.py index d4cbd32..d90b549 100644 --- a/pwnagotchi/ui/faces.py +++ b/pwnagotchi/ui/faces.py @@ -7,6 +7,7 @@ BORED = '(-__-)' INTENSE = '(°▃▃°)' COOL = '(⌐■_■)' HAPPY = '(•‿‿•)' +GRATEFUL = '(^‿‿^)' EXCITED = '(ᵔ◡◡ᵔ)' MOTIVATED = '(☼‿‿☼)' DEMOTIVATED = '(≖__≖)' diff --git a/pwnagotchi/ui/view.py b/pwnagotchi/ui/view.py index dcb31a2..3542c8d 100644 --- a/pwnagotchi/ui/view.py +++ b/pwnagotchi/ui/view.py @@ -290,6 +290,11 @@ class View(object): self.set('status', self._voice.on_miss(who)) self.update() + def on_grateful(self): + self.set('face', faces.GRATEFUL) + self.set('status', self._voice.on_grateful()) + self.update() + def on_lonely(self): self.set('face', faces.LONELY) self.set('status', self._voice.on_lonely()) diff --git a/pwnagotchi/voice.py b/pwnagotchi/voice.py index 530c815..cf98fda 100644 --- a/pwnagotchi/voice.py +++ b/pwnagotchi/voice.py @@ -85,6 +85,11 @@ class Voice: self._('{name} missed!').format(name=who), self._('Missed!')]) + def on_grateful(self): + return random.choice([ + self._('Good friends are a blessing!'), + self._('I love my friends!')]) + def on_lonely(self): return random.choice([ self._('Nobody wants to play with me ...'),