new: new grateful status that can override sad/bored/lonely if units with a strong bond are nearby

This commit is contained in:
Simone Margaritelli
2019-10-23 18:19:43 +02:00
parent 277906a673
commit 36c3ea5bbc
8 changed files with 144 additions and 94 deletions

View File

@ -8,6 +8,7 @@ import _thread
import pwnagotchi import pwnagotchi
import pwnagotchi.utils as utils import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins import pwnagotchi.plugins as plugins
from pwnagotchi.automata import Automata
from pwnagotchi.log import LastSession from pwnagotchi.log import LastSession
from pwnagotchi.bettercap import Client from pwnagotchi.bettercap import Client
from pwnagotchi.mesh.utils import AsyncAdvertiser from pwnagotchi.mesh.utils import AsyncAdvertiser
@ -16,13 +17,14 @@ from pwnagotchi.ai.train import AsyncTrainer
RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery' RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
class Agent(Client, AsyncAdvertiser, AsyncTrainer): class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def __init__(self, view, config, keypair): def __init__(self, view, config, keypair):
Client.__init__(self, config['bettercap']['hostname'], Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'], config['bettercap']['scheme'],
config['bettercap']['port'], config['bettercap']['port'],
config['bettercap']['username'], config['bettercap']['username'],
config['bettercap']['password']) config['bettercap']['password'])
Automata.__init__(self, config, view)
AsyncAdvertiser.__init__(self, config, view, keypair) AsyncAdvertiser.__init__(self, config, view, keypair)
AsyncTrainer.__init__(self, config) AsyncTrainer.__init__(self, config)
@ -49,36 +51,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
def supported_channels(self): def supported_channels(self):
return self._supported_channels 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): def setup_events(self):
logging.info("connecting to %s ..." % self.url) logging.info("connecting to %s ..." % self.url)
@ -155,11 +127,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
self.next_epoch() self.next_epoch()
self.set_ready() 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): def recon(self):
recon_time = self._config['personality']['recon_time'] recon_time = self._config['personality']['recon_time']
max_inactive = self._config['personality']['max_inactive_scale'] max_inactive = self._config['personality']['max_inactive_scale']
@ -277,6 +244,11 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
def _update_peers(self): def _update_peers(self):
self._view.set_closest_peer(self._closest_peer, len(self._peers)) 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): def _save_recovery_data(self):
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE) logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
with open(RECOVERY_DATA_FILE, 'w') as fp: 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'] 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): def associate(self, ap, throttle=0):
if self.is_stale(): if self.is_stale():
logging.debug("recon is stale, skipping assoc(%s)" % ap['mac']) logging.debug("recon is stale, skipping assoc(%s)" % ap['mac'])
@ -482,44 +439,3 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
except Exception as e: except Exception as e:
logging.error("error: %s" % 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

View File

@ -8,7 +8,6 @@ import logging
import pwnagotchi.plugins as plugins import pwnagotchi.plugins as plugins
import pwnagotchi.ai as ai import pwnagotchi.ai as ai
from pwnagotchi.ai.epoch import Epoch
class Stats(object): class Stats(object):
@ -88,7 +87,6 @@ class AsyncTrainer(object):
def __init__(self, config): def __init__(self, config):
self._config = config self._config = config
self._model = None self._model = None
self._epoch = Epoch(config)
self._is_training = False self._is_training = False
self._training_epochs = 0 self._training_epochs = 0
self._nn_path = self._config['ai']['path'] self._nn_path = self._config['ai']['path']

118
pwnagotchi/automata.py Normal file
View File

@ -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

View File

@ -161,6 +161,9 @@ personality:
bored_num_epochs: 15 bored_num_epochs: 15
# number of inactive epochs that triggers the sad state # number of inactive epochs that triggers the sad state
sad_num_epochs: 25 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 configuration
ui: ui:
@ -176,6 +179,7 @@ ui:
cool: '(⌐■_■)' cool: '(⌐■_■)'
happy: '(•‿‿•)' happy: '(•‿‿•)'
excited: '(ᵔ◡◡ᵔ)' excited: '(ᵔ◡◡ᵔ)'
grateful: '(^‿‿^)'
motivated: '(☼‿‿☼)' motivated: '(☼‿‿☼)'
demotivated: '(≖__≖)' demotivated: '(≖__≖)'
smart: '(✜‿‿✜)' smart: '(✜‿‿✜)'

View File

@ -53,6 +53,9 @@ class AsyncAdvertiser(object):
self._advertisement['face'] = new self._advertisement['face'] = new
grid.set_advertisement_data(self._advertisement) 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): def _on_new_peer(self, peer):
self._view.on_new_peer(peer) self._view.on_new_peer(peer)
plugins.on('peer_detected', self, peer) plugins.on('peer_detected', self, peer)

View File

@ -7,6 +7,7 @@ BORED = '(-__-)'
INTENSE = '(°▃▃°)' INTENSE = '(°▃▃°)'
COOL = '(⌐■_■)' COOL = '(⌐■_■)'
HAPPY = '(•‿‿•)' HAPPY = '(•‿‿•)'
GRATEFUL = '(^‿‿^)'
EXCITED = '(ᵔ◡◡ᵔ)' EXCITED = '(ᵔ◡◡ᵔ)'
MOTIVATED = '(☼‿‿☼)' MOTIVATED = '(☼‿‿☼)'
DEMOTIVATED = '(≖__≖)' DEMOTIVATED = '(≖__≖)'

View File

@ -290,6 +290,11 @@ class View(object):
self.set('status', self._voice.on_miss(who)) self.set('status', self._voice.on_miss(who))
self.update() self.update()
def on_grateful(self):
self.set('face', faces.GRATEFUL)
self.set('status', self._voice.on_grateful())
self.update()
def on_lonely(self): def on_lonely(self):
self.set('face', faces.LONELY) self.set('face', faces.LONELY)
self.set('status', self._voice.on_lonely()) self.set('status', self._voice.on_lonely())

View File

@ -85,6 +85,11 @@ class Voice:
self._('{name} missed!').format(name=who), self._('{name} missed!').format(name=who),
self._('Missed!')]) self._('Missed!')])
def on_grateful(self):
return random.choice([
self._('Good friends are a blessing!'),
self._('I love my friends!')])
def on_lonely(self): def on_lonely(self):
return random.choice([ return random.choice([
self._('Nobody wants to play with me ...'), self._('Nobody wants to play with me ...'),