mirror of
https://github.com/jayofelony/pwnagotchi.git
synced 2025-07-01 18:37:27 -04:00
new: new grateful status that can override sad/bored/lonely if units with a strong bond are nearby
This commit is contained in:
@ -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
|
|
||||||
|
@ -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
118
pwnagotchi/automata.py
Normal 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
|
@ -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: '(✜‿‿✜)'
|
||||||
|
@ -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)
|
||||||
|
@ -7,6 +7,7 @@ BORED = '(-__-)'
|
|||||||
INTENSE = '(°▃▃°)'
|
INTENSE = '(°▃▃°)'
|
||||||
COOL = '(⌐■_■)'
|
COOL = '(⌐■_■)'
|
||||||
HAPPY = '(•‿‿•)'
|
HAPPY = '(•‿‿•)'
|
||||||
|
GRATEFUL = '(^‿‿^)'
|
||||||
EXCITED = '(ᵔ◡◡ᵔ)'
|
EXCITED = '(ᵔ◡◡ᵔ)'
|
||||||
MOTIVATED = '(☼‿‿☼)'
|
MOTIVATED = '(☼‿‿☼)'
|
||||||
DEMOTIVATED = '(≖__≖)'
|
DEMOTIVATED = '(≖__≖)'
|
||||||
|
@ -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())
|
||||||
|
@ -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 ...'),
|
||||||
|
Reference in New Issue
Block a user