mirror of
https://github.com/jayofelony/pwnagotchi.git
synced 2025-07-01 18:37:27 -04:00
misc: refactored code to work as a setup.py based package
Signed-off-by: Simone Margaritelli <evilsocket@gmail.com>
This commit is contained in:
48
pwnagotchi/__init__.py
Normal file
48
pwnagotchi/__init__.py
Normal file
@ -0,0 +1,48 @@
|
||||
import subprocess
|
||||
|
||||
version = '1.0.0plz4'
|
||||
|
||||
_name = None
|
||||
|
||||
|
||||
def name():
|
||||
global _name
|
||||
if _name is None:
|
||||
with open('/etc/hostname', 'rt') as fp:
|
||||
_name = fp.read().strip()
|
||||
return _name
|
||||
|
||||
|
||||
def mem_usage():
|
||||
out = subprocess.getoutput("free -m")
|
||||
for line in out.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("Mem:"):
|
||||
parts = list(map(int, line.split()[1:]))
|
||||
tot = parts[0]
|
||||
used = parts[1]
|
||||
free = parts[2]
|
||||
return used / tot
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cpu_load():
|
||||
with open('/proc/stat', 'rt') as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line.startswith('cpu '):
|
||||
parts = list(map(int, line.split()[1:]))
|
||||
user_n = parts[0]
|
||||
sys_n = parts[2]
|
||||
idle_n = parts[3]
|
||||
tot = user_n + sys_n + idle_n
|
||||
return (user_n + sys_n) / tot
|
||||
return 0
|
||||
|
||||
|
||||
def temperature(celsius=True):
|
||||
with open('/sys/class/thermal/thermal_zone0/temp', 'rt') as fp:
|
||||
temp = int(fp.read().strip())
|
||||
c = int(temp / 1000)
|
||||
return c if celsius else ((c * (9 / 5)) + 32)
|
549
pwnagotchi/agent.py
Normal file
549
pwnagotchi/agent.py
Normal file
@ -0,0 +1,549 @@
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import _thread
|
||||
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.bettercap import Client
|
||||
from pwnagotchi.mesh.utils import AsyncAdvertiser
|
||||
from pwnagotchi.ai.train import AsyncTrainer
|
||||
|
||||
RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
|
||||
|
||||
|
||||
class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
def __init__(self, view, config):
|
||||
Client.__init__(self, config['bettercap']['hostname'],
|
||||
config['bettercap']['scheme'],
|
||||
config['bettercap']['port'],
|
||||
config['bettercap']['username'],
|
||||
config['bettercap']['password'])
|
||||
AsyncAdvertiser.__init__(self, config, view)
|
||||
AsyncTrainer.__init__(self, config)
|
||||
|
||||
self._started_at = time.time()
|
||||
self._filter = None if config['main']['filter'] is None else re.compile(config['main']['filter'])
|
||||
self._current_channel = 0
|
||||
self._supported_channels = utils.iface_channels(config['main']['iface'])
|
||||
self._view = view
|
||||
self._access_points = []
|
||||
self._last_pwnd = None
|
||||
self._history = {}
|
||||
self._handshakes = {}
|
||||
|
||||
@staticmethod
|
||||
def is_connected():
|
||||
try:
|
||||
socket.create_connection(("www.google.com", 80))
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def config(self):
|
||||
return self._config
|
||||
|
||||
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)
|
||||
|
||||
for tag in self._config['bettercap']['silence']:
|
||||
try:
|
||||
self.run('events.ignore %s' % tag, verbose_errors=False)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _reset_wifi_settings(self):
|
||||
mon_iface = self._config['main']['iface']
|
||||
self.run('set wifi.interface %s' % mon_iface)
|
||||
self.run('set wifi.ap.ttl %d' % self._config['personality']['ap_ttl'])
|
||||
self.run('set wifi.sta.ttl %d' % self._config['personality']['sta_ttl'])
|
||||
self.run('set wifi.rssi.min %d' % self._config['personality']['min_rssi'])
|
||||
self.run('set wifi.handshakes.file %s' % self._config['bettercap']['handshakes'])
|
||||
self.run('set wifi.handshakes.aggregate false')
|
||||
|
||||
def start_monitor_mode(self):
|
||||
mon_iface = self._config['main']['iface']
|
||||
mon_start_cmd = self._config['main']['mon_start_cmd']
|
||||
restart = not self._config['main']['no_restart']
|
||||
has_mon = False
|
||||
|
||||
while has_mon is False:
|
||||
s = self.session()
|
||||
for iface in s['interfaces']:
|
||||
if iface['name'] == mon_iface:
|
||||
logging.info("found monitor interface: %s" % iface['name'])
|
||||
has_mon = True
|
||||
break
|
||||
|
||||
if has_mon is False:
|
||||
if mon_start_cmd is not None and mon_start_cmd != '':
|
||||
logging.info("starting monitor interface ...")
|
||||
self.run('!%s' % mon_start_cmd)
|
||||
else:
|
||||
logging.info("waiting for monitor interface %s ..." % mon_iface)
|
||||
time.sleep(1)
|
||||
|
||||
logging.info("supported channels: %s" % self._supported_channels)
|
||||
logging.info("handshakes will be collected inside %s" % self._config['bettercap']['handshakes'])
|
||||
|
||||
self._reset_wifi_settings()
|
||||
|
||||
wifi_running = self.is_module_running('wifi')
|
||||
if wifi_running and restart:
|
||||
logging.debug("restarting wifi module ...")
|
||||
self.restart_module('wifi.recon')
|
||||
self.run('wifi.clear')
|
||||
elif not wifi_running:
|
||||
logging.debug("starting wifi module ...")
|
||||
self.start_module('wifi.recon')
|
||||
|
||||
self.start_advertising()
|
||||
|
||||
def start(self):
|
||||
self.start_ai()
|
||||
self.setup_events()
|
||||
self.set_starting()
|
||||
self.start_monitor_mode()
|
||||
self.start_event_polling()
|
||||
# print initial stats
|
||||
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 check_channels(self, channels):
|
||||
busy_channels = [ch for ch, aps in channels]
|
||||
# if we're hopping and no filter is configured
|
||||
if self._config['personality']['channels'] == [] and self._config['main']['filter'] is None:
|
||||
# check if any of the non overlapping channels is free
|
||||
for ch in self._epoch.non_overlapping_channels:
|
||||
if ch not in busy_channels:
|
||||
self._epoch.non_overlapping_channels[ch] += 1
|
||||
logging.info("channel %d is free from %d epochs" % (ch, self._epoch.non_overlapping_channels[ch]))
|
||||
elif self._epoch.non_overlapping_channels[ch] > 0:
|
||||
self._epoch.non_overlapping_channels[ch] -= 1
|
||||
# report any channel that has been free for at least 3 epochs
|
||||
for ch, num_epochs_free in self._epoch.non_overlapping_channels.items():
|
||||
if num_epochs_free >= 3:
|
||||
logging.info("channel %d has been free for %d epochs" % (ch, num_epochs_free))
|
||||
self.set_free_channel(ch)
|
||||
|
||||
def recon(self):
|
||||
recon_time = self._config['personality']['recon_time']
|
||||
max_inactive = self._config['personality']['max_inactive_scale']
|
||||
recon_mul = self._config['personality']['recon_inactive_multiplier']
|
||||
channels = self._config['personality']['channels']
|
||||
|
||||
if self._epoch.inactive_for >= max_inactive:
|
||||
recon_time *= recon_mul
|
||||
|
||||
self._view.set('channel', '*')
|
||||
|
||||
if not channels:
|
||||
self._current_channel = 0
|
||||
logging.debug("RECON %ds" % recon_time)
|
||||
self.run('wifi.recon.channel clear')
|
||||
else:
|
||||
logging.debug("RECON %ds ON CHANNELS %s" % (recon_time, ','.join(map(str, channels))))
|
||||
try:
|
||||
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
|
||||
self.wait_for(recon_time, sleeping=False)
|
||||
|
||||
def _filter_included(self, ap):
|
||||
return self._filter is None or \
|
||||
self._filter.match(ap['hostname']) is not None or \
|
||||
self._filter.match(ap['mac']) is not None
|
||||
|
||||
def set_access_points(self, aps):
|
||||
self._access_points = aps
|
||||
plugins.on('wifi_update', self, aps)
|
||||
self._epoch.observe(aps, self._advertiser.peers() if self._advertiser is not None else ())
|
||||
return self._access_points
|
||||
|
||||
def get_access_points(self):
|
||||
whitelist = self._config['main']['whitelist']
|
||||
aps = []
|
||||
try:
|
||||
s = self.session()
|
||||
for ap in s['wifi']['aps']:
|
||||
if ap['hostname'] not in whitelist:
|
||||
if self._filter_included(ap):
|
||||
aps.append(ap)
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
|
||||
aps.sort(key=lambda ap: ap['channel'])
|
||||
return self.set_access_points(aps)
|
||||
|
||||
def get_access_points_by_channel(self):
|
||||
aps = self.get_access_points()
|
||||
channels = self._config['personality']['channels']
|
||||
grouped = {}
|
||||
|
||||
# group by channel
|
||||
for ap in aps:
|
||||
ch = ap['channel']
|
||||
# if we're sticking to a channel, skip anything
|
||||
# which is not on that channel
|
||||
if channels != [] and ch not in channels:
|
||||
continue
|
||||
|
||||
if ch not in grouped:
|
||||
grouped[ch] = [ap]
|
||||
else:
|
||||
grouped[ch].append(ap)
|
||||
|
||||
# sort by more populated channels
|
||||
return sorted(grouped.items(), key=lambda kv: len(kv[1]), reverse=True)
|
||||
|
||||
def _find_ap_sta_in(self, station_mac, ap_mac, session):
|
||||
for ap in session['wifi']['aps']:
|
||||
if ap['mac'] == ap_mac:
|
||||
for sta in ap['clients']:
|
||||
if sta['mac'] == station_mac:
|
||||
return (ap, sta)
|
||||
return (ap, {'mac': station_mac, 'vendor': ''})
|
||||
return None
|
||||
|
||||
def _update_uptime(self, s):
|
||||
secs = time.time() - self._started_at
|
||||
self._view.set('uptime', utils.secs_to_hhmmss(secs))
|
||||
self._view.set('epoch', '%04d' % self._epoch.epoch)
|
||||
|
||||
def _update_counters(self):
|
||||
tot_aps = len(self._access_points)
|
||||
tot_stas = sum(len(ap['clients']) for ap in self._access_points)
|
||||
if self._current_channel == 0:
|
||||
self._view.set('aps', '%d' % tot_aps)
|
||||
self._view.set('sta', '%d' % tot_stas)
|
||||
else:
|
||||
aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
|
||||
stas_on_channel = sum(
|
||||
[len(ap['clients']) for ap in self._access_points if ap['channel'] == self._current_channel])
|
||||
self._view.set('aps', '%d (%d)' % (aps_on_channel, tot_aps))
|
||||
self._view.set('sta', '%d (%d)' % (stas_on_channel, tot_stas))
|
||||
|
||||
def _update_handshakes(self, new_shakes=0):
|
||||
if new_shakes > 0:
|
||||
self._epoch.track(handshake=True, inc=new_shakes)
|
||||
|
||||
tot = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
|
||||
txt = '%d (%d)' % (len(self._handshakes), tot)
|
||||
|
||||
if self._last_pwnd is not None:
|
||||
txt += ' [%s]' % self._last_pwnd[:20]
|
||||
|
||||
self._view.set('shakes', txt)
|
||||
|
||||
if new_shakes > 0:
|
||||
self._view.on_handshakes(new_shakes)
|
||||
|
||||
def _update_advertisement(self, s):
|
||||
run_handshakes = len(self._handshakes)
|
||||
tot_handshakes = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
|
||||
started = s['started_at'].split('.')[0]
|
||||
started = datetime.strptime(started, '%Y-%m-%dT%H:%M:%S')
|
||||
started = time.mktime(started.timetuple())
|
||||
self._advertiser.update({ \
|
||||
'pwnd_run': run_handshakes,
|
||||
'pwnd_tot': tot_handshakes,
|
||||
'uptime': time.time() - started,
|
||||
'epoch': self._epoch.epoch})
|
||||
|
||||
def _update_peers(self):
|
||||
peer = self._advertiser.closest_peer()
|
||||
self._view.set_closest_peer(peer)
|
||||
|
||||
def _save_recovery_data(self):
|
||||
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
|
||||
with open(RECOVERY_DATA_FILE, 'w') as fp:
|
||||
data = {
|
||||
'started_at': self._started_at,
|
||||
'epoch': self._epoch.epoch,
|
||||
'history': self._history,
|
||||
'handshakes': self._handshakes,
|
||||
'last_pwnd': self._last_pwnd
|
||||
}
|
||||
json.dump(data, fp)
|
||||
|
||||
def _load_recovery_data(self, delete=True, no_exceptions=True):
|
||||
try:
|
||||
with open(RECOVERY_DATA_FILE, 'rt') as fp:
|
||||
data = json.load(fp)
|
||||
logging.info("found recovery data: %s" % data)
|
||||
self._started_at = data['started_at']
|
||||
self._epoch.epoch = data['epoch']
|
||||
self._handshakes = data['handshakes']
|
||||
self._history = data['history']
|
||||
self._last_pwnd = data['last_pwnd']
|
||||
|
||||
if delete:
|
||||
logging.info("deleting %s" % RECOVERY_DATA_FILE)
|
||||
os.unlink(RECOVERY_DATA_FILE)
|
||||
except:
|
||||
if not no_exceptions:
|
||||
raise
|
||||
|
||||
def _event_poller(self):
|
||||
self._load_recovery_data()
|
||||
|
||||
self.run('events.clear')
|
||||
|
||||
logging.debug("event polling started ...")
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
new_shakes = 0
|
||||
s = self.session()
|
||||
self._update_uptime(s)
|
||||
|
||||
if self._advertiser is not None:
|
||||
self._update_advertisement(s)
|
||||
self._update_peers()
|
||||
|
||||
self._update_counters()
|
||||
|
||||
try:
|
||||
for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
|
||||
filename = h['data']['file']
|
||||
sta_mac = h['data']['station']
|
||||
ap_mac = h['data']['ap']
|
||||
key = "%s -> %s" % (sta_mac, ap_mac)
|
||||
|
||||
if key not in self._handshakes:
|
||||
self._handshakes[key] = h
|
||||
new_shakes += 1
|
||||
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
|
||||
if ap_and_station is None:
|
||||
logging.warning("!!! captured new handshake: %s !!!" % key)
|
||||
self._last_pwnd = ap_mac
|
||||
plugins.on('handshake', self, filename, ap_mac, sta_mac)
|
||||
else:
|
||||
(ap, sta) = ap_and_station
|
||||
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
|
||||
'hostname'] != '<hidden>' else ap_mac
|
||||
logging.warning("!!! captured new handshake on channel %d: %s (%s) -> %s [%s (%s)] !!!" % ( \
|
||||
ap['channel'],
|
||||
sta['mac'], sta['vendor'],
|
||||
ap['hostname'], ap['mac'], ap['vendor']))
|
||||
plugins.on('handshake', self, filename, ap, sta)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
|
||||
finally:
|
||||
self._update_handshakes(new_shakes)
|
||||
|
||||
def start_event_polling(self):
|
||||
_thread.start_new_thread(self._event_poller, ())
|
||||
|
||||
def is_module_running(self, module):
|
||||
s = self.session()
|
||||
for m in s['modules']:
|
||||
if m['name'] == module:
|
||||
return m['running']
|
||||
return False
|
||||
|
||||
def start_module(self, module):
|
||||
self.run('%s on' % module)
|
||||
|
||||
def restart_module(self, module):
|
||||
self.run('%s off; %s on' % (module, module))
|
||||
|
||||
def _has_handshake(self, bssid):
|
||||
for key in self._handshakes:
|
||||
if bssid.lower() in key:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _should_interact(self, who):
|
||||
if self._has_handshake(who):
|
||||
return False
|
||||
|
||||
elif who not in self._history:
|
||||
self._history[who] = 1
|
||||
return True
|
||||
|
||||
else:
|
||||
self._history[who] += 1
|
||||
|
||||
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'])
|
||||
return
|
||||
|
||||
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
|
||||
self._view.on_assoc(ap)
|
||||
|
||||
try:
|
||||
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients]..." % ( \
|
||||
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients'])))
|
||||
self.run('wifi.assoc %s' % ap['mac'])
|
||||
self._epoch.track(assoc=True)
|
||||
except Exception as e:
|
||||
self._on_error(ap['mac'], e)
|
||||
|
||||
plugins.on('association', self, ap)
|
||||
if throttle > 0:
|
||||
time.sleep(throttle)
|
||||
self._view.on_normal()
|
||||
|
||||
def deauth(self, ap, sta, throttle=0):
|
||||
if self.is_stale():
|
||||
logging.debug("recon is stale, skipping deauth(%s)" % sta['mac'])
|
||||
return
|
||||
|
||||
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
|
||||
self._view.on_deauth(sta)
|
||||
|
||||
try:
|
||||
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d ..." % (
|
||||
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel']))
|
||||
self.run('wifi.deauth %s' % sta['mac'])
|
||||
self._epoch.track(deauth=True)
|
||||
except Exception as e:
|
||||
self._on_error(sta['mac'], e)
|
||||
|
||||
plugins.on('deauthentication', self, ap, sta)
|
||||
if throttle > 0:
|
||||
time.sleep(throttle)
|
||||
self._view.on_normal()
|
||||
|
||||
def set_channel(self, channel, verbose=True):
|
||||
if self.is_stale():
|
||||
logging.debug("recon is stale, skipping set_channel(%d)" % channel)
|
||||
return
|
||||
|
||||
# if in the previous loop no client stations has been deauthenticated
|
||||
# and only association frames have been sent, we don't need to wait
|
||||
# very long before switching channel as we don't have to wait for
|
||||
# such client stations to reconnect in order to sniff the handshake.
|
||||
wait = 0
|
||||
if self._epoch.did_deauth:
|
||||
wait = self._config['personality']['hop_recon_time']
|
||||
elif self._epoch.did_associate:
|
||||
wait = self._config['personality']['min_recon_time']
|
||||
|
||||
if channel != self._current_channel:
|
||||
if self._current_channel != 0 and wait > 0:
|
||||
if verbose:
|
||||
logging.info("waiting for %ds on channel %d ..." % (wait, self._current_channel))
|
||||
else:
|
||||
logging.debug("waiting for %ds on channel %d ..." % (wait, self._current_channel))
|
||||
self.wait_for(wait)
|
||||
if verbose and self._epoch.any_activity:
|
||||
logging.info("CHANNEL %d" % channel)
|
||||
try:
|
||||
self.run('wifi.recon.channel %d' % channel)
|
||||
self._current_channel = channel
|
||||
self._epoch.track(hop=True)
|
||||
self._view.set('channel', '%d' % channel)
|
||||
|
||||
plugins.on('channel_hop', self, channel)
|
||||
|
||||
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()
|
||||
logging.warning("rebooting the system ...")
|
||||
os.system("/usr/bin/sync")
|
||||
os.system("/usr/sbin/shutdown -r now")
|
||||
|
||||
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
|
42
pwnagotchi/ai/__init__.py
Normal file
42
pwnagotchi/ai/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
import os
|
||||
|
||||
# https://stackoverflow.com/questions/40426502/is-there-a-way-to-suppress-the-messages-tensorflow-prints/40426709
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # or any {'0', '1', '2'}
|
||||
import warnings
|
||||
|
||||
# https://stackoverflow.com/questions/15777951/how-to-suppress-pandas-future-warning
|
||||
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def load(config, agent, epoch, from_disk=True):
|
||||
config = config['ai']
|
||||
if not config['enabled']:
|
||||
logging.info("ai disabled")
|
||||
return False
|
||||
|
||||
logging.info("[ai] bootstrapping dependencies ...")
|
||||
|
||||
from stable_baselines import A2C
|
||||
from stable_baselines.common.policies import MlpLstmPolicy
|
||||
from stable_baselines.common.vec_env import DummyVecEnv
|
||||
|
||||
import pwnagotchi.ai.gym as wrappers
|
||||
|
||||
env = wrappers.Environment(agent, epoch)
|
||||
env = DummyVecEnv([lambda: env])
|
||||
|
||||
logging.info("[ai] bootstrapping model ...")
|
||||
|
||||
a2c = A2C(MlpLstmPolicy, env, **config['params'])
|
||||
|
||||
if from_disk and os.path.exists(config['path']):
|
||||
logging.info("[ai] loading %s ..." % config['path'])
|
||||
a2c.load(config['path'], env)
|
||||
else:
|
||||
logging.info("[ai] model created:")
|
||||
for key, value in config['params'].items():
|
||||
logging.info(" %s: %s" % (key, value))
|
||||
|
||||
return a2c
|
205
pwnagotchi/ai/epoch.py
Normal file
205
pwnagotchi/ai/epoch.py
Normal file
@ -0,0 +1,205 @@
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
|
||||
from pwnagotchi.ai.reward import RewardFunction
|
||||
|
||||
|
||||
class Epoch(object):
|
||||
def __init__(self, config):
|
||||
self.epoch = 0
|
||||
self.config = config
|
||||
# how many consecutive epochs with no activity
|
||||
self.inactive_for = 0
|
||||
# how many consecutive epochs with activity
|
||||
self.active_for = 0
|
||||
# number of epochs with no visible access points
|
||||
self.blind_for = 0
|
||||
# did deauth in this epoch in the current channel?
|
||||
self.did_deauth = False
|
||||
# number of deauths in this epoch
|
||||
self.num_deauths = 0
|
||||
# did associate in this epoch in the current channel?
|
||||
self.did_associate = False
|
||||
# number of associations in this epoch
|
||||
self.num_assocs = 0
|
||||
# number of assocs or deauths missed
|
||||
self.num_missed = 0
|
||||
# did get any handshake in this epoch?
|
||||
self.did_handshakes = False
|
||||
# number of handshakes captured in this epoch
|
||||
self.num_shakes = 0
|
||||
# number of channels hops
|
||||
self.num_hops = 0
|
||||
# number of seconds sleeping
|
||||
self.num_slept = 0
|
||||
# any activity at all during this epoch?
|
||||
self.any_activity = False
|
||||
# when the current epoch started
|
||||
self.epoch_started = time.time()
|
||||
# last epoch duration
|
||||
self.epoch_duration = 0
|
||||
# https://www.metageek.com/training/resources/why-channels-1-6-11.html
|
||||
self.non_overlapping_channels = {1: 0, 6: 0, 11: 0}
|
||||
# observation vectors
|
||||
self._observation = {
|
||||
'aps_histogram': [0.0] * wifi.NumChannels,
|
||||
'sta_histogram': [0.0] * wifi.NumChannels,
|
||||
'peers_histogram': [0.0] * wifi.NumChannels
|
||||
}
|
||||
self._observation_ready = threading.Event()
|
||||
self._epoch_data = {}
|
||||
self._epoch_data_ready = threading.Event()
|
||||
self._reward = RewardFunction()
|
||||
|
||||
def wait_for_epoch_data(self, with_observation=True, timeout=None):
|
||||
# if with_observation:
|
||||
# self._observation_ready.wait(timeout)
|
||||
# self._observation_ready.clear()
|
||||
self._epoch_data_ready.wait(timeout)
|
||||
self._epoch_data_ready.clear()
|
||||
return self._epoch_data if with_observation is False else {**self._observation, **self._epoch_data}
|
||||
|
||||
def data(self):
|
||||
return self._epoch_data
|
||||
|
||||
def observe(self, aps, peers):
|
||||
num_aps = len(aps)
|
||||
if num_aps == 0:
|
||||
self.blind_for += 1
|
||||
else:
|
||||
self.blind_for = 0
|
||||
|
||||
num_aps = len(aps) + 1e-10
|
||||
num_sta = sum(len(ap['clients']) for ap in aps) + 1e-10
|
||||
num_peers = len(peers) + 1e-10
|
||||
|
||||
aps_per_chan = [0.0] * wifi.NumChannels
|
||||
sta_per_chan = [0.0] * wifi.NumChannels
|
||||
peers_per_chan = [0.0] * wifi.NumChannels
|
||||
|
||||
for ap in aps:
|
||||
ch_idx = ap['channel'] - 1
|
||||
try:
|
||||
aps_per_chan[ch_idx] += 1.0
|
||||
sta_per_chan[ch_idx] += len(ap['clients'])
|
||||
except IndexError as e:
|
||||
logging.error("got data on channel %d, we can store %d channels" % (ap['channel'], wifi.NumChannels))
|
||||
|
||||
for peer in peers:
|
||||
try:
|
||||
peers_per_chan[peer.last_channel - 1] += 1.0
|
||||
except IndexError as e:
|
||||
logging.error(
|
||||
"got peer data on channel %d, we can store %d channels" % (peer.last_channel, wifi.NumChannels))
|
||||
|
||||
# normalize
|
||||
aps_per_chan = [e / num_aps for e in aps_per_chan]
|
||||
sta_per_chan = [e / num_sta for e in sta_per_chan]
|
||||
peers_per_chan = [e / num_peers for e in peers_per_chan]
|
||||
|
||||
self._observation = {
|
||||
'aps_histogram': aps_per_chan,
|
||||
'sta_histogram': sta_per_chan,
|
||||
'peers_histogram': peers_per_chan
|
||||
}
|
||||
self._observation_ready.set()
|
||||
|
||||
def track(self, deauth=False, assoc=False, handshake=False, hop=False, sleep=False, miss=False, inc=1):
|
||||
if deauth:
|
||||
self.num_deauths += inc
|
||||
self.did_deauth = True
|
||||
self.any_activity = True
|
||||
|
||||
if assoc:
|
||||
self.num_assocs += inc
|
||||
self.did_associate = True
|
||||
self.any_activity = True
|
||||
|
||||
if miss:
|
||||
self.num_missed += inc
|
||||
|
||||
if hop:
|
||||
self.num_hops += inc
|
||||
# these two are used in order to determine the sleep time in seconds
|
||||
# before switching to a new channel ... if nothing happened so far
|
||||
# during this epoch on the current channel, we will sleep less
|
||||
self.did_deauth = False
|
||||
self.did_associate = False
|
||||
|
||||
if handshake:
|
||||
self.num_shakes += inc
|
||||
self.did_handshakes = True
|
||||
|
||||
if sleep:
|
||||
self.num_slept += inc
|
||||
|
||||
def next(self):
|
||||
if self.any_activity is False and self.did_handshakes is False:
|
||||
self.inactive_for += 1
|
||||
self.active_for = 0
|
||||
else:
|
||||
self.active_for += 1
|
||||
self.inactive_for = 0
|
||||
|
||||
now = time.time()
|
||||
cpu = pwnagotchi.cpu_load()
|
||||
mem = pwnagotchi.mem_usage()
|
||||
temp = pwnagotchi.temperature()
|
||||
|
||||
self.epoch_duration = now - self.epoch_started
|
||||
|
||||
# cache the state of this epoch for other threads to read
|
||||
self._epoch_data = {
|
||||
'duration_secs': self.epoch_duration,
|
||||
'slept_for_secs': self.num_slept,
|
||||
'blind_for_epochs': self.blind_for,
|
||||
'inactive_for_epochs': self.inactive_for,
|
||||
'active_for_epochs': self.active_for,
|
||||
'missed_interactions': self.num_missed,
|
||||
'num_hops': self.num_hops,
|
||||
'num_deauths': self.num_deauths,
|
||||
'num_associations': self.num_assocs,
|
||||
'num_handshakes': self.num_shakes,
|
||||
'cpu_load': cpu,
|
||||
'mem_usage': mem,
|
||||
'temperature': temp
|
||||
}
|
||||
|
||||
self._epoch_data['reward'] = self._reward(self.epoch + 1, self._epoch_data)
|
||||
self._epoch_data_ready.set()
|
||||
|
||||
logging.info("[epoch %d] duration=%s slept_for=%s blind=%d inactive=%d active=%d hops=%d missed=%d "
|
||||
"deauths=%d assocs=%d handshakes=%d cpu=%d%% mem=%d%% temperature=%dC reward=%s" % (
|
||||
self.epoch,
|
||||
utils.secs_to_hhmmss(self.epoch_duration),
|
||||
utils.secs_to_hhmmss(self.num_slept),
|
||||
self.blind_for,
|
||||
self.inactive_for,
|
||||
self.active_for,
|
||||
self.num_hops,
|
||||
self.num_missed,
|
||||
self.num_deauths,
|
||||
self.num_assocs,
|
||||
self.num_shakes,
|
||||
cpu * 100,
|
||||
mem * 100,
|
||||
temp,
|
||||
self._epoch_data['reward']))
|
||||
|
||||
self.epoch += 1
|
||||
self.epoch_started = now
|
||||
self.did_deauth = False
|
||||
self.num_deauths = 0
|
||||
self.did_associate = False
|
||||
self.num_assocs = 0
|
||||
self.num_missed = 0
|
||||
self.did_handshakes = False
|
||||
self.num_shakes = 0
|
||||
self.num_hops = 0
|
||||
self.num_slept = 0
|
||||
self.any_activity = False
|
60
pwnagotchi/ai/featurizer.py
Normal file
60
pwnagotchi/ai/featurizer.py
Normal file
@ -0,0 +1,60 @@
|
||||
import numpy as np
|
||||
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
|
||||
MAX_EPOCH_DURATION = 1024
|
||||
|
||||
histogram_size = wifi.NumChannels
|
||||
|
||||
shape = (1,
|
||||
# aps per channel
|
||||
histogram_size +
|
||||
# clients per channel
|
||||
histogram_size +
|
||||
# peers per channel
|
||||
histogram_size +
|
||||
# duration
|
||||
1 +
|
||||
# inactive
|
||||
1 +
|
||||
# active
|
||||
1 +
|
||||
# missed
|
||||
1 +
|
||||
# hops
|
||||
1 +
|
||||
# deauths
|
||||
1 +
|
||||
# assocs
|
||||
1 +
|
||||
# handshakes
|
||||
1)
|
||||
|
||||
|
||||
def featurize(state, step):
|
||||
tot_epochs = step + 1e-10
|
||||
tot_interactions = (state['num_deauths'] + state['num_associations']) + 1e-10
|
||||
return np.concatenate((
|
||||
# aps per channel
|
||||
state['aps_histogram'],
|
||||
# clients per channel
|
||||
state['sta_histogram'],
|
||||
# peers per channel
|
||||
state['peers_histogram'],
|
||||
# duration
|
||||
[np.clip(state['duration_secs'] / MAX_EPOCH_DURATION, 0.0, 1.0)],
|
||||
# inactive
|
||||
[state['inactive_for_epochs'] / tot_epochs],
|
||||
# active
|
||||
[state['active_for_epochs'] / tot_epochs],
|
||||
# missed
|
||||
[state['missed_interactions'] / tot_interactions],
|
||||
# hops
|
||||
[state['num_hops'] / wifi.NumChannels],
|
||||
# deauths
|
||||
[state['num_deauths'] / tot_interactions],
|
||||
# assocs
|
||||
[state['num_associations'] / tot_interactions],
|
||||
# handshakes
|
||||
[state['num_handshakes'] / tot_interactions],
|
||||
))
|
145
pwnagotchi/ai/gym.py
Normal file
145
pwnagotchi/ai/gym.py
Normal file
@ -0,0 +1,145 @@
|
||||
import logging
|
||||
import gym
|
||||
from gym import spaces
|
||||
import numpy as np
|
||||
|
||||
import pwnagotchi.ai.featurizer as featurizer
|
||||
import pwnagotchi.ai.reward as reward
|
||||
from pwnagotchi.ai.parameter import Parameter
|
||||
|
||||
|
||||
class Environment(gym.Env):
|
||||
metadata = {'render.modes': ['human']}
|
||||
params = [
|
||||
Parameter('min_rssi', min_value=-200, max_value=-50),
|
||||
Parameter('ap_ttl', min_value=30, max_value=600),
|
||||
Parameter('sta_ttl', min_value=60, max_value=300),
|
||||
|
||||
Parameter('recon_time', min_value=5, max_value=60),
|
||||
Parameter('max_inactive_scale', min_value=3, max_value=10),
|
||||
Parameter('recon_inactive_multiplier', min_value=1, max_value=3),
|
||||
Parameter('hop_recon_time', min_value=5, max_value=60),
|
||||
Parameter('min_recon_time', min_value=1, max_value=30),
|
||||
Parameter('max_interactions', min_value=1, max_value=25),
|
||||
Parameter('max_misses_for_recon', min_value=3, max_value=10),
|
||||
Parameter('excited_num_epochs', min_value=5, max_value=30),
|
||||
Parameter('bored_num_epochs', min_value=5, max_value=30),
|
||||
Parameter('sad_num_epochs', min_value=5, max_value=30),
|
||||
]
|
||||
|
||||
def __init__(self, agent, epoch):
|
||||
super(Environment, self).__init__()
|
||||
self._agent = agent
|
||||
self._epoch = epoch
|
||||
self._epoch_num = 0
|
||||
self._last_render = None
|
||||
|
||||
channels = agent.supported_channels()
|
||||
Environment.params += [
|
||||
Parameter('_channel_%d' % ch, min_value=0, max_value=1, meta=ch + 1) for ch in
|
||||
range(featurizer.histogram_size) if ch + 1 in channels
|
||||
]
|
||||
|
||||
self.last = {
|
||||
'reward': 0.0,
|
||||
'observation': None,
|
||||
'policy': None,
|
||||
'params': {},
|
||||
'state': None,
|
||||
'state_v': None
|
||||
}
|
||||
|
||||
self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.params if p.trainable])
|
||||
self.observation_space = spaces.Box(low=0, high=1, shape=featurizer.shape, dtype=np.float32)
|
||||
self.reward_range = reward.range
|
||||
|
||||
@staticmethod
|
||||
def policy_size():
|
||||
return len(list(p for p in Environment.params if p.trainable))
|
||||
|
||||
@staticmethod
|
||||
def policy_to_params(policy):
|
||||
num = len(policy)
|
||||
params = {}
|
||||
|
||||
assert len(Environment.params) == num
|
||||
|
||||
channels = []
|
||||
|
||||
for i in range(num):
|
||||
param = Environment.params[i]
|
||||
|
||||
if '_channel' not in param.name:
|
||||
params[param.name] = param.to_param_value(policy[i])
|
||||
else:
|
||||
has_chan = param.to_param_value(policy[i])
|
||||
# print("%s policy:%s bool:%s" % (param.name, policy[i], has_chan))
|
||||
chan = param.meta
|
||||
if has_chan:
|
||||
channels.append(chan)
|
||||
|
||||
params['channels'] = channels
|
||||
|
||||
return params
|
||||
|
||||
def _next_epoch(self):
|
||||
logging.debug("[ai] waiting for epoch to finish ...")
|
||||
return self._epoch.wait_for_epoch_data()
|
||||
|
||||
def _apply_policy(self, policy):
|
||||
new_params = Environment.policy_to_params(policy)
|
||||
self.last['policy'] = policy
|
||||
self.last['params'] = new_params
|
||||
self._agent.on_ai_policy(new_params)
|
||||
|
||||
def step(self, policy):
|
||||
# create the parameters from the policy and update
|
||||
# update them in the algorithm
|
||||
self._apply_policy(policy)
|
||||
self._epoch_num += 1
|
||||
|
||||
# wait for the algorithm to run with the new parameters
|
||||
state = self._next_epoch()
|
||||
|
||||
self.last['reward'] = state['reward']
|
||||
self.last['state'] = state
|
||||
self.last['state_v'] = featurizer.featurize(state, self._epoch_num)
|
||||
|
||||
self._agent.on_ai_step()
|
||||
|
||||
return self.last['state_v'], self.last['reward'], not self._agent.is_training(), {}
|
||||
|
||||
def reset(self):
|
||||
# logging.info("[ai] resetting environment ...")
|
||||
self._epoch_num = 0
|
||||
state = self._next_epoch()
|
||||
self.last['state'] = state
|
||||
self.last['state_v'] = featurizer.featurize(state, 1)
|
||||
return self.last['state_v']
|
||||
|
||||
def _render_histogram(self, hist):
|
||||
for ch in range(featurizer.histogram_size):
|
||||
if hist[ch]:
|
||||
logging.info(" CH %d: %s" % (ch + 1, hist[ch]))
|
||||
|
||||
def render(self, mode='human', close=False, force=False):
|
||||
# when using a vectorialized environment, render gets called twice
|
||||
# avoid rendering the same data
|
||||
if self._last_render == self._epoch_num:
|
||||
return
|
||||
|
||||
if not self._agent.is_training() and not force:
|
||||
return
|
||||
|
||||
self._last_render = self._epoch_num
|
||||
|
||||
logging.info("[ai] --- training epoch %d/%d ---" % (self._epoch_num, self._agent.training_epochs()))
|
||||
logging.info("[ai] REWARD: %f" % self.last['reward'])
|
||||
|
||||
logging.debug("[ai] policy: %s" % ', '.join("%s:%s" % (name, value) for name, value in self.last['params'].items()))
|
||||
|
||||
logging.info("[ai] observation:")
|
||||
for name, value in self.last['state'].items():
|
||||
if 'histogram' in name:
|
||||
logging.info(" %s" % name.replace('_histogram', ''))
|
||||
self._render_histogram(value)
|
30
pwnagotchi/ai/parameter.py
Normal file
30
pwnagotchi/ai/parameter.py
Normal file
@ -0,0 +1,30 @@
|
||||
from gym import spaces
|
||||
|
||||
|
||||
class Parameter(object):
|
||||
def __init__(self, name, value=0.0, min_value=0, max_value=2, meta=None, trainable=True):
|
||||
self.name = name
|
||||
self.trainable = trainable
|
||||
self.meta = meta
|
||||
self.value = value
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value + 1
|
||||
|
||||
# gym.space.Discrete is within [0, 1, 2, ..., n-1]
|
||||
if self.min_value < 0:
|
||||
self.scale_factor = abs(self.min_value)
|
||||
elif self.min_value > 0:
|
||||
self.scale_factor = -self.min_value
|
||||
else:
|
||||
self.scale_factor = 0
|
||||
|
||||
def space_size(self):
|
||||
return self.max_value + self.scale_factor
|
||||
|
||||
def space(self):
|
||||
return spaces.Discrete(self.max_value + self.scale_factor)
|
||||
|
||||
def to_param_value(self, policy_v):
|
||||
self.value = policy_v - self.scale_factor
|
||||
assert self.min_value <= self.value <= self.max_value
|
||||
return int(self.value)
|
21
pwnagotchi/ai/reward.py
Normal file
21
pwnagotchi/ai/reward.py
Normal file
@ -0,0 +1,21 @@
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
|
||||
range = (-.7, 1.02)
|
||||
fuck_zero = 1e-20
|
||||
|
||||
|
||||
class RewardFunction(object):
|
||||
def __call__(self, epoch_n, state):
|
||||
tot_epochs = epoch_n + fuck_zero
|
||||
tot_interactions = max(state['num_deauths'] + state['num_associations'], state['num_handshakes']) + fuck_zero
|
||||
tot_channels = wifi.NumChannels
|
||||
|
||||
h = state['num_handshakes'] / tot_interactions
|
||||
a = .2 * (state['active_for_epochs'] / tot_epochs)
|
||||
c = .1 * (state['num_hops'] / tot_channels)
|
||||
|
||||
b = -.3 * (state['blind_for_epochs'] / tot_epochs)
|
||||
m = -.3 * (state['missed_interactions'] / tot_interactions)
|
||||
i = -.2 * (state['inactive_for_epochs'] / tot_epochs)
|
||||
|
||||
return h + a + c + b + i + m
|
191
pwnagotchi/ai/train.py
Normal file
191
pwnagotchi/ai/train.py
Normal file
@ -0,0 +1,191 @@
|
||||
import _thread
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.ai as ai
|
||||
from pwnagotchi.ai.epoch import Epoch
|
||||
|
||||
|
||||
class Stats(object):
|
||||
def __init__(self, path, events_receiver):
|
||||
self._lock = threading.Lock()
|
||||
self._receiver = events_receiver
|
||||
|
||||
self.path = path
|
||||
self.born_at = time.time()
|
||||
# total epochs lived (trained + just eval)
|
||||
self.epochs_lived = 0
|
||||
# total training epochs
|
||||
self.epochs_trained = 0
|
||||
|
||||
self.worst_reward = 0.0
|
||||
self.best_reward = 0.0
|
||||
|
||||
self.load()
|
||||
|
||||
def on_epoch(self, data, training):
|
||||
best_r = False
|
||||
worst_r = False
|
||||
with self._lock:
|
||||
reward = data['reward']
|
||||
if reward < self.worst_reward:
|
||||
self.worst_reward = reward
|
||||
worst_r = True
|
||||
|
||||
elif reward > self.best_reward:
|
||||
best_r = True
|
||||
self.best_reward = reward
|
||||
|
||||
self.epochs_lived += 1
|
||||
if training:
|
||||
self.epochs_trained += 1
|
||||
|
||||
self.save()
|
||||
|
||||
if best_r:
|
||||
self._receiver.on_ai_best_reward(reward)
|
||||
elif worst_r:
|
||||
self._receiver.on_ai_worst_reward(reward)
|
||||
|
||||
def load(self):
|
||||
with self._lock:
|
||||
if os.path.exists(self.path) and os.path.getsize(self.path) > 0:
|
||||
logging.info("[ai] loading %s" % self.path)
|
||||
with open(self.path, 'rt') as fp:
|
||||
obj = json.load(fp)
|
||||
|
||||
self.born_at = obj['born_at']
|
||||
self.epochs_lived, self.epochs_trained = obj['epochs_lived'], obj['epochs_trained']
|
||||
self.best_reward, self.worst_reward = obj['rewards']['best'], obj['rewards']['worst']
|
||||
|
||||
def save(self):
|
||||
with self._lock:
|
||||
logging.info("[ai] saving %s" % self.path)
|
||||
|
||||
data = json.dumps({
|
||||
'born_at': self.born_at,
|
||||
'epochs_lived': self.epochs_lived,
|
||||
'epochs_trained': self.epochs_trained,
|
||||
'rewards': {
|
||||
'best': self.best_reward,
|
||||
'worst': self.worst_reward
|
||||
}
|
||||
})
|
||||
|
||||
temp = "%s.tmp" % self.path
|
||||
with open(temp, 'wt') as fp:
|
||||
fp.write(data)
|
||||
|
||||
os.replace(temp, self.path)
|
||||
|
||||
|
||||
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']
|
||||
self._stats = Stats("%s.json" % os.path.splitext(self._nn_path)[0], self)
|
||||
|
||||
def set_training(self, training, for_epochs=0):
|
||||
self._is_training = training
|
||||
self._training_epochs = for_epochs
|
||||
|
||||
if training:
|
||||
plugins.on('ai_training_start', self, for_epochs)
|
||||
else:
|
||||
plugins.on('ai_training_end', self)
|
||||
|
||||
def is_training(self):
|
||||
return self._is_training
|
||||
|
||||
def training_epochs(self):
|
||||
return self._training_epochs
|
||||
|
||||
def start_ai(self):
|
||||
_thread.start_new_thread(self._ai_worker, ())
|
||||
|
||||
def _save_ai(self):
|
||||
logging.info("[ai] saving model to %s ..." % self._nn_path)
|
||||
temp = "%s.tmp" % self._nn_path
|
||||
self._model.save(temp)
|
||||
os.replace(temp, self._nn_path)
|
||||
|
||||
def on_ai_step(self):
|
||||
self._model.env.render()
|
||||
|
||||
if self._is_training:
|
||||
self._save_ai()
|
||||
|
||||
self._stats.on_epoch(self._epoch.data(), self._is_training)
|
||||
|
||||
def on_ai_training_step(self, _locals, _globals):
|
||||
self._model.env.render()
|
||||
plugins.on('ai_training_step', self, _locals, _globals)
|
||||
|
||||
def on_ai_policy(self, new_params):
|
||||
plugins.on('ai_policy', self, new_params)
|
||||
logging.info("[ai] setting new policy:")
|
||||
for name, value in new_params.items():
|
||||
if name in self._config['personality']:
|
||||
curr_value = self._config['personality'][name]
|
||||
if curr_value != value:
|
||||
logging.info("[ai] ! %s: %s -> %s" % (name, curr_value, value))
|
||||
self._config['personality'][name] = value
|
||||
else:
|
||||
logging.error("[ai] param %s not in personality configuration!" % name)
|
||||
|
||||
self.run('set wifi.ap.ttl %d' % self._config['personality']['ap_ttl'])
|
||||
self.run('set wifi.sta.ttl %d' % self._config['personality']['sta_ttl'])
|
||||
self.run('set wifi.rssi.min %d' % self._config['personality']['min_rssi'])
|
||||
|
||||
def on_ai_ready(self):
|
||||
self._view.on_ai_ready()
|
||||
plugins.on('ai_ready', self)
|
||||
|
||||
def on_ai_best_reward(self, r):
|
||||
logging.info("[ai] best reward so far: %s" % r)
|
||||
self._view.on_motivated(r)
|
||||
plugins.on('ai_best_reward', self, r)
|
||||
|
||||
def on_ai_worst_reward(self, r):
|
||||
logging.info("[ai] worst reward so far: %s" % r)
|
||||
self._view.on_demotivated(r)
|
||||
plugins.on('ai_worst_reward', self, r)
|
||||
|
||||
def _ai_worker(self):
|
||||
self._model = ai.load(self._config, self, self._epoch)
|
||||
|
||||
if self._model:
|
||||
self.on_ai_ready()
|
||||
|
||||
epochs_per_episode = self._config['ai']['epochs_per_episode']
|
||||
|
||||
obs = None
|
||||
while True:
|
||||
self._model.env.render()
|
||||
# enter in training mode?
|
||||
if random.random() > self._config['ai']['laziness']:
|
||||
logging.info("[ai] learning for %d epochs ..." % epochs_per_episode)
|
||||
try:
|
||||
self.set_training(True, epochs_per_episode)
|
||||
self._model.learn(total_timesteps=epochs_per_episode, callback=self.on_ai_training_step)
|
||||
except Exception as e:
|
||||
logging.exception("[ai] error while training")
|
||||
finally:
|
||||
self.set_training(False)
|
||||
obs = self._model.env.reset()
|
||||
# init the first time
|
||||
elif obs is None:
|
||||
obs = self._model.env.reset()
|
||||
|
||||
# run the inference
|
||||
action, _ = self._model.predict(obs)
|
||||
obs, _, _, _ = self._model.env.step(action)
|
16
pwnagotchi/ai/utils.py
Normal file
16
pwnagotchi/ai/utils.py
Normal file
@ -0,0 +1,16 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def normalize(v, min_v, max_v):
|
||||
return (v - min_v) / (max_v - min_v)
|
||||
|
||||
|
||||
def as_batches(x, y, batch_size, shuffle=True):
|
||||
x_size = len(x)
|
||||
assert x_size == len(y)
|
||||
|
||||
indices = np.random.permutation(x_size) if shuffle else None
|
||||
|
||||
for offset in range(0, x_size - batch_size + 1, batch_size):
|
||||
excerpt = indices[offset:offset + batch_size] if shuffle else slice(offset, offset + batch_size)
|
||||
yield x[excerpt], y[excerpt]
|
39
pwnagotchi/bettercap.py
Normal file
39
pwnagotchi/bettercap.py
Normal file
@ -0,0 +1,39 @@
|
||||
import logging
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, hostname='localhost', scheme='http', port=8081, username='user', password='pass'):
|
||||
self.hostname = hostname
|
||||
self.scheme = scheme
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.url = "%s://%s:%d/api" % (scheme, hostname, port)
|
||||
self.auth = HTTPBasicAuth(username, password)
|
||||
|
||||
def _decode(self, r, verbose_errors=True):
|
||||
try:
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
if r.status_code == 200:
|
||||
logging.error("error while decoding json: error='%s' resp='%s'" % (e, r.text))
|
||||
else:
|
||||
err = "error %d: %s" % (r.status_code, r.text.strip())
|
||||
if verbose_errors:
|
||||
logging.info(err)
|
||||
raise Exception(err)
|
||||
return r.text
|
||||
|
||||
def session(self):
|
||||
r = requests.get("%s/session" % self.url, auth=self.auth)
|
||||
return self._decode(r)
|
||||
|
||||
def events(self):
|
||||
r = requests.get("%s/events" % self.url, auth=self.auth)
|
||||
return self._decode(r)
|
||||
|
||||
def run(self, command, verbose_errors=True):
|
||||
r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command})
|
||||
return self._decode(r, verbose_errors=verbose_errors)
|
176
pwnagotchi/defaults.yml
Normal file
176
pwnagotchi/defaults.yml
Normal file
@ -0,0 +1,176 @@
|
||||
# main algorithm configuration
|
||||
main:
|
||||
# currently implemented: en (default), de, el, fr, it, mk, nl, ru, se
|
||||
lang: en
|
||||
# custom plugins path, if null only default plugins with be loaded
|
||||
custom_plugins:
|
||||
# which plugins to load and enable
|
||||
plugins:
|
||||
auto-update:
|
||||
enabled: false
|
||||
interval: 1 # every day
|
||||
auto-backup:
|
||||
enabled: false
|
||||
interval: 1 # every day
|
||||
files:
|
||||
- /root/brain.nn
|
||||
- /root/brain.json
|
||||
- /root/custom.yml
|
||||
- /root/handshakes
|
||||
- /etc/ssh
|
||||
- /etc/hostname
|
||||
- /etc/hosts
|
||||
- /etc/motd
|
||||
- /var/log/pwnagotchi.log
|
||||
commands:
|
||||
- 'tar czf /tmp/backup.tar.gz {files}'
|
||||
- 'scp /tmp/backup.tar.gz pwnagotchi@10.0.0.1:/home/pwnagotchi/backups/backup-$(date +%s).tar.gz'
|
||||
gps:
|
||||
enabled: false
|
||||
twitter:
|
||||
enabled: false
|
||||
consumer_key: aaa
|
||||
consumer_secret: aaa
|
||||
access_token_key: aaa
|
||||
access_token_secret: aaa
|
||||
onlinehashcrack:
|
||||
enabled: false
|
||||
email: ~
|
||||
wpa-sec:
|
||||
enabled: false
|
||||
api_key: ~
|
||||
|
||||
# monitor interface to use
|
||||
iface: mon0
|
||||
# command to run to bring the mon interface up in case it's not up already
|
||||
mon_start_cmd: /usr/bin/monstart
|
||||
mon_stop_cmd: /usr/bin/monstop
|
||||
mon_max_blind_epochs: 50
|
||||
# log file
|
||||
log: /var/log/pwnagotchi.log
|
||||
# if true, will not restart the wifi module
|
||||
no_restart: false
|
||||
# access points to ignore
|
||||
whitelist:
|
||||
- EXAMPLE_NETWORK
|
||||
- ANOTHER_EXAMPLE_NETWORK
|
||||
# if not null, filter access points by this regular expression
|
||||
filter: null
|
||||
# cryptographic key for identity
|
||||
pubkey: /etc/ssh/ssh_host_rsa_key.pub
|
||||
|
||||
ai:
|
||||
# if false, only the default 'personality' will be used
|
||||
enabled: true
|
||||
path: /root/brain.nn
|
||||
# 1.0 - laziness = probability of start training
|
||||
laziness: 0.1
|
||||
# how many epochs to train on
|
||||
epochs_per_episode: 50
|
||||
params:
|
||||
# discount factor
|
||||
gamma: 0.99
|
||||
# the number of steps to run for each environment per update
|
||||
n_steps: 1
|
||||
# value function coefficient for the loss calculation
|
||||
vf_coef: 0.25
|
||||
# entropy coefficient for the loss calculation
|
||||
ent_coef: 0.01
|
||||
# maximum value for the gradient clipping
|
||||
max_grad_norm: 0.5
|
||||
# the learning rate
|
||||
learning_rate: 0.0010
|
||||
# rmsprop decay parameter
|
||||
alpha: 0.99
|
||||
# rmsprop epsilon
|
||||
epsilon: 0.00001
|
||||
# the verbosity level: 0 none, 1 training information, 2 tensorflow debug
|
||||
verbose: 1
|
||||
# type of scheduler for the learning rate update ('linear', 'constant', 'double_linear_con', 'middle_drop' or 'double_middle_drop')
|
||||
lr_schedule: 'constant'
|
||||
# the log location for tensorboard (if None, no logging)
|
||||
tensorboard_log: null
|
||||
|
||||
personality:
|
||||
# advertise our presence
|
||||
advertise: true
|
||||
# perform a deauthentication attack to client stations in order to get full or half handshakes
|
||||
deauth: true
|
||||
# send association frames to APs in order to get the PMKID
|
||||
associate: true
|
||||
# list of channels to recon on, or empty for all channels
|
||||
channels: []
|
||||
# minimum WiFi signal strength in dBm
|
||||
min_rssi: -200
|
||||
# number of seconds for wifi.ap.ttl
|
||||
ap_ttl: 120
|
||||
# number of seconds for wifi.sta.ttl
|
||||
sta_ttl: 300
|
||||
# time in seconds to wait during channel recon
|
||||
recon_time: 30
|
||||
# number of inactive epochs after which recon_time gets multiplied by recon_inactive_multiplier
|
||||
max_inactive_scale: 2
|
||||
# if more than max_inactive_scale epochs are inactive, recon_time *= recon_inactive_multiplier
|
||||
recon_inactive_multiplier: 2
|
||||
# time in seconds to wait during channel hopping if activity has been performed
|
||||
hop_recon_time: 10
|
||||
# time in seconds to wait during channel hopping if no activity has been performed
|
||||
min_recon_time: 5
|
||||
# maximum amount of deauths/associations per BSSID per session
|
||||
max_interactions: 3
|
||||
# maximum amount of misses before considering the data stale and triggering a new recon
|
||||
max_misses_for_recon: 5
|
||||
# number of active epochs that triggers the excited state
|
||||
excited_num_epochs: 10
|
||||
# number of inactive epochs that triggers the bored state
|
||||
bored_num_epochs: 15
|
||||
# number of inactive epochs that triggers the sad state
|
||||
sad_num_epochs: 25
|
||||
|
||||
# ui configuration
|
||||
ui:
|
||||
# ePaper display can update every 3 secs anyway, set to 0 to only refresh for major data changes
|
||||
# IMPORTANT: The lifespan of an eINK display depends on the cumulative amount of refreshes. If you want to
|
||||
# preserve your display over time, you should set this value to 0.0 so that the display will be refreshed only
|
||||
# if any of the important data fields changed (the uptime and blinking cursor won't trigger a refresh).
|
||||
fps: 0.0
|
||||
display:
|
||||
enabled: true
|
||||
rotation: 180
|
||||
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2
|
||||
type: 'waveshare_2'
|
||||
# Possible options red/yellow/black (black used for monocromatic displays)
|
||||
color: 'black'
|
||||
video:
|
||||
enabled: true
|
||||
address: '10.0.0.2'
|
||||
port: 8080
|
||||
|
||||
|
||||
# bettercap rest api configuration
|
||||
bettercap:
|
||||
# api scheme://hostname:port username and password
|
||||
scheme: http
|
||||
hostname: localhost
|
||||
port: 8081
|
||||
username: pwnagotchi
|
||||
password: pwnagotchi
|
||||
# folder where bettercap stores the WPA handshakes, given that
|
||||
# wifi.handshakes.aggregate will be set to false and individual
|
||||
# pcap files will be created in order to minimize the chances
|
||||
# of a single pcap file to get corrupted
|
||||
handshakes: /root/handshakes
|
||||
# events to mute in bettercap's events stream
|
||||
silence:
|
||||
- ble.device.new
|
||||
- ble.device.lost
|
||||
- ble.device.disconnected
|
||||
- ble.device.connected
|
||||
- ble.device.service.discovered
|
||||
- ble.device.characteristic.discovered
|
||||
- wifi.client.new
|
||||
- wifi.client.lost
|
||||
- wifi.client.probe
|
||||
- wifi.ap.new
|
||||
- wifi.ap.lost
|
||||
- mod.started
|
BIN
pwnagotchi/locale/de/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/de/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
208
pwnagotchi/locale/de/LC_MESSAGES/voice.po
Normal file
208
pwnagotchi/locale/de/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,208 @@
|
||||
# German language
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# dadav <33197631+dadav@users.noreply.github.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
|
||||
"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hi, ich bin ein Pwnagotchi! Starte ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack den Planet!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "KI bereit."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Das neurale Netz ist bereit."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, Channel {channel} ist frei! Dein AP wir des dir danken."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Mir ist langweilig..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Lass uns laufen gehen!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Das ist der beste Tag meines Lebens."
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Scheis Tag :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Mir ist sau langweilig..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Ich bin sehr traurig..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Ich bin traurig"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Ich lebe das Leben!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Ich pwne, also bin ich."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "So viele Netwerke!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Ich habe sooo viel Spaß!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Mein Verbrechen ist das der Neugier ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Hallo {name}, nett Dich kennenzulernen."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Gerät {name} ist in der nähe!!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ...tschüß {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} ist weg ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Whoops ...{name} ist weg."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} verpasst!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Verpasst!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Niemand will mit mir spielen ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Ich fühl michso alleine ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Wo sind denn alle?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Schlafe für {secs}s"
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Warte für {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Schaue mich um ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what}, lass uns Freunde sein!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Verbinde mit {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Ich denke, dass {mac} kein WiFi brauch!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Deauthentifiziere {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Kicke {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} Stationen gekicked\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} Freunde gefunden\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "{num} Handshakes aufgez.\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 Peer getroffen."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} Peers getroffen"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Ich war {duration} am Pwnen und habe {deauthed} Clients gekickt! Außerdem "
|
||||
"habe ich {associated} neue Freunde getroffen und {handshakes} Handshakes "
|
||||
"gefressen! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "Stunden"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "Minuten"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunden"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "Stunde"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "Minute"
|
||||
|
||||
msgid "second"
|
||||
msgstr "Sekunde"
|
BIN
pwnagotchi/locale/el/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/el/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
209
pwnagotchi/locale/el/LC_MESSAGES/voice.po
Normal file
209
pwnagotchi/locale/el/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,209 @@
|
||||
# pwnigotchi voice data
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR Periklis Fregkos <fregkos@gmail.com>, 2019.
|
||||
# CO AUTHOR Panos Vasilopoulos <hello@alwayslivid.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: 2019-10-03 08:00+0000\n"
|
||||
"Last-Translator: Periklis Fregkos <fregkos@gmail.com>\n"
|
||||
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language: el\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Γειά, είμαι το Pwnagotchi! Εκκινούμαι ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Νέα μέρα, νέο κυνήγι, νέα pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hackαρε τον πλανήτη!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "ΤΝ έτοιμη."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Το νευρωνικό δίκτυοείναι έτοιμο."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Ε, το κανάλι {channel} είναιελεύθερο! Το AP σου θαείναι ευγνώμων."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Βαριέμαι ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Ας πάμε μια βόλτα!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Είναι η καλύτερημέρα της ζωής μου!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Σκατένια μέρα :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Βαριέμαι πάρα πολύ ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Είμαι πολύ λυπημένο ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Είμαι λυπημένο"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Ζω την ζωή μου!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Pwnάρω, άρα υπάρχω."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Τόσα πολλά δίκτυα!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Περνάω τέλεια!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Η περιέργεια είναιτο μόνο έγκλημά μου ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Γειά {name}!Χάρηκα για τη γνωριμία. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Η μονάδα {name} είναι κοντά! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Εμμ ...αντίο {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "Το {name} έφυγε ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Ουπς ... Εξαφανίστηκε το {name}."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "Έχασα το {name}!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Το έχασα!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Κανείς δε θέλει ναπαίξει μαζί μου ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Νιώθω μοναχός μου ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Μα, πού πήγαν όλοι;!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Ξεκουράζομαι για {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Περιμένω για {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Ψάχνω τριγύρω ({secs})"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Εε! {what}, ας γίνουμε φίλοι!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Συνδέομαι με το {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Που'σαι ρε τρελέ {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Μόλις αποφάσισα ότι η {mac} δε χρείαζεται WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Πετάω έξω την {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Μπανάρω την {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Τέλεια δικέ μου, πήραμε {num} νέες χειραψίες!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ουπς, κάτιπήγε λάθος ... Επανεκκινούμαι ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Έριξα {num} σταθμούς\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Έκανα {num} νέους φίλους\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Πήρα {num} χειραψίες\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Γνώρισα 1 φίλο"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Γνώρισα {num} φίλους"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Pwnαρα για {duration} και έριξα {deauthed} πελάτες! Επίσης γνώρισα "
|
||||
"{associated} νέους φίλους και καταβρόχθισα {handshakes} χειραψίες! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
BIN
pwnagotchi/locale/fr/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/fr/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
210
pwnagotchi/locale/fr/LC_MESSAGES/voice.po
Normal file
210
pwnagotchi/locale/fr/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,210 @@
|
||||
# pwnigotchi voice data
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <7271496+quantumsheep@users.noreply.github.com>, 2019.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: 2019-10-03 10:34+0200\n"
|
||||
"Last-Translator: quantumsheep <7271496+quantumsheep@users.noreply.github."
|
||||
"com>\n"
|
||||
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language: french\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Bonjour, je suis Pwnagotchi! Démarrage ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Nouvelle journée, nouvelle chasse, nouveau pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack la planète!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "IA prête."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Le réseau neuronal est prêt."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, le channel {channel} est libre! Ton AP va dis merci."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Je m'ennuie ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Allons faire un tour!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "C'est le meilleur jour de ma vie!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Journée de merde :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Je m'ennuie énormément ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Je suis très triste ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Je suis triste"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Je vis la vie!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Je pwn donc je suis."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Autant de réseaux!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Je m'amuse tellement!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Mon crime est celui de la curiosité ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Bonjour {name}! Ravis de te rencontrer. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "L'unité {name} est proche! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Hum ... au revoir {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} est parti ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Oups ... {name} est parti."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} raté!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Raté!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Personne ne veut jouer avec moi ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Je me sens si seul ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Où est tout le monde?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Fais la sieste pendant {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Attends pendant {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Regarde autour ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what}, soyons amis!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Association à {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Décidé à l'instant que {mac} n'a pas besoin de WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Désauthentification de {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Cool, nous avons {num} nouveaux handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oups, quelque chose s'est mal passé ... Redémarrage ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} stations kick\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Fait {num} nouveaux amis\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Récupéré {num} handshakes\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 peer rencontré"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} peers recontrés"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"J'ai pwn durant {duration} et kick {deauthed} clients! J'ai aussi rencontré "
|
||||
"{associated} nouveaux amis and mangé {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
BIN
pwnagotchi/locale/it/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/it/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
207
pwnagotchi/locale/it/LC_MESSAGES/voice.po
Normal file
207
pwnagotchi/locale/it/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,207 @@
|
||||
# pwnaigotchi voice data
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR 5h4d0wb0y <28193209+5h4d0wb0y@users.noreply.github.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: 2019-10-02 17:20+0000\n"
|
||||
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language: italian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Ciao! Piacere Pwnagotchi! Caricamento ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Nuovo giorno...nuovi handshakes!!!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "IA pronta."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "La rete neurale è pronta."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, il canale {channel} è libero! Il tuo AP ringrazia."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Che noia ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Andiamo a fare una passeggiata!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Questo è il più bel giorno della mia vita!!!!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Giorno di merda :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Sono estremamente annoiato ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Sono molto triste..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Sono triste"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Mi sento vivo!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Pwn ergo sum."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Qui è pieno di reti!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Mi sto divertendo tantissimo!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Ciao {name}! E' un piacere. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "L'Unità {name} è vicina! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ... addio {name}, mi mancherai..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} se n'è andato ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Whoops ...{name} se n'è andato."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} è scomparso..."
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Ehi! Dove sei andato!?"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Nessuno vuole giocare con me..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Mi sento così solo..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Dove sono tutti?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Schiaccio un pisolino per {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Aspetto {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Do uno sguardo qui intorno... ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what}! Diventiamo amici!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Collegamento con {what} in corso..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Yo {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Ho appena deciso che {mac} non necessita di WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Sto prendendo a calci {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Bene, abbiamo {num} handshake{plural} in più!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ops, qualcosa è andato storto ... Riavvio ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} stazioni pestate\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} nuovi amici\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "{num} handshakes presi\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 peer incontrato"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} peers incontrati"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Ho lavorato per {duration} e preso a calci {deauthed} clients! Ho anche "
|
||||
"incontrato {associate} nuovi amici e ho mangiato {handshakes} handshakes! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "ore"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minuti"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "secondi"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "ora"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minuto"
|
||||
|
||||
msgid "second"
|
||||
msgstr "secondo"
|
BIN
pwnagotchi/locale/mk/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/mk/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
209
pwnagotchi/locale/mk/LC_MESSAGES/voice.po
Normal file
209
pwnagotchi/locale/mk/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,209 @@
|
||||
# pwnigotchi voice data
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR <33197631+dadav@users.noreply.github.com>, 2019.
|
||||
# kovach <2214005+kovachwt@users.noreply.github.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: 2019-09-30 23:53+0200\n"
|
||||
"Last-Translator: kovach <2214005+kovachwt@users.noreply.github.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: mk\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ДреееММмммМммм"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Здраво, јас сум Pwnagotchi! Почнувам ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Нов ден, нов лов, ќе си газиме!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Хак д Планет!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI спремно."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Невронската мрежае спремна."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Еј, каналот {channel} еслободен! APто ќе тикаже фала."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Досаднооо ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Ајде да шетнеме!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Ова ми е најдобриот ден во животот!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Срање ден :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Ултра досадно ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Многу тажно ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Тажно"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Ммхх животче!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Си газам значи постојам."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Мммм колку мрежи!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Јухуу забавноо ее!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Виновен сум само заљубопитност ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Здраво{name}! Мило ми е. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Опаа {name} е во близина! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Хмм ...чао {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} го снема ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Уупс ... {name} го снема."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} промаши!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Промаши!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Никој не сака даси игра со мене ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Толку сам ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Каде се сите?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Ќе дремнам {secs}с ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Дреммм"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "Дремммм ({secs}с)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Чекам {secs}с ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Шарам наоколу ({secs}с)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Еј {what} ајде да се дружиме!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Се закачувам на {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Јо {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Знаеш што, на {mac} не му треба WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Го деавтентицирам {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Кикбан {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Кул, фативме {num} нови ракувања!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Упс, нешто не еко што треба ... Рестартирам ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Избацив {num} станици\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} нови другарчиња\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Фатив {num} ракувања\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Запознав 1 пријател"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Запознав {num} пријатели"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Си газам веќе {duration} и избацив {deauthed} клиенти! Запознав {associated} "
|
||||
"нови другарчиња и лапнав {handshakes} ракувања! #pwnagotchi #pwnlog #pwnlife "
|
||||
"#hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
BIN
pwnagotchi/locale/nl/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/nl/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
208
pwnagotchi/locale/nl/LC_MESSAGES/voice.po
Normal file
208
pwnagotchi/locale/nl/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,208 @@
|
||||
# pwnigotchi voice data
|
||||
# Copyright (C) 2019
|
||||
# This file is distributed under the same license as the pwnagotchi package.
|
||||
# FIRST AUTHOR justin-p@users.noreply.github.com, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
|
||||
"Last-Translator: Justin-P <justin-p@users.noreply.github.com>\n"
|
||||
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hoi, Ik ben Pwnagotchi! Opstarten ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Nieuwe dag, nieuwe jacht, nieuwe pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hack de Wereld!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI is klaar."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Neuronen netwerkis klaar voor gebruik."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hey, kanaal {channel} is vrij! Je AP zal je bedanken."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Ik verveel me ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Laten we een rondje lopen!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Dit is de beste dag van mijn leven!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Ruk dag :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Ik verveel me kapot ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Ik ben ergverdrietig ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Ik ben verdrietig"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Beter kan het levenniet worden!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Ik pwn daarom besta ik."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Zo veel netwerken!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Dit is zo leuk!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Mijn enige misdrijf is mijn nieuwsgierigheid ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Hallo {name}! Leuk je te ontmoeten. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Unit {name} is dichtbij! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ...tot ziens {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} is weg"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Whoopsie ...{name} is weg"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} gemist!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Gemist!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Niemand wil metmij spelen ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Zo alleen ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Waar is iedereen?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Dutje doen voor {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Even {secs}s wachten ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Rond kijken ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hey {what}, laten we vriendenworden!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Verbinden met {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Ik vind dat {mac} genoeg WiFiheeft gehad!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "De-autoriseren {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Ik ga {mac} even kicken!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Gaaf, we hebben {num} nieuwe handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Oops, iets ging fout ...Rebooting ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} stations gekicked\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "{num} nieuwe vrienden\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "{num} nieuwe handshakes\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "1 peer ontmoet"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "{num} peers ontmoet"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Ik heb gepwned voor {duration} and heb {deauthed} clients gekicked! Ik heb "
|
||||
"ook {associated} nieuwe vrienden gevonden en heb {handshakes} handshakes "
|
||||
"gegeten! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
BIN
pwnagotchi/locale/ru/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ru/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
205
pwnagotchi/locale/ru/LC_MESSAGES/voice.po
Normal file
205
pwnagotchi/locale/ru/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,205 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-03 16:47+0200\n"
|
||||
"PO-Revision-Date: 2019-10-05 18:50+0300\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
"Last-Translator: Elliot Manson\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
|
||||
"Language: ru_RU\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr "ZzzzZZzzzzZzzz"
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Привет, я Pwnagotchi! Поехали …"
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Новый день, новая охота, новые взломы!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Взломаем всю планету!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "Искусственный интеллект готов."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Нейронная сеть готова."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Мне скучно …"
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Пойдем прогуляемся!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Это лучший день в моей жизни!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Дерьмовый день :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Мне очень скучно …"
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Мне очень грустно …"
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Мне грустно"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Я живу своей жизнью!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Я взламываю, поэтому я существую."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Так, много сетей!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Мне так весело!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Моё преступление - это любопытство …"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Привет, {name}! Приятно познакомиться. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Цель {name} близко! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Хм … до свидания {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} исчезла …"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Упс … {name} исчезла."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} упустил!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Промахнулся!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Никто не хочет играть со мной …"
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Мне так одиноко …"
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Где все?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Дремлет {secs}с …"
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr "Zzzzz"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr "ZzzZzzz ({secs}c)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Ждем {secs}c …"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Оглядываюсь вокруг ({secs}с)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Эй, {what} давай дружить!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Связываюсь с {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Йоy {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Просто решил, что {mac} не нужен WiFi! Кхе-кхе)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Деаутентификация {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Кикаю {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Круто, мы получили {num} новое рукопожатие!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Ой, что-то пошло не так … Перезагружаюсь …"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Кикнул {num} станцию\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Заимел {num} новых друзей\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Получил {num} рукопожатие\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Встретился один знакомый"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Встретились {num} приятелей"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
"Я взламывал {duration} и кикнул {deauthed} клиентов! Я также встретил "
|
||||
"{associated} новых друзей и съел {handshakes} рукопожатий! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "часов"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "час"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "минут"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "минуту"
|
BIN
pwnagotchi/locale/se/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/se/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
202
pwnagotchi/locale/se/LC_MESSAGES/voice.po
Normal file
202
pwnagotchi/locale/se/LC_MESSAGES/voice.po
Normal file
@ -0,0 +1,202 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-03 16:47+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Mike Eriksson <mike@swedishmike.org>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Hej, jag är Pwnagotchi! Startar ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Ny dag, ny jakt, nya pwns!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Hacka planeten!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI klar."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Det neurala nätverket är klart."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Du, kanal {channel} är ledig! Din AP will gilla detta."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Jag har det så tråkigt..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Dags för en promenad!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Det här är den bästa dagen i mitt liv!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Idag suger :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Jag är extremt uttråkad ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Jag är jätteledsen ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Jag är ledsen"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Nu leker livet!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Jag pwnar därför är jag."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Så många nätverk!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Fan vad skoj jag har!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Mitt brott är att vara nyfiken ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Hejsan {name}! Trevligt att träffas {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Enheten {name} är nära! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm ... farväl {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "{name} är borta ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Hoppsan ... {name} är borta."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "{name} missade!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Bom!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Ingen vill leka med mig ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Jag är så ensam ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Var är alla?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Sover för {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Väntar {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Tittar omkring mig ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hejsan {what} låt oss vara vänner"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Ansluter till {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Jag bestämde just att {mac} inte behöver WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Lysande, vi har {num} ny handskakningar{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Hoppsan, någpt gick fel ... Startar om ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "Sparkade {num} stationer\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Har {num} nya vänner\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Har {num} handskakningar\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Mötte 1 jämlike"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Mötte {num} jämlikar"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr "Jag har pwnat för {duration} och sparkat ut {deauthed} klienter, Jag "
|
||||
"har också träffat {associated} nya vänner och har skakat {handshakes} händer! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "timmar"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "timme"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "minuter"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "minut"
|
206
pwnagotchi/locale/voice.pot
Normal file
206
pwnagotchi/locale/voice.pot
Normal file
@ -0,0 +1,206 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-05 14:10+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr ""
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr ""
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr ""
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr ""
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr ""
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr ""
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr ""
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr ""
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
|
||||
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
msgstr ""
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
177
pwnagotchi/log.py
Normal file
177
pwnagotchi/log.py
Normal file
@ -0,0 +1,177 @@
|
||||
import hashlib
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from pwnagotchi.voice import Voice
|
||||
from pwnagotchi.mesh.peer import Peer
|
||||
from file_read_backwards import FileReadBackwards
|
||||
|
||||
LAST_SESSION_FILE = '/root/.pwnagotchi-last-session'
|
||||
|
||||
|
||||
class SessionParser(object):
|
||||
EPOCH_TOKEN = '[epoch '
|
||||
EPOCH_PARSER = re.compile(r'^\s*\[epoch (\d+)\] (.+)')
|
||||
EPOCH_DATA_PARSER = re.compile(r'([a-z_]+)=([^\s]+)')
|
||||
TRAINING_TOKEN = ' training epoch '
|
||||
START_TOKEN = 'connecting to http'
|
||||
DEAUTH_TOKEN = 'deauthing '
|
||||
ASSOC_TOKEN = 'sending association frame to '
|
||||
HANDSHAKE_TOKEN = '!!! captured new handshake '
|
||||
PEER_TOKEN = 'detected unit '
|
||||
|
||||
def _get_last_saved_session_id(self):
|
||||
saved = ''
|
||||
try:
|
||||
with open(LAST_SESSION_FILE, 'rt') as fp:
|
||||
saved = fp.read().strip()
|
||||
except:
|
||||
saved = ''
|
||||
return saved
|
||||
|
||||
def save_session_id(self):
|
||||
with open(LAST_SESSION_FILE, 'w+t') as fp:
|
||||
fp.write(self.last_session_id)
|
||||
self.last_saved_session_id = self.last_session_id
|
||||
|
||||
def _parse_datetime(self, dt):
|
||||
dt = dt.split('.')[0]
|
||||
dt = dt.split(',')[0]
|
||||
dt = datetime.strptime(dt.split('.')[0], '%Y-%m-%d %H:%M:%S')
|
||||
return time.mktime(dt.timetuple())
|
||||
|
||||
def _parse_stats(self):
|
||||
self.duration = ''
|
||||
self.duration_human = ''
|
||||
self.deauthed = 0
|
||||
self.associated = 0
|
||||
self.handshakes = 0
|
||||
self.epochs = 0
|
||||
self.train_epochs = 0
|
||||
self.peers = 0
|
||||
self.last_peer = None
|
||||
self.min_reward = 1000
|
||||
self.max_reward = -1000
|
||||
self.avg_reward = 0
|
||||
|
||||
started_at = None
|
||||
stopped_at = None
|
||||
cache = {}
|
||||
|
||||
for line in self.last_session:
|
||||
parts = line.split(']')
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
line_timestamp = parts[0].strip('[')
|
||||
line = ']'.join(parts[1:])
|
||||
stopped_at = self._parse_datetime(line_timestamp)
|
||||
if started_at is None:
|
||||
started_at = stopped_at
|
||||
|
||||
if SessionParser.DEAUTH_TOKEN in line and line not in cache:
|
||||
self.deauthed += 1
|
||||
cache[line] = 1
|
||||
|
||||
elif SessionParser.ASSOC_TOKEN in line and line not in cache:
|
||||
self.associated += 1
|
||||
cache[line] = 1
|
||||
|
||||
elif SessionParser.HANDSHAKE_TOKEN in line and line not in cache:
|
||||
self.handshakes += 1
|
||||
cache[line] = 1
|
||||
|
||||
elif SessionParser.TRAINING_TOKEN in line:
|
||||
self.train_epochs += 1
|
||||
|
||||
elif SessionParser.EPOCH_TOKEN in line:
|
||||
self.epochs += 1
|
||||
m = SessionParser.EPOCH_PARSER.findall(line)
|
||||
if m:
|
||||
epoch_num, epoch_data = m[0]
|
||||
m = SessionParser.EPOCH_DATA_PARSER.findall(epoch_data)
|
||||
for key, value in m:
|
||||
if key == 'reward':
|
||||
reward = float(value)
|
||||
self.avg_reward += reward
|
||||
if reward < self.min_reward:
|
||||
self.min_reward = reward
|
||||
|
||||
elif reward > self.max_reward:
|
||||
self.max_reward = reward
|
||||
|
||||
elif SessionParser.PEER_TOKEN in line:
|
||||
m = self._peer_parser.findall(line)
|
||||
if m:
|
||||
name, pubkey, rssi, sid, pwnd_tot, uptime = m[0]
|
||||
if pubkey not in cache:
|
||||
self.last_peer = Peer(sid, 1, int(rssi),
|
||||
{'name': name,
|
||||
'identity': pubkey,
|
||||
'pwnd_tot': int(pwnd_tot)})
|
||||
self.peers += 1
|
||||
cache[pubkey] = self.last_peer
|
||||
else:
|
||||
cache[pubkey].adv['pwnd_tot'] = pwnd_tot
|
||||
|
||||
if started_at is not None:
|
||||
self.duration = stopped_at - started_at
|
||||
mins, secs = divmod(self.duration, 60)
|
||||
hours, mins = divmod(mins, 60)
|
||||
else:
|
||||
hours = mins = secs = 0
|
||||
|
||||
self.duration = '%02d:%02d:%02d' % (hours, mins, secs)
|
||||
self.duration_human = []
|
||||
if hours > 0:
|
||||
self.duration_human.append('%d %s' % (hours, self.voice.hhmmss(hours, 'h')))
|
||||
if mins > 0:
|
||||
self.duration_human.append('%d %s' % (mins, self.voice.hhmmss(mins, 'm')))
|
||||
if secs > 0:
|
||||
self.duration_human.append('%d %s' % (secs, self.voice.hhmmss(secs, 's')))
|
||||
|
||||
self.duration_human = ', '.join(self.duration_human)
|
||||
self.avg_reward /= (self.epochs if self.epochs else 1)
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.voice = Voice(lang=config['main']['lang'])
|
||||
self.path = config['main']['log']
|
||||
self.last_session = None
|
||||
self.last_session_id = ''
|
||||
self.last_saved_session_id = ''
|
||||
self.duration = ''
|
||||
self.duration_human = ''
|
||||
self.deauthed = 0
|
||||
self.associated = 0
|
||||
self.handshakes = 0
|
||||
self.peers = 0
|
||||
self.last_peer = None
|
||||
self._peer_parser = re.compile(
|
||||
'detected unit (.+)@(.+) \(v.+\) on channel \d+ \(([\d\-]+) dBm\) \[sid:(.+) pwnd_tot:(\d+) uptime:(\d+)\]')
|
||||
|
||||
lines = []
|
||||
|
||||
if os.path.exists(self.path):
|
||||
with FileReadBackwards(self.path, encoding="utf-8") as fp:
|
||||
for line in fp:
|
||||
line = line.strip()
|
||||
if line != "" and line[0] != '[':
|
||||
continue
|
||||
lines.append(line)
|
||||
if SessionParser.START_TOKEN in line:
|
||||
break
|
||||
lines.reverse()
|
||||
|
||||
if len(lines) == 0:
|
||||
lines.append("Initial Session");
|
||||
|
||||
self.last_session = lines
|
||||
self.last_session_id = hashlib.md5(lines[0].encode()).hexdigest()
|
||||
self.last_saved_session_id = self._get_last_saved_session_id()
|
||||
|
||||
self._parse_stats()
|
||||
|
||||
def is_new(self):
|
||||
return self.last_session_id != self.last_saved_session_id
|
14
pwnagotchi/mesh/__init__.py
Normal file
14
pwnagotchi/mesh/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
import os
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
import hashlib
|
||||
|
||||
def new_session_id():
|
||||
return ':'.join(['%02x' % b for b in os.urandom(6)])
|
||||
|
||||
|
||||
def get_identity(config):
|
||||
pubkey = None
|
||||
with open(config['main']['pubkey']) as fp:
|
||||
pubkey = RSA.importKey(fp.read())
|
||||
return pubkey, hashlib.sha1(pubkey.exportKey('DER')).hexdigest()
|
182
pwnagotchi/mesh/advertise.py
Normal file
182
pwnagotchi/mesh/advertise.py
Normal file
@ -0,0 +1,182 @@
|
||||
import time
|
||||
import json
|
||||
import _thread
|
||||
import threading
|
||||
import logging
|
||||
from scapy.all import Dot11, Dot11FCS, Dot11Elt, RadioTap, sendp, sniff
|
||||
|
||||
import pwnagotchi.ui.faces as faces
|
||||
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
from pwnagotchi.mesh import new_session_id
|
||||
from pwnagotchi.mesh.peer import Peer
|
||||
|
||||
|
||||
def _dummy_peer_cb(peer):
|
||||
pass
|
||||
|
||||
|
||||
class Advertiser(object):
|
||||
MAX_STALE_TIME = 300
|
||||
|
||||
def __init__(self, iface, name, version, identity, period=0.3, data={}):
|
||||
self._iface = iface
|
||||
self._period = period
|
||||
self._running = False
|
||||
self._stopped = threading.Event()
|
||||
self._peers_lock = threading.Lock()
|
||||
self._adv_lock = threading.Lock()
|
||||
self._new_peer_cb = _dummy_peer_cb
|
||||
self._lost_peer_cb = _dummy_peer_cb
|
||||
self._peers = {}
|
||||
self._frame = None
|
||||
self._me = Peer(new_session_id(), 0, 0, {
|
||||
'name': name,
|
||||
'version': version,
|
||||
'identity': identity,
|
||||
'face': faces.FRIEND,
|
||||
'pwnd_run': 0,
|
||||
'pwnd_tot': 0,
|
||||
'uptime': 0,
|
||||
'epoch': 0,
|
||||
'data': data
|
||||
})
|
||||
self.update()
|
||||
|
||||
def update(self, values={}):
|
||||
with self._adv_lock:
|
||||
for field, value in values.items():
|
||||
self._me.adv[field] = value
|
||||
self._frame = wifi.encapsulate(payload=json.dumps(self._me.adv), addr_from=self._me.session_id)
|
||||
|
||||
def on_peer(self, new_cb, lost_cb):
|
||||
self._new_peer_cb = new_cb
|
||||
self._lost_peer_cb = lost_cb
|
||||
|
||||
def on_face_change(self, old, new):
|
||||
self.update({'face': new})
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
_thread.start_new_thread(self._sender, ())
|
||||
_thread.start_new_thread(self._listener, ())
|
||||
_thread.start_new_thread(self._pruner, ())
|
||||
|
||||
def num_peers(self):
|
||||
with self._peers_lock:
|
||||
return len(self._peers)
|
||||
|
||||
def peers(self):
|
||||
with self._peers_lock:
|
||||
return list(self._peers.values())
|
||||
|
||||
def closest_peer(self):
|
||||
closest = None
|
||||
with self._peers_lock:
|
||||
for ident, peer in self._peers.items():
|
||||
if closest is None or peer.is_closer(closest):
|
||||
closest = peer
|
||||
return closest
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self._stopped.set()
|
||||
|
||||
def _sender(self):
|
||||
logging.info("started advertiser thread (period:%s sid:%s) ..." % (str(self._period), self._me.session_id))
|
||||
while self._running:
|
||||
try:
|
||||
sendp(self._frame, iface=self._iface, verbose=False, count=1, inter=self._period)
|
||||
except OSError as ose:
|
||||
logging.warning("non critical issue while sending advertising packet: %s" % ose)
|
||||
except Exception as e:
|
||||
logging.exception("error")
|
||||
time.sleep(self._period)
|
||||
|
||||
def _on_advertisement(self, src_session_id, channel, rssi, adv):
|
||||
ident = adv['identity']
|
||||
with self._peers_lock:
|
||||
if ident not in self._peers:
|
||||
peer = Peer(src_session_id, channel, rssi, adv)
|
||||
logging.info("detected unit %s (v%s) on channel %d (%s dBm) [sid:%s pwnd_tot:%d uptime:%d]" % ( \
|
||||
peer.full_name(),
|
||||
peer.version(),
|
||||
channel,
|
||||
rssi,
|
||||
src_session_id,
|
||||
peer.pwnd_total(),
|
||||
peer.uptime()))
|
||||
|
||||
self._peers[ident] = peer
|
||||
self._new_peer_cb(peer)
|
||||
else:
|
||||
self._peers[ident].update(src_session_id, channel, rssi, adv)
|
||||
|
||||
def _parse_identity(self, radio, dot11, dot11elt):
|
||||
payload = b''
|
||||
while dot11elt:
|
||||
payload += dot11elt.info
|
||||
dot11elt = dot11elt.payload.getlayer(Dot11Elt)
|
||||
|
||||
if payload != b'':
|
||||
adv = json.loads(payload)
|
||||
self._on_advertisement( \
|
||||
dot11.addr3,
|
||||
wifi.freq_to_channel(radio.Channel),
|
||||
radio.dBm_AntSignal,
|
||||
adv)
|
||||
|
||||
def _is_broadcasted_advertisement(self, dot11):
|
||||
# dst bcast + protocol signature + not ours
|
||||
return dot11 is not None and \
|
||||
dot11.addr1 == wifi.BroadcastAddress and \
|
||||
dot11.addr2 == wifi.SignatureAddress and \
|
||||
dot11.addr3 != self._me.session_id
|
||||
|
||||
def _is_frame_for_us(self, dot11):
|
||||
# dst is us + protocol signature + not ours (why would we send a frame to ourself anyway?)
|
||||
return dot11 is not None and \
|
||||
dot11.addr1 == self._me.session_id and \
|
||||
dot11.addr2 == wifi.SignatureAddress and \
|
||||
dot11.addr3 != self._me.session_id
|
||||
|
||||
def _on_packet(self, p):
|
||||
# https://github.com/secdev/scapy/issues/1590
|
||||
if p.haslayer(Dot11):
|
||||
dot11 = p[Dot11]
|
||||
elif p.haslayer(Dot11FCS):
|
||||
dot11 = p[Dot11FCS]
|
||||
else:
|
||||
dot11 = None
|
||||
|
||||
if self._is_broadcasted_advertisement(dot11):
|
||||
try:
|
||||
dot11elt = p.getlayer(Dot11Elt)
|
||||
if dot11elt.ID == wifi.Dot11ElemID_Identity:
|
||||
self._parse_identity(p[RadioTap], dot11, dot11elt)
|
||||
|
||||
else:
|
||||
raise Exception("unknown frame id %d" % dot11elt.ID)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("error decoding packet from %s" % dot11.addr3)
|
||||
|
||||
def _listener(self):
|
||||
# logging.info("started advertisements listener ...")
|
||||
expr = "type mgt subtype beacon and ether src %s" % wifi.SignatureAddress
|
||||
sniff(iface=self._iface, filter=expr, prn=self._on_packet, store=0, stop_filter=lambda x: self._stopped.isSet())
|
||||
|
||||
def _pruner(self):
|
||||
while self._running:
|
||||
time.sleep(10)
|
||||
with self._peers_lock:
|
||||
stale = []
|
||||
for ident, peer in self._peers.items():
|
||||
inactive_for = peer.inactive_for()
|
||||
if inactive_for >= Advertiser.MAX_STALE_TIME:
|
||||
logging.info("peer %s lost (inactive for %ds)" % (peer.full_name(), inactive_for))
|
||||
self._lost_peer_cb(peer)
|
||||
stale.append(ident)
|
||||
|
||||
for ident in stale:
|
||||
del self._peers[ident]
|
66
pwnagotchi/mesh/peer.py
Normal file
66
pwnagotchi/mesh/peer.py
Normal file
@ -0,0 +1,66 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
import pwnagotchi.mesh.wifi as wifi
|
||||
import pwnagotchi.ui.faces as faces
|
||||
|
||||
|
||||
class Peer(object):
|
||||
def __init__(self, sid, channel, rssi, adv):
|
||||
self.first_seen = time.time()
|
||||
self.last_seen = self.first_seen
|
||||
self.session_id = sid
|
||||
self.last_channel = channel
|
||||
self.presence = [0] * wifi.NumChannels
|
||||
self.adv = adv
|
||||
self.rssi = rssi
|
||||
self.presence[channel - 1] = 1
|
||||
|
||||
def update(self, sid, channel, rssi, adv):
|
||||
if self.name() != adv['name']:
|
||||
logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), adv['name']))
|
||||
|
||||
if self.session_id != sid:
|
||||
logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, sid))
|
||||
|
||||
self.presence[channel - 1] += 1
|
||||
self.adv = adv
|
||||
self.rssi = rssi
|
||||
self.session_id = sid
|
||||
self.last_seen = time.time()
|
||||
|
||||
def inactive_for(self):
|
||||
return time.time() - self.last_seen
|
||||
|
||||
def _adv_field(self, name, default='???'):
|
||||
return self.adv[name] if name in self.adv else default
|
||||
|
||||
def face(self):
|
||||
return self._adv_field('face', default=faces.FRIEND)
|
||||
|
||||
def name(self):
|
||||
return self._adv_field('name')
|
||||
|
||||
def identity(self):
|
||||
return self._adv_field('identity')
|
||||
|
||||
def version(self):
|
||||
return self._adv_field('version')
|
||||
|
||||
def pwnd_run(self):
|
||||
return int(self._adv_field('pwnd_run', default=0))
|
||||
|
||||
def pwnd_total(self):
|
||||
return int(self._adv_field('pwnd_tot', default=0))
|
||||
|
||||
def uptime(self):
|
||||
return self._adv_field('uptime', default=0)
|
||||
|
||||
def epoch(self):
|
||||
return self._adv_field('epoch', default=0)
|
||||
|
||||
def full_name(self):
|
||||
return '%s@%s' % (self.name(), self.identity())
|
||||
|
||||
def is_closer(self, other):
|
||||
return self.rssi > other.rssi
|
45
pwnagotchi/mesh/utils.py
Normal file
45
pwnagotchi/mesh/utils.py
Normal file
@ -0,0 +1,45 @@
|
||||
import _thread
|
||||
import logging
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.mesh import get_identity
|
||||
|
||||
|
||||
class AsyncAdvertiser(object):
|
||||
def __init__(self, config, view):
|
||||
self._config = config
|
||||
self._view = view
|
||||
self._public_key, self._identity = get_identity(config)
|
||||
self._advertiser = None
|
||||
|
||||
def start_advertising(self):
|
||||
_thread.start_new_thread(self._adv_worker, ())
|
||||
|
||||
def _adv_worker(self):
|
||||
# this will take some time due to scapy being slow to be imported ...
|
||||
from pwnagotchi.mesh.advertise import Advertiser
|
||||
|
||||
self._advertiser = Advertiser(
|
||||
self._config['main']['iface'],
|
||||
pwnagotchi.name(),
|
||||
pwnagotchi.version,
|
||||
self._identity,
|
||||
period=0.3,
|
||||
data=self._config['personality'])
|
||||
|
||||
self._advertiser.on_peer(self._on_new_unit, self._on_lost_unit)
|
||||
|
||||
if self._config['personality']['advertise']:
|
||||
self._advertiser.start()
|
||||
self._view.on_state_change('face', self._advertiser.on_face_change)
|
||||
else:
|
||||
logging.warning("advertising is disabled")
|
||||
|
||||
def _on_new_unit(self, peer):
|
||||
self._view.on_new_peer(peer)
|
||||
plugins.on('peer_detected', self, peer)
|
||||
|
||||
def _on_lost_unit(self, peer):
|
||||
self._view.on_lost_peer(peer)
|
||||
plugins.on('peer_lost', self, peer)
|
37
pwnagotchi/mesh/wifi.py
Normal file
37
pwnagotchi/mesh/wifi.py
Normal file
@ -0,0 +1,37 @@
|
||||
SignatureAddress = 'de:ad:be:ef:de:ad'
|
||||
BroadcastAddress = 'ff:ff:ff:ff:ff:ff'
|
||||
Dot11ElemID_Identity = 222
|
||||
NumChannels = 140
|
||||
|
||||
def freq_to_channel(freq):
|
||||
if freq <= 2472:
|
||||
return int(((freq - 2412) / 5) + 1)
|
||||
elif freq == 2484:
|
||||
return int(14)
|
||||
elif 5035 <= freq <= 5865:
|
||||
return int(((freq - 5035) / 5) + 7)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def encapsulate(payload, addr_from, addr_to=BroadcastAddress):
|
||||
from scapy.all import Dot11, Dot11Beacon, Dot11Elt, RadioTap
|
||||
|
||||
radio = RadioTap()
|
||||
dot11 = Dot11(type=0, subtype=8, addr1=addr_to, addr2=SignatureAddress, addr3=addr_from)
|
||||
beacon = Dot11Beacon(cap='ESS')
|
||||
frame = radio / dot11 / beacon
|
||||
|
||||
data_size = len(payload)
|
||||
data_left = data_size
|
||||
data_off = 0
|
||||
chunk_size = 255
|
||||
|
||||
while data_left > 0:
|
||||
sz = min(chunk_size, data_left)
|
||||
chunk = payload[data_off: data_off + sz]
|
||||
frame /= Dot11Elt(ID=Dot11ElemID_Identity, info=chunk, len=sz)
|
||||
data_off += sz
|
||||
data_left -= sz
|
||||
|
||||
return frame
|
60
pwnagotchi/plugins/__init__.py
Normal file
60
pwnagotchi/plugins/__init__.py
Normal file
@ -0,0 +1,60 @@
|
||||
import os
|
||||
import glob
|
||||
import importlib, importlib.util
|
||||
|
||||
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
|
||||
loaded = {}
|
||||
|
||||
|
||||
def dummy_callback():
|
||||
pass
|
||||
|
||||
|
||||
def on(event_name, *args, **kwargs):
|
||||
global loaded
|
||||
cb_name = 'on_%s' % event_name
|
||||
for _, plugin in loaded.items():
|
||||
if cb_name in plugin.__dict__:
|
||||
# print("calling %s %s(%s)" %(cb_name, args, kwargs))
|
||||
plugin.__dict__[cb_name](*args, **kwargs)
|
||||
|
||||
|
||||
def load_from_file(filename):
|
||||
plugin_name = os.path.basename(filename.replace(".py", ""))
|
||||
spec = importlib.util.spec_from_file_location(plugin_name, filename)
|
||||
instance = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(instance)
|
||||
return plugin_name, instance
|
||||
|
||||
|
||||
def load_from_path(path, enabled=()):
|
||||
global loaded
|
||||
for filename in glob.glob(os.path.join(path, "*.py")):
|
||||
name, plugin = load_from_file(filename)
|
||||
if name in loaded:
|
||||
raise Exception("plugin %s already loaded from %s" % (name, plugin.__file__))
|
||||
elif name not in enabled:
|
||||
# print("plugin %s is not enabled" % name)
|
||||
pass
|
||||
else:
|
||||
loaded[name] = plugin
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def load(config):
|
||||
enabled = [name for name, options in config['main']['plugins'].items() if 'enabled' in options and options['enabled']]
|
||||
custom_path = config['main']['custom_plugins'] if 'custom_plugins' in config['main'] else None
|
||||
# load default plugins
|
||||
loaded = load_from_path(default_path, enabled=enabled)
|
||||
# set the options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.__dict__['OPTIONS'] = config['main']['plugins'][name]
|
||||
# load custom ones
|
||||
if custom_path is not None:
|
||||
loaded = load_from_path(custom_path, enabled=enabled)
|
||||
# set the options
|
||||
for name, plugin in loaded.items():
|
||||
plugin.__dict__['OPTIONS'] = config['main']['plugins'][name]
|
||||
|
||||
on('loaded')
|
63
pwnagotchi/plugins/default/auto-backup.py
Normal file
63
pwnagotchi/plugins/default/auto-backup.py
Normal file
@ -0,0 +1,63 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'auto-backup'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin backups files when internet is availaible.'
|
||||
|
||||
from pwnagotchi.utils import StatusFile
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
OPTIONS = dict()
|
||||
READY = False
|
||||
STATUS = StatusFile('/root/.auto-backup')
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
|
||||
if 'files' not in OPTIONS or ('files' in OPTIONS and OPTIONS['files'] is None):
|
||||
logging.error("AUTO-BACKUP: No files to backup.")
|
||||
return
|
||||
|
||||
if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
|
||||
logging.error("AUTO-BACKUP: Interval is not set.")
|
||||
return
|
||||
|
||||
if 'commands' not in OPTIONS or ('commands' in OPTIONS and OPTIONS['commands'] is None):
|
||||
logging.error("AUTO-BACKUP: No commands given.")
|
||||
return
|
||||
|
||||
READY = True
|
||||
logging.info("AUTO-BACKUP: Successfuly loaded.")
|
||||
|
||||
|
||||
def on_internet_available(display, config, log):
|
||||
global STATUS
|
||||
|
||||
if READY:
|
||||
if STATUS.newer_then_days(OPTIONS['interval']):
|
||||
return
|
||||
|
||||
files_to_backup = " ".join(OPTIONS['files'])
|
||||
try:
|
||||
logging.info("AUTO-BACKUP: Backing up ...")
|
||||
display.set('status', 'Backing up ...')
|
||||
display.update()
|
||||
|
||||
for cmd in OPTIONS['commands']:
|
||||
logging.info(f"AUTO-BACKUP: Running {cmd.format(files=files_to_backup)}")
|
||||
process = subprocess.Popen(cmd.format(files=files_to_backup), shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
raise OSError(f"Command failed (rc: {process.returncode})")
|
||||
|
||||
logging.info("AUTO-BACKUP: backup done")
|
||||
STATUS.update()
|
||||
except OSError as os_e:
|
||||
logging.info(f"AUTO-BACKUP: Error: {os_e}")
|
||||
|
||||
display.set('status', 'Backup done!')
|
||||
display.update()
|
56
pwnagotchi/plugins/default/auto-update.py
Normal file
56
pwnagotchi/plugins/default/auto-update.py
Normal file
@ -0,0 +1,56 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'auto-update'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin performs an "apt update && apt upgrade" when internet is availaible.'
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
OPTIONS = dict()
|
||||
READY = False
|
||||
STATUS = StatusFile('/root/.auto-update')
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global READY
|
||||
|
||||
if 'interval' not in OPTIONS or ('interval' in OPTIONS and OPTIONS['interval'] is None):
|
||||
logging.error("AUTO-UPDATE: Interval is not set.")
|
||||
return
|
||||
|
||||
READY = True
|
||||
|
||||
|
||||
def on_internet_available(display, config, log):
|
||||
global STATUS
|
||||
|
||||
if READY:
|
||||
if STATUS.newer_then_days(OPTIONS['interval']):
|
||||
return
|
||||
|
||||
try:
|
||||
display.set('status', 'Updating ...')
|
||||
display.update()
|
||||
|
||||
logging.info("AUTO-UPDATE: updating packages index ...")
|
||||
|
||||
update = subprocess.Popen('apt update -y', shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
update.wait()
|
||||
|
||||
logging.info("AUTO-UPDATE: updating packages ...")
|
||||
|
||||
upgrade = subprocess.Popen('apt upgrade -y', shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
upgrade.wait()
|
||||
|
||||
logging.info("AUTO-UPDATE: complete.")
|
||||
|
||||
STATUS.update()
|
||||
except Exception as e:
|
||||
logging.exception("AUTO-UPDATE ERROR")
|
||||
|
||||
display.set('status', 'Updated!')
|
||||
display.update()
|
170
pwnagotchi/plugins/default/example.py
Normal file
170
pwnagotchi/plugins/default/example.py
Normal file
@ -0,0 +1,170 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'hello_world'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'An example plugin for pwnagotchi that implements all the available callbacks.'
|
||||
|
||||
import logging
|
||||
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
|
||||
|
||||
# Will be set with the options in config.yml config['main']['plugins'][__name__]
|
||||
OPTIONS = dict()
|
||||
|
||||
# called when the plugin is loaded
|
||||
def on_loaded():
|
||||
logging.warning("WARNING: plugin %s should be disabled!" % __name__)
|
||||
|
||||
|
||||
# called in manual mode when there's internet connectivity
|
||||
def on_internet_available(ui, config, log):
|
||||
pass
|
||||
|
||||
|
||||
# called to setup the ui elements
|
||||
def on_ui_setup(ui):
|
||||
# add custom UI elements
|
||||
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
|
||||
# called when the ui is updated
|
||||
def on_ui_update(ui):
|
||||
# update those elements
|
||||
some_voltage = 0.1
|
||||
some_capacity = 100.0
|
||||
|
||||
ui.set('ups', "%4.2fV/%2i%%" % (some_voltage, some_capacity))
|
||||
|
||||
|
||||
# called when the hardware display setup is done, display is an hardware specific object
|
||||
def on_display_setup(display):
|
||||
pass
|
||||
|
||||
|
||||
# called when everything is ready and the main loop is about to start
|
||||
def on_ready(agent):
|
||||
logging.info("unit is ready")
|
||||
# you can run custom bettercap commands if you want
|
||||
# agent.run('ble.recon on')
|
||||
# or set a custom state
|
||||
# agent.set_bored()
|
||||
|
||||
|
||||
# called when the AI finished loading
|
||||
def on_ai_ready(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the AI finds a new set of parameters
|
||||
def on_ai_policy(agent, policy):
|
||||
pass
|
||||
|
||||
|
||||
# called when the AI starts training for a given number of epochs
|
||||
def on_ai_training_start(agent, epochs):
|
||||
pass
|
||||
|
||||
|
||||
# called after the AI completed a training epoch
|
||||
def on_ai_training_step(agent, _locals, _globals):
|
||||
pass
|
||||
|
||||
|
||||
# called when the AI has done training
|
||||
def on_ai_training_end(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the AI got the best reward so far
|
||||
def on_ai_best_reward(agent, reward):
|
||||
pass
|
||||
|
||||
|
||||
# called when the AI got the worst reward so far
|
||||
def on_ai_worst_reward(agent, reward):
|
||||
pass
|
||||
|
||||
|
||||
# called when a non overlapping wifi channel is found to be free
|
||||
def on_free_channel(agent, channel):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to bored
|
||||
def on_bored(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to sad
|
||||
def on_sad(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to excited
|
||||
def on_excited(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the status is set to lonely
|
||||
def on_lonely(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is rebooting the board
|
||||
def on_rebooting(agent):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is waiting for t seconds
|
||||
def on_wait(agent, t):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is sleeping for t seconds
|
||||
def on_sleep(agent, t):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent refreshed its access points list
|
||||
def on_wifi_update(agent, access_points):
|
||||
pass
|
||||
|
||||
|
||||
# called when the agent is sending an association frame
|
||||
def on_association(agent, access_point):
|
||||
pass
|
||||
|
||||
|
||||
# callend when the agent is deauthenticating a client station from an AP
|
||||
def on_deauthentication(agent, access_point, client_station):
|
||||
pass
|
||||
|
||||
|
||||
# callend when the agent is tuning on a specific channel
|
||||
def on_channel_hop(agent, channel):
|
||||
pass
|
||||
|
||||
|
||||
# called when a new handshake is captured, access_point and client_station are json objects
|
||||
# if the agent could match the BSSIDs to the current list, otherwise they are just the strings of the BSSIDs
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
pass
|
||||
|
||||
|
||||
# called when an epoch is over (where an epoch is a single loop of the main algorithm)
|
||||
def on_epoch(agent, epoch, epoch_data):
|
||||
pass
|
||||
|
||||
|
||||
# called when a new peer is detected
|
||||
def on_peer_detected(agent, peer):
|
||||
pass
|
||||
|
||||
|
||||
# called when a known peer is lost
|
||||
def on_peer_lost(agent, peer):
|
||||
pass
|
46
pwnagotchi/plugins/default/gps.py
Normal file
46
pwnagotchi/plugins/default/gps.py
Normal file
@ -0,0 +1,46 @@
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'gps'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'Save GPS coordinates whenever an handshake is captured.'
|
||||
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
|
||||
device = '/dev/ttyUSB0'
|
||||
speed = 19200
|
||||
running = False
|
||||
|
||||
|
||||
def on_loaded():
|
||||
logging.info("gps plugin loaded for %s" % device)
|
||||
|
||||
|
||||
def on_ready(agent):
|
||||
global running
|
||||
|
||||
if os.path.exists(device):
|
||||
logging.info("enabling gps bettercap's module for %s" % device)
|
||||
try:
|
||||
agent.run('gps off')
|
||||
except:
|
||||
pass
|
||||
|
||||
agent.run('set gps.device %s' % device)
|
||||
agent.run('set gps.speed %d' % speed)
|
||||
agent.run('gps on')
|
||||
running = True
|
||||
else:
|
||||
logging.warning("no GPS detected")
|
||||
|
||||
|
||||
def on_handshake(agent, filename, access_point, client_station):
|
||||
if running:
|
||||
info = agent.session()
|
||||
gps = info['gps']
|
||||
gps_filename = filename.replace('.pcap', '.gps.json')
|
||||
|
||||
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
|
||||
with open(gps_filename, 'w+t') as fp:
|
||||
json.dump(gps, fp)
|
68
pwnagotchi/plugins/default/memtemp.py
Normal file
68
pwnagotchi/plugins/default/memtemp.py
Normal file
@ -0,0 +1,68 @@
|
||||
# tempmem shows memory infos and cpu temperature
|
||||
#
|
||||
# totalmem usedmem freemem cputemp
|
||||
#
|
||||
__author__ = 'https://github.com/xenDE'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'memtemp'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin that will add a memory and temperature indicator'
|
||||
|
||||
import struct
|
||||
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class MEMTEMP:
|
||||
|
||||
# set the minimum seconds before refresh the values
|
||||
refresh_wait = 30
|
||||
|
||||
refresh_ts_last = time.time() - refresh_wait
|
||||
|
||||
def __init__(self):
|
||||
# only import when the module is loaded and enabled
|
||||
import os
|
||||
|
||||
def get_temp(self):
|
||||
try:
|
||||
temp = os.popen('/opt/vc/bin/vcgencmd measure_temp').readlines()[0].split('=')[1].replace("\n", '').replace("'","")
|
||||
return 'cpu:' + temp
|
||||
except:
|
||||
return 'cpu:0.0C'
|
||||
# cpu:37.4C
|
||||
|
||||
def get_mem_info(self):
|
||||
try:
|
||||
# includes RAM + Swap Memory:
|
||||
# total, used, free = map(int, os.popen('free -t -m').readlines()[-1].split()[1:])
|
||||
# without Swap, only real memory:
|
||||
total, used, free = map(int, os.popen('free -t -m').readlines()[-3].split()[1:4])
|
||||
return "tm:"+str(total)+" um:"+str(used)+" fm:"+str(free)
|
||||
except:
|
||||
return "tm:0 um:0 fm:0"
|
||||
# tm:532 um:82 fm:353
|
||||
|
||||
|
||||
memtemp = None
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global memtemp
|
||||
memtemp = MEMTEMP()
|
||||
|
||||
|
||||
def on_ui_setup(ui):
|
||||
ui.add_element('memtemp', LabeledValue(color=BLACK, label='SYS', value='tm:0 um:0 fm:0 0.0C', position=(0, ui.height()-28),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
if time.time() > memtemp.refresh_ts_last + memtemp.refresh_wait:
|
||||
ui.set('memtemp', "%s %s" % (memtemp.get_mem_info(), memtemp.get_temp()))
|
||||
memtemp.refresh_ts_last = time.time()
|
||||
|
84
pwnagotchi/plugins/default/onlinehashcrack.py
Normal file
84
pwnagotchi/plugins/default/onlinehashcrack.py
Normal file
@ -0,0 +1,84 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'onlinehashcrack'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploades handshakes to https://onlinehashcrack.com'
|
||||
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
|
||||
READY = False
|
||||
ALREADY_UPLOADED = None
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
global EMAIL
|
||||
global ALREADY_UPLOADED
|
||||
|
||||
if not 'email' in OPTIONS or ('email' in OPTIONS and OPTIONS['email'] is None):
|
||||
logging.error("OHC: Email isn't set. Can't upload to onlinehashcrack.com")
|
||||
return
|
||||
|
||||
try:
|
||||
with open('/root/.ohc_uploads', 'r') as f:
|
||||
ALREADY_UPLOADED = f.read().splitlines()
|
||||
except OSError:
|
||||
logging.warning('OHC: No upload-file found.')
|
||||
ALREADY_UPLOADED = []
|
||||
|
||||
READY = True
|
||||
|
||||
|
||||
def _upload_to_ohc(path, timeout=30):
|
||||
"""
|
||||
Uploads the file to onlinehashcrack.com
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
data = {'email': OPTIONS['email']}
|
||||
payload = {'file': file_to_upload}
|
||||
|
||||
try:
|
||||
result = requests.post('https://api.onlinehashcrack.com',
|
||||
data=data,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if 'already been sent' in result.text:
|
||||
logging.warning(f"{path} was already uploaded.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"OHC: Got an exception while uploading {path} -> {e}")
|
||||
raise e
|
||||
|
||||
|
||||
def on_internet_available(display, config, log):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
if READY:
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames]
|
||||
handshake_new = set(handshake_paths) - set(ALREADY_UPLOADED)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status', f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
_upload_to_ohc(handshake)
|
||||
ALREADY_UPLOADED.append(handshake)
|
||||
with open('/root/.ohc_uploads', 'a') as f:
|
||||
f.write(handshake + "\n")
|
||||
logging.info(f"OHC: Successfuly uploaded {handshake}")
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
except OSError as os_e:
|
||||
logging.error(f"OHC: Got the following error: {os_e}")
|
||||
|
46
pwnagotchi/plugins/default/twitter.py
Normal file
46
pwnagotchi/plugins/default/twitter.py
Normal file
@ -0,0 +1,46 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'twitter'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin creates tweets about the recent activity of pwnagotchi'
|
||||
|
||||
import logging
|
||||
from pwnagotchi.voice import Voice
|
||||
|
||||
OPTIONS = dict()
|
||||
|
||||
def on_loaded():
|
||||
logging.info("twitter plugin loaded.")
|
||||
|
||||
|
||||
# called in manual mode when there's internet connectivity
|
||||
def on_internet_available(ui, config, log):
|
||||
if log.is_new() and log.handshakes > 0:
|
||||
try:
|
||||
import tweepy
|
||||
except ImportError:
|
||||
logging.error("Couldn't import tweepy")
|
||||
return
|
||||
|
||||
logging.info("detected a new session and internet connectivity!")
|
||||
|
||||
picture = '/dev/shm/pwnagotchi.png'
|
||||
|
||||
ui.on_manual_mode(log)
|
||||
ui.update(force=True)
|
||||
ui.image().save(picture, 'png')
|
||||
ui.set('status', 'Tweeting...')
|
||||
ui.update(force=True)
|
||||
|
||||
try:
|
||||
auth = tweepy.OAuthHandler(OPTIONS['consumer_key'], OPTIONS['consumer_secret'])
|
||||
auth.set_access_token(OPTIONS['access_token_key'], OPTIONS['access_token_secret'])
|
||||
api = tweepy.API(auth)
|
||||
|
||||
tweet = Voice(lang=config['main']['lang']).on_log_tweet(log)
|
||||
api.update_with_media(filename=picture, status=tweet)
|
||||
log.save_session_id()
|
||||
|
||||
logging.info("tweeted: %s" % tweet)
|
||||
except Exception as e:
|
||||
logging.exception("error while tweeting")
|
64
pwnagotchi/plugins/default/ups_lite.py
Normal file
64
pwnagotchi/plugins/default/ups_lite.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Based on UPS Lite v1.1 from https://github.com/xenDE
|
||||
#
|
||||
# funtions for get UPS status - needs enable "i2c" in raspi-config
|
||||
#
|
||||
# https://github.com/linshuqin329/UPS-Lite
|
||||
#
|
||||
# For Raspberry Pi Zero Ups Power Expansion Board with Integrated Serial Port S3U4
|
||||
# https://www.ebay.de/itm/For-Raspberry-Pi-Zero-Ups-Power-Expansion-Board-with-Integrated-Serial-Port-S3U4/323873804310
|
||||
# https://www.aliexpress.com/item/32888533624.html
|
||||
__author__ = 'evilsocket@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'ups_lite'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1'
|
||||
|
||||
import struct
|
||||
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
|
||||
|
||||
# TODO: add enable switch in config.yml an cleanup all to the best place
|
||||
class UPS:
|
||||
def __init__(self):
|
||||
# only import when the module is loaded and enabled
|
||||
import smbus
|
||||
# 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1)
|
||||
self._bus = smbus.SMBus(1)
|
||||
|
||||
def voltage(self):
|
||||
try:
|
||||
address = 0x36
|
||||
read = self._bus.read_word_data(address, 2)
|
||||
swapped = struct.unpack("<H", struct.pack(">H", read))[0]
|
||||
return swapped * 1.25 / 1000 / 16
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
def capacity(self):
|
||||
try:
|
||||
address = 0x36
|
||||
read = self._bus.read_word_data(address, 4)
|
||||
swapped = struct.unpack("<H", struct.pack(">H", read))[0]
|
||||
return swapped / 256
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
|
||||
ups = None
|
||||
|
||||
|
||||
def on_loaded():
|
||||
global ups
|
||||
ups = UPS()
|
||||
|
||||
|
||||
def on_ui_setup(ui):
|
||||
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 - 25, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
ui.set('ups', "%4.2fV/%2i%%" % (ups.voltage(), ups.capacity()))
|
83
pwnagotchi/plugins/default/wpa-sec.py
Normal file
83
pwnagotchi/plugins/default/wpa-sec.py
Normal file
@ -0,0 +1,83 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'wpa-sec'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploades handshakes to https://wpa-sec.stanev.org'
|
||||
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
|
||||
READY = False
|
||||
ALREADY_UPLOADED = None
|
||||
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
global API_KEY
|
||||
global ALREADY_UPLOADED
|
||||
|
||||
if not 'api_key' in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
|
||||
logging.error("WPA_SEC: API-KEY isn't set. Can't upload to wpa-sec.stanev.org")
|
||||
return
|
||||
|
||||
try:
|
||||
with open('/root/.wpa_sec_uploads', 'r') as f:
|
||||
ALREADY_UPLOADED = f.read().splitlines()
|
||||
except OSError:
|
||||
logging.warning('WPA_SEC: No upload-file found.')
|
||||
ALREADY_UPLOADED = []
|
||||
|
||||
READY = True
|
||||
|
||||
|
||||
def _upload_to_wpasec(path, timeout=30):
|
||||
"""
|
||||
Uploads the file to wpa-sec.stanev.org
|
||||
"""
|
||||
with open(path, 'rb') as file_to_upload:
|
||||
headers = {'key': OPTIONS['api_key']}
|
||||
payload = {'file': file_to_upload}
|
||||
|
||||
try:
|
||||
result = requests.post('https://wpa-sec.stanev.org/?submit',
|
||||
headers=headers,
|
||||
files=payload,
|
||||
timeout=timeout)
|
||||
if ' already submitted' in result.text:
|
||||
logging.warning(f"{path} was already submitted.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"WPA_SEC: Got an exception while uploading {path} -> {e}")
|
||||
raise e
|
||||
|
||||
|
||||
def on_internet_available(display, config, log):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
if READY:
|
||||
handshake_dir = config['bettercap']['handshakes']
|
||||
handshake_filenames = os.listdir(handshake_dir)
|
||||
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames]
|
||||
handshake_new = set(handshake_paths) - set(ALREADY_UPLOADED)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("WPA_SEC: Internet connectivity detected.\
|
||||
Uploading new handshakes to wpa-sec.stanev.org")
|
||||
|
||||
for idx, handshake in enumerate(handshake_new):
|
||||
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
|
||||
display.update(force=True)
|
||||
try:
|
||||
_upload_to_wpasec(handshake)
|
||||
ALREADY_UPLOADED.append(handshake)
|
||||
with open('/root/.wpa_sec_uploads', 'a') as f:
|
||||
f.write(handshake + "\n")
|
||||
logging.info(f"WPA_SEC: Successfuly uploaded {handshake}")
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
except OSError as os_e:
|
||||
logging.error(f"WPA_SEC: Got the following error: {os_e}")
|
1
pwnagotchi/ui/__init__.py
Normal file
1
pwnagotchi/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
74
pwnagotchi/ui/components.py
Normal file
74
pwnagotchi/ui/components.py
Normal file
@ -0,0 +1,74 @@
|
||||
from PIL import Image
|
||||
from textwrap import TextWrapper
|
||||
|
||||
|
||||
class Widget(object):
|
||||
def __init__(self, xy, color=0):
|
||||
self.xy = xy
|
||||
self.color = color
|
||||
|
||||
def draw(self, canvas, drawer):
|
||||
raise Exception("not implemented")
|
||||
|
||||
|
||||
class Bitmap(Widget):
|
||||
def __init__(self, path, xy, color=0):
|
||||
super().__init__(xy, color)
|
||||
self.image = Image.open(path)
|
||||
|
||||
def draw(self, canvas, drawer):
|
||||
canvas.paste(self.image, self.xy)
|
||||
|
||||
|
||||
class Line(Widget):
|
||||
def __init__(self, xy, color=0, width=1):
|
||||
super().__init__(xy, color)
|
||||
self.width = width
|
||||
|
||||
def draw(self, canvas, drawer):
|
||||
drawer.line(self.xy, fill=self.color, width=self.width)
|
||||
|
||||
|
||||
class Rect(Widget):
|
||||
def draw(self, canvas, drawer):
|
||||
drawer.rectangle(self.xy, outline=self.color)
|
||||
|
||||
|
||||
class FilledRect(Widget):
|
||||
def draw(self, canvas, drawer):
|
||||
drawer.rectangle(self.xy, fill=self.color)
|
||||
|
||||
|
||||
class Text(Widget):
|
||||
def __init__(self, value="", position=(0, 0), font=None, color=0, wrap=False, max_length=0):
|
||||
super().__init__(position, color)
|
||||
self.value = value
|
||||
self.font = font
|
||||
self.wrap = wrap
|
||||
self.max_length = max_length
|
||||
self.wrapper = TextWrapper(width=self.max_length, replace_whitespace=False) if wrap else None
|
||||
|
||||
def draw(self, canvas, drawer):
|
||||
if self.value is not None:
|
||||
if self.wrap:
|
||||
text = '\n'.join(self.wrapper.wrap(self.value))
|
||||
else:
|
||||
text = self.value
|
||||
drawer.text(self.xy, text, font=self.font, fill=self.color)
|
||||
|
||||
|
||||
class LabeledValue(Widget):
|
||||
def __init__(self, label, value="", position=(0, 0), label_font=None, text_font=None, color=0):
|
||||
super().__init__(position, color)
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.label_font = label_font
|
||||
self.text_font = text_font
|
||||
|
||||
def draw(self, canvas, drawer):
|
||||
if self.label is None:
|
||||
drawer.text(self.xy, self.value, font=self.label_font, fill=self.color)
|
||||
else:
|
||||
pos = self.xy
|
||||
drawer.text(pos, self.label, font=self.label_font, fill=self.color)
|
||||
drawer.text((pos[0] + 5 + 5 * len(self.label), pos[1]), self.value, font=self.text_font, fill=self.color)
|
221
pwnagotchi/ui/display.py
Normal file
221
pwnagotchi/ui/display.py
Normal file
@ -0,0 +1,221 @@
|
||||
import _thread
|
||||
from threading import Lock
|
||||
|
||||
import shutil
|
||||
import logging
|
||||
import os
|
||||
import pwnagotchi, pwnagotchi.plugins as plugins
|
||||
|
||||
from pwnagotchi.ui.view import WHITE, View
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
|
||||
class VideoHandler(BaseHTTPRequestHandler):
|
||||
_lock = Lock()
|
||||
_index = """<html>
|
||||
<head>
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/ui" id="ui"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
window.onload = function() {
|
||||
var image = document.getElementById("ui");
|
||||
function updateImage() {
|
||||
image.src = image.src.split("?")[0] + "?" + new Date().getTime();
|
||||
}
|
||||
setInterval(updateImage, %d);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
@staticmethod
|
||||
def render(img):
|
||||
with VideoHandler._lock:
|
||||
img.save("/root/pwnagotchi.png", format='PNG')
|
||||
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(bytes(self._index % (pwnagotchi.name(), 1000), "utf8"))
|
||||
except:
|
||||
pass
|
||||
|
||||
elif self.path.startswith('/ui'):
|
||||
with self._lock:
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'image/png')
|
||||
self.end_headers()
|
||||
try:
|
||||
with open("/root/pwnagotchi.png", 'rb') as fp:
|
||||
shutil.copyfileobj(fp, self.wfile)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
self.send_response(404)
|
||||
|
||||
|
||||
class Display(View):
|
||||
def __init__(self, config, state={}):
|
||||
super(Display, self).__init__(config, state)
|
||||
self._enabled = config['ui']['display']['enabled']
|
||||
self._rotation = config['ui']['display']['rotation']
|
||||
self._video_enabled = config['ui']['display']['video']['enabled']
|
||||
self._video_port = config['ui']['display']['video']['port']
|
||||
self._video_address = config['ui']['display']['video']['address']
|
||||
self._display_type = config['ui']['display']['type']
|
||||
self._display_color = config['ui']['display']['color']
|
||||
|
||||
self._render_cb = None
|
||||
self._display = None
|
||||
self._httpd = None
|
||||
|
||||
if self._enabled:
|
||||
self._init_display()
|
||||
else:
|
||||
self.on_render(self._on_view_rendered)
|
||||
logging.warning("display module is disabled")
|
||||
|
||||
if self._video_enabled:
|
||||
_thread.start_new_thread(self._http_serve, ())
|
||||
|
||||
def _http_serve(self):
|
||||
if self._video_address is not None:
|
||||
self._httpd = HTTPServer((self._video_address, self._video_port), VideoHandler)
|
||||
logging.info("ui available at http://%s:%d/" % (self._video_address, self._video_port))
|
||||
self._httpd.serve_forever()
|
||||
else:
|
||||
logging.info("could not get ip of usb0, video server not starting")
|
||||
|
||||
def _is_inky(self):
|
||||
return self._display_type in ('inkyphat', 'inky')
|
||||
|
||||
def _is_papirus(self):
|
||||
return self._display_type in ('papirus', 'papi')
|
||||
|
||||
def _is_waveshare_v1(self):
|
||||
return self._display_type in ('waveshare_1', 'ws_1', 'waveshare1', 'ws1')
|
||||
|
||||
def _is_waveshare_v2(self):
|
||||
return self._display_type in ('waveshare_2', 'ws_2', 'waveshare2', 'ws2')
|
||||
|
||||
def _is_waveshare(self):
|
||||
return self._is_waveshare_v1() or self._is_waveshare_v2()
|
||||
|
||||
def _init_display(self):
|
||||
if self._is_inky():
|
||||
logging.info("initializing inky display")
|
||||
from inky import InkyPHAT
|
||||
self._display = InkyPHAT(self._display_color)
|
||||
self._display.set_border(InkyPHAT.BLACK)
|
||||
self._render_cb = self._inky_render
|
||||
|
||||
elif self._is_papirus():
|
||||
logging.info("initializing papirus display")
|
||||
from pwnagotchi.ui.papirus.epd import EPD
|
||||
os.environ['EPD_SIZE'] = '2.0'
|
||||
self._display = EPD()
|
||||
self._display.clear()
|
||||
self._render_cb = self._papirus_render
|
||||
|
||||
elif self._is_waveshare_v1():
|
||||
logging.info("initializing waveshare v1 display")
|
||||
from pwnagotchi.ui.waveshare.v1.epd2in13 import EPD
|
||||
self._display = EPD()
|
||||
self._display.init(self._display.lut_full_update)
|
||||
self._display.Clear(0xFF)
|
||||
self._display.init(self._display.lut_partial_update)
|
||||
self._render_cb = self._waveshare_render
|
||||
|
||||
elif self._is_waveshare_v2():
|
||||
logging.info("initializing waveshare v2 display")
|
||||
from pwnagotchi.ui.waveshare.v2.waveshare import EPD
|
||||
self._display = EPD()
|
||||
self._display.init(self._display.FULL_UPDATE)
|
||||
self._display.Clear(WHITE)
|
||||
self._display.init(self._display.PART_UPDATE)
|
||||
self._render_cb = self._waveshare_render
|
||||
|
||||
else:
|
||||
logging.critical("unknown display type %s" % self._display_type)
|
||||
|
||||
plugins.on('display_setup', self._display)
|
||||
|
||||
self.on_render(self._on_view_rendered)
|
||||
|
||||
def clear(self):
|
||||
if self._display is None:
|
||||
logging.error("no display object created")
|
||||
elif self._is_inky():
|
||||
self._display.Clear()
|
||||
elif self._is_papirus():
|
||||
self._display.clear()
|
||||
elif self._is_waveshare():
|
||||
self._display.Clear(WHITE)
|
||||
else:
|
||||
logging.critical("unknown display type %s" % self._display_type)
|
||||
|
||||
def _inky_render(self):
|
||||
if self._display_color != 'mono':
|
||||
display_colors = 3
|
||||
else:
|
||||
display_colors = 2
|
||||
|
||||
img_buffer = self._canvas.convert('RGB').convert('P', palette=1, colors=display_colors)
|
||||
if self._display_color == 'red':
|
||||
img_buffer.putpalette([
|
||||
255, 255, 255, # index 0 is white
|
||||
0, 0, 0, # index 1 is black
|
||||
255, 0, 0 # index 2 is red
|
||||
])
|
||||
elif self._display_color == 'yellow':
|
||||
img_buffer.putpalette([
|
||||
255, 255, 255, # index 0 is white
|
||||
0, 0, 0, # index 1 is black
|
||||
255, 255, 0 # index 2 is yellow
|
||||
])
|
||||
else:
|
||||
img_buffer.putpalette([
|
||||
255, 255, 255, # index 0 is white
|
||||
0, 0, 0 # index 1 is black
|
||||
])
|
||||
|
||||
self._display.set_image(img_buffer)
|
||||
try:
|
||||
self._display.show()
|
||||
except:
|
||||
print("")
|
||||
|
||||
def _papirus_render(self):
|
||||
self._display.display(self._canvas)
|
||||
self._display.partial_update()
|
||||
|
||||
def _waveshare_render(self):
|
||||
buf = self._display.getbuffer(self._canvas)
|
||||
if self._is_waveshare_v1():
|
||||
self._display.display(buf)
|
||||
elif self._is_waveshare_v2():
|
||||
self._display.displayPartial(buf)
|
||||
|
||||
def image(self):
|
||||
img = None
|
||||
if self._canvas is not None:
|
||||
img = self._canvas if self._rotation == 0 else self._canvas.rotate(-self._rotation)
|
||||
return img
|
||||
|
||||
def _on_view_rendered(self, img):
|
||||
VideoHandler.render(img)
|
||||
|
||||
if self._enabled:
|
||||
self._canvas = (img if self._rotation == 0 else img.rotate(self._rotation))
|
||||
if self._render_cb is not None:
|
||||
self._render_cb()
|
18
pwnagotchi/ui/faces.py
Normal file
18
pwnagotchi/ui/faces.py
Normal file
@ -0,0 +1,18 @@
|
||||
LOOK_R = '(⌐■_■)'
|
||||
LOOK_L = '(■_■¬)'
|
||||
SLEEP = '(⇀‿‿↼)'
|
||||
SLEEP2 = '(≖‿‿≖)'
|
||||
AWAKE = '(◕‿‿◕)'
|
||||
BORED = '(-__-)'
|
||||
INTENSE = '(°▃▃°)'
|
||||
COOL = '(⊙☁◉┐)'
|
||||
HAPPY = '(•‿‿•)'
|
||||
EXCITED = '(ᵔ◡◡ᵔ)'
|
||||
MOTIVATED = '(☼‿‿☼)'
|
||||
DEMOTIVATED = '(≖__≖)'
|
||||
SMART = '(✜‿‿✜)'
|
||||
LONELY = '(ب__ب)'
|
||||
SAD = '(╥☁╥ )'
|
||||
FRIEND = '(♥‿‿♥)'
|
||||
BROKEN = '(☓‿‿☓)'
|
||||
DEBUG = '(#__#)'
|
16
pwnagotchi/ui/fonts.py
Normal file
16
pwnagotchi/ui/fonts.py
Normal file
@ -0,0 +1,16 @@
|
||||
from PIL import ImageFont
|
||||
|
||||
PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSansMono'
|
||||
|
||||
Bold = ImageFont.truetype("%s-Bold.ttf" % PATH, 10)
|
||||
BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, 8)
|
||||
Medium = ImageFont.truetype("%s.ttf" % PATH, 10)
|
||||
Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, 25)
|
||||
|
||||
|
||||
def setup(bold, bold_small, medium, huge):
|
||||
global PATH, Bold, BoldSmall, Medium, Huge
|
||||
Bold = ImageFont.truetype("%s-Bold.ttf" % PATH, bold)
|
||||
BoldSmall = ImageFont.truetype("%s-Bold.ttf" % PATH, bold_small)
|
||||
Medium = ImageFont.truetype("%s.ttf" % PATH, medium)
|
||||
Huge = ImageFont.truetype("%s-Bold.ttf" % PATH, huge)
|
0
pwnagotchi/ui/papirus/__init__.py
Normal file
0
pwnagotchi/ui/papirus/__init__.py
Normal file
213
pwnagotchi/ui/papirus/epd.py
Normal file
213
pwnagotchi/ui/papirus/epd.py
Normal file
@ -0,0 +1,213 @@
|
||||
#qCopyright 2013-2015 Pervasive Displays, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at:
|
||||
#
|
||||
# http:#www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied. See the License for the specific language
|
||||
# governing permissions and limitations under the License.
|
||||
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageOps
|
||||
from pwnagotchi.ui.papirus.lm75b import LM75B
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3,):
|
||||
def b(x):
|
||||
return x
|
||||
else:
|
||||
def b(x):
|
||||
return x.encode('ISO-8859-1')
|
||||
|
||||
class EPDError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
class EPD(object):
|
||||
|
||||
"""EPD E-Ink interface
|
||||
|
||||
to use:
|
||||
from EPD import EPD
|
||||
|
||||
epd = EPD([path='/path/to/epd'], [auto=boolean], [rotation = 0|90|180|270])
|
||||
|
||||
image = Image.new('1', epd.size, 0)
|
||||
# draw on image
|
||||
epd.clear() # clear the panel
|
||||
epd.display(image) # tranfer image data
|
||||
epd.update() # refresh the panel image - not needed if auto=true
|
||||
"""
|
||||
|
||||
|
||||
PANEL_RE = re.compile('^([A-Za-z]+)\s+(\d+\.\d+)\s+(\d+)x(\d+)\s+COG\s+(\d+)\s+FILM\s+(\d+)\s*$', flags=0)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._epd_path = '/dev/epd'
|
||||
self._width = 200
|
||||
self._height = 96
|
||||
self._panel = 'EPD 2.0'
|
||||
self._cog = 0
|
||||
self._film = 0
|
||||
self._auto = False
|
||||
self._lm75b = LM75B()
|
||||
self._rotation = 0
|
||||
self._uselm75b = True
|
||||
|
||||
if len(args) > 0:
|
||||
self._epd_path = args[0]
|
||||
elif 'epd' in kwargs:
|
||||
self._epd_path = kwargs['epd']
|
||||
|
||||
if ('auto' in kwargs) and kwargs['auto']:
|
||||
self._auto = True
|
||||
if ('rotation' in kwargs):
|
||||
rot = kwargs['rotation']
|
||||
if rot in (0, 90, 180, 270):
|
||||
self._rotation = rot
|
||||
else:
|
||||
raise EPDError('rotation can only be 0, 90, 180 or 270')
|
||||
|
||||
with open(os.path.join(self._epd_path, 'version')) as f:
|
||||
self._version = f.readline().rstrip('\n')
|
||||
|
||||
with open(os.path.join(self._epd_path, 'panel')) as f:
|
||||
line = f.readline().rstrip('\n')
|
||||
m = self.PANEL_RE.match(line)
|
||||
if m is None:
|
||||
raise EPDError('invalid panel string')
|
||||
self._panel = m.group(1) + ' ' + m.group(2)
|
||||
self._width = int(m.group(3))
|
||||
self._height = int(m.group(4))
|
||||
self._cog = int(m.group(5))
|
||||
self._film = int(m.group(6))
|
||||
|
||||
if self._width < 1 or self._height < 1:
|
||||
raise EPDError('invalid panel geometry')
|
||||
if self._rotation in (90, 270):
|
||||
self._width, self._height = self._height, self._width
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return (self._width, self._height)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def panel(self):
|
||||
return self._panel
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
return self._cog
|
||||
|
||||
@property
|
||||
def film(self):
|
||||
return self._film
|
||||
|
||||
@property
|
||||
def auto(self):
|
||||
return self._auto
|
||||
|
||||
@auto.setter
|
||||
def auto(self, flag):
|
||||
if flag:
|
||||
self._auto = True
|
||||
else:
|
||||
self._auto = False
|
||||
|
||||
@property
|
||||
def rotation(self):
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, rot):
|
||||
if rot not in (0, 90, 180, 270):
|
||||
raise EPDError('rotation can only be 0, 90, 180 or 270')
|
||||
if abs(self._rotation - rot) == 90 or abs(self._rotation - rot) == 270:
|
||||
self._width, self._height = self._height, self._width
|
||||
self._rotation = rot
|
||||
|
||||
@property
|
||||
def use_lm75b(self):
|
||||
return self._uselm75b
|
||||
|
||||
@use_lm75b.setter
|
||||
def use_lm75b(self, flag):
|
||||
if flag:
|
||||
self._uselm75b = True
|
||||
else:
|
||||
self._uselm75b = False
|
||||
|
||||
def error_status(self):
|
||||
with open(os.path.join(self._epd_path, 'error'), 'r') as f:
|
||||
return(f.readline().rstrip('\n'))
|
||||
|
||||
def rotation_angle(self, rotation):
|
||||
angles = { 90 : Image.ROTATE_90, 180 : Image.ROTATE_180, 270 : Image.ROTATE_270 }
|
||||
return angles[rotation]
|
||||
|
||||
def display(self, image):
|
||||
|
||||
# attempt grayscale conversion, and then to single bit
|
||||
# better to do this before calling this if the image is to
|
||||
# be dispayed several times
|
||||
if image.mode != "1":
|
||||
image = ImageOps.grayscale(image).convert("1", dither=Image.FLOYDSTEINBERG)
|
||||
|
||||
if image.mode != "1":
|
||||
raise EPDError('only single bit images are supported')
|
||||
|
||||
if image.size != self.size:
|
||||
raise EPDError('image size mismatch')
|
||||
|
||||
if self._rotation != 0:
|
||||
image = image.transpose(self.rotation_angle(self._rotation))
|
||||
|
||||
with open(os.path.join(self._epd_path, 'LE', 'display_inverse'), 'r+b') as f:
|
||||
f.write(image.tobytes())
|
||||
|
||||
if self.auto:
|
||||
self.update()
|
||||
|
||||
|
||||
def update(self):
|
||||
self._command('U')
|
||||
|
||||
def partial_update(self):
|
||||
self._command('P')
|
||||
|
||||
def fast_update(self):
|
||||
self._command('F')
|
||||
|
||||
def clear(self):
|
||||
self._command('C')
|
||||
|
||||
def _command(self, c):
|
||||
if self._uselm75b:
|
||||
with open(os.path.join(self._epd_path, 'temperature'), 'wb') as f:
|
||||
f.write(b(repr(self._lm75b.getTempC())))
|
||||
with open(os.path.join(self._epd_path, 'command'), 'wb') as f:
|
||||
f.write(b(c))
|
46
pwnagotchi/ui/papirus/lm75b.py
Normal file
46
pwnagotchi/ui/papirus/lm75b.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Minimal support for LM75b temperature sensor on the Papirus HAT / Papirus Zero
|
||||
# This module allows you to read the temperature.
|
||||
# The OS-output (Over-temperature Shutdown) connected to GPIO xx (pin 11) is not supported
|
||||
# by this module
|
||||
#
|
||||
|
||||
from __future__ import (print_function, division)
|
||||
|
||||
import smbus
|
||||
|
||||
LM75B_ADDRESS = 0x48
|
||||
|
||||
LM75B_TEMP_REGISTER = 0
|
||||
LM75B_CONF_REGISTER = 1
|
||||
LM75B_THYST_REGISTER = 2
|
||||
LM75B_TOS_REGISTER = 3
|
||||
|
||||
LM75B_CONF_NORMAL = 0
|
||||
|
||||
class LM75B(object):
|
||||
def __init__(self, address=LM75B_ADDRESS, busnum=1):
|
||||
self._address = address
|
||||
self._bus = smbus.SMBus(busnum)
|
||||
self._bus.write_byte_data(self._address, LM75B_CONF_REGISTER, LM75B_CONF_NORMAL)
|
||||
|
||||
def getTempCFloat(self):
|
||||
"""Return temperature in degrees Celsius as float"""
|
||||
raw = self._bus.read_word_data(self._address, LM75B_TEMP_REGISTER) & 0xFFFF
|
||||
raw = ((raw << 8) & 0xFF00) + (raw >> 8)
|
||||
return (raw / 32.0) / 8.0
|
||||
|
||||
def getTempFFloat(self):
|
||||
"""Return temperature in degrees Fahrenheit as float"""
|
||||
return (self.getTempCFloat() * (9.0 / 5.0)) + 32.0
|
||||
|
||||
def getTempC(self):
|
||||
"""Return temperature in degrees Celsius as integer, so it can be
|
||||
used to write to /dev/epd/temperature"""
|
||||
raw = self._bus.read_word_data(self._address, LM75B_TEMP_REGISTER) & 0xFFFF
|
||||
raw = ((raw << 8) & 0xFF00) + (raw >> 8)
|
||||
return (raw + 128) // 256 # round to nearest integer
|
||||
|
||||
if __name__ == "__main__":
|
||||
sens = LM75B()
|
||||
print(sens.getTempC(), sens.getTempFFloat())
|
||||
|
52
pwnagotchi/ui/state.py
Normal file
52
pwnagotchi/ui/state.py
Normal file
@ -0,0 +1,52 @@
|
||||
from threading import Lock
|
||||
|
||||
|
||||
class State(object):
|
||||
def __init__(self, state={}):
|
||||
self._state = state
|
||||
self._lock = Lock()
|
||||
self._listeners = {}
|
||||
self._changes = {}
|
||||
|
||||
def add_element(self, key, elem):
|
||||
self._state[key] = elem
|
||||
self._changes[key] = True
|
||||
|
||||
def add_listener(self, key, cb):
|
||||
with self._lock:
|
||||
self._listeners[key] = cb
|
||||
|
||||
def items(self):
|
||||
with self._lock:
|
||||
return self._state.items()
|
||||
|
||||
def get(self, key):
|
||||
with self._lock:
|
||||
return self._state[key].value if key in self._state else None
|
||||
|
||||
def reset(self):
|
||||
with self._lock:
|
||||
self._changes = {}
|
||||
|
||||
def changes(self, ignore=()):
|
||||
with self._lock:
|
||||
changes = []
|
||||
for change in self._changes.keys():
|
||||
if change not in ignore:
|
||||
changes.append(change)
|
||||
return changes
|
||||
|
||||
def has_changes(self):
|
||||
with self._lock:
|
||||
return len(self._changes) > 0
|
||||
|
||||
def set(self, key, value):
|
||||
with self._lock:
|
||||
if key in self._state:
|
||||
prev = self._state[key].value
|
||||
self._state[key].value = value
|
||||
|
||||
if prev != value:
|
||||
self._changes[key] = True
|
||||
if key in self._listeners and self._listeners[key] is not None:
|
||||
self._listeners[key](prev, value)
|
333
pwnagotchi/ui/view.py
Normal file
333
pwnagotchi/ui/view.py
Normal file
@ -0,0 +1,333 @@
|
||||
import _thread
|
||||
from threading import Lock
|
||||
import time
|
||||
import logging
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.voice import Voice
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
import pwnagotchi.ui.faces as faces
|
||||
from pwnagotchi.ui.components import *
|
||||
from pwnagotchi.ui.state import State
|
||||
|
||||
WHITE = 0xff
|
||||
BLACK = 0x00
|
||||
|
||||
|
||||
def setup_display_specifics(config):
|
||||
width = 0
|
||||
height = 0
|
||||
face_pos = (0, 0)
|
||||
name_pos = (0, 0)
|
||||
status_pos = (0, 0)
|
||||
|
||||
if config['ui']['display']['type'] in ('inky', 'inkyphat'):
|
||||
fonts.setup(10, 8, 10, 25)
|
||||
|
||||
width = 212
|
||||
height = 104
|
||||
face_pos = (0, int(height / 4))
|
||||
name_pos = (int(width / 2) - 15, int(height * .15))
|
||||
status_pos = (int(width / 2) - 15, int(height * .30))
|
||||
|
||||
elif config['ui']['display']['type'] in ('papirus', 'papi'):
|
||||
fonts.setup(10, 8, 10, 23)
|
||||
|
||||
width = 200
|
||||
height = 96
|
||||
face_pos = (0, int(height / 4))
|
||||
name_pos = (int(width / 2) - 15, int(height * .15))
|
||||
status_pos = (int(width / 2) - 15, int(height * .30))
|
||||
|
||||
elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1',
|
||||
'ws_2', 'ws2', 'waveshare_2', 'waveshare2'):
|
||||
fonts.setup(10, 9, 10, 35)
|
||||
|
||||
width = 250
|
||||
height = 122
|
||||
face_pos = (0, 40)
|
||||
name_pos = (125, 20)
|
||||
status_pos = (125, 35)
|
||||
|
||||
return width, height, face_pos, name_pos, status_pos
|
||||
|
||||
|
||||
class View(object):
|
||||
def __init__(self, config, state={}):
|
||||
self._render_cbs = []
|
||||
self._config = config
|
||||
self._canvas = None
|
||||
self._lock = Lock()
|
||||
self._voice = Voice(lang=config['main']['lang'])
|
||||
|
||||
self._width, self._height, \
|
||||
face_pos, name_pos, status_pos = setup_display_specifics(config)
|
||||
|
||||
self._state = State(state={
|
||||
'channel': LabeledValue(color=BLACK, label='CH', value='00', position=(0, 0), label_font=fonts.Bold,
|
||||
text_font=fonts.Medium),
|
||||
'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=(30, 0), label_font=fonts.Bold,
|
||||
text_font=fonts.Medium),
|
||||
|
||||
# 'epoch': LabeledValue(color=BLACK, label='E', value='0000', position=(145, 0), label_font=fonts.Bold,
|
||||
# text_font=fonts.Medium),
|
||||
|
||||
'uptime': LabeledValue(color=BLACK, label='UP', value='00:00:00', position=(self._width - 65, 0),
|
||||
label_font=fonts.Bold,
|
||||
text_font=fonts.Medium),
|
||||
|
||||
'line1': Line([0, int(self._height * .12), self._width, int(self._height * .12)], color=BLACK),
|
||||
'line2': Line(
|
||||
[0, self._height - int(self._height * .12), self._width, self._height - int(self._height * .12)],
|
||||
color=BLACK),
|
||||
|
||||
'face': Text(value=faces.SLEEP, position=face_pos, color=BLACK, font=fonts.Huge),
|
||||
|
||||
'friend_face': Text(value=None, position=(0, (self._height * 0.88) - 15), font=fonts.Bold, color=BLACK),
|
||||
'friend_name': Text(value=None, position=(40, (self._height * 0.88) - 13), font=fonts.BoldSmall,
|
||||
color=BLACK),
|
||||
|
||||
'name': Text(value='%s>' % 'pwnagotchi', position=name_pos, color=BLACK, font=fonts.Bold),
|
||||
|
||||
'status': Text(value=self._voice.default(),
|
||||
position=status_pos,
|
||||
color=BLACK,
|
||||
font=fonts.Medium,
|
||||
wrap=True,
|
||||
# the current maximum number of characters per line, assuming each character is 6 pixels wide
|
||||
max_length=(self._width - status_pos[0]) // 6),
|
||||
|
||||
'shakes': LabeledValue(label='PWND ', value='0 (00)', color=BLACK,
|
||||
position=(0, self._height - int(self._height * .12) + 1), label_font=fonts.Bold,
|
||||
text_font=fonts.Medium),
|
||||
'mode': Text(value='AUTO', position=(self._width - 25, self._height - int(self._height * .12) + 1),
|
||||
font=fonts.Bold, color=BLACK),
|
||||
})
|
||||
|
||||
for key, value in state.items():
|
||||
self._state.set(key, value)
|
||||
|
||||
plugins.on('ui_setup', self)
|
||||
|
||||
if config['ui']['fps'] > 0.0:
|
||||
_thread.start_new_thread(self._refresh_handler, ())
|
||||
self._ignore_changes = ()
|
||||
else:
|
||||
logging.warning("ui.fps is 0, the display will only update for major changes")
|
||||
self._ignore_changes = ('uptime', 'name')
|
||||
|
||||
def add_element(self, key, elem):
|
||||
self._state.add_element(key, elem)
|
||||
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
def on_state_change(self, key, cb):
|
||||
self._state.add_listener(key, cb)
|
||||
|
||||
def on_render(self, cb):
|
||||
if cb not in self._render_cbs:
|
||||
self._render_cbs.append(cb)
|
||||
|
||||
def _refresh_handler(self):
|
||||
delay = 1.0 / self._config['ui']['fps']
|
||||
# logging.info("view refresh handler started with period of %.2fs" % delay)
|
||||
|
||||
while True:
|
||||
name = self._state.get('name')
|
||||
self.set('name', name.rstrip('█').strip() if '█' in name else (name + ' █'))
|
||||
self.update()
|
||||
time.sleep(delay)
|
||||
|
||||
def set(self, key, value):
|
||||
self._state.set(key, value)
|
||||
|
||||
def on_starting(self):
|
||||
self.set('status', self._voice.on_starting())
|
||||
self.set('face', faces.AWAKE)
|
||||
|
||||
def on_ai_ready(self):
|
||||
self.set('mode', '')
|
||||
self.set('face', faces.HAPPY)
|
||||
self.set('status', self._voice.on_ai_ready())
|
||||
self.update()
|
||||
|
||||
def on_manual_mode(self, log):
|
||||
self.set('mode', 'MANU')
|
||||
self.set('face', faces.SAD if log.handshakes == 0 else faces.HAPPY)
|
||||
self.set('status', self._voice.on_log(log))
|
||||
self.set('epoch', "%04d" % log.epochs)
|
||||
self.set('uptime', log.duration)
|
||||
self.set('channel', '-')
|
||||
self.set('aps', "%d" % log.associated)
|
||||
self.set('shakes', '%d (%s)' % (log.handshakes, \
|
||||
utils.total_unique_handshakes(self._config['bettercap']['handshakes'])))
|
||||
self.set_closest_peer(log.last_peer)
|
||||
|
||||
def is_normal(self):
|
||||
return self._state.get('face') not in (
|
||||
faces.INTENSE,
|
||||
faces.COOL,
|
||||
faces.BORED,
|
||||
faces.HAPPY,
|
||||
faces.EXCITED,
|
||||
faces.MOTIVATED,
|
||||
faces.DEMOTIVATED,
|
||||
faces.SMART,
|
||||
faces.SAD,
|
||||
faces.LONELY)
|
||||
|
||||
def on_normal(self):
|
||||
self.set('face', faces.AWAKE)
|
||||
self.set('status', self._voice.on_normal())
|
||||
self.update()
|
||||
|
||||
def set_closest_peer(self, peer):
|
||||
if peer is None:
|
||||
self.set('friend_face', None)
|
||||
self.set('friend_name', None)
|
||||
else:
|
||||
# ref. https://www.metageek.com/training/resources/understanding-rssi-2.html
|
||||
if peer.rssi >= -67:
|
||||
num_bars = 4
|
||||
elif peer.rssi >= -70:
|
||||
num_bars = 3
|
||||
elif peer.rssi >= -80:
|
||||
num_bars = 2
|
||||
else:
|
||||
num_bars = 1
|
||||
|
||||
name = '▌' * num_bars
|
||||
name += '│' * (4 - num_bars)
|
||||
name += ' %s %d (%d)' % (peer.name(), peer.pwnd_run(), peer.pwnd_total())
|
||||
|
||||
self.set('friend_face', peer.face())
|
||||
self.set('friend_name', name)
|
||||
self.update()
|
||||
|
||||
def on_new_peer(self, peer):
|
||||
self.set('face', faces.FRIEND)
|
||||
self.set('status', self._voice.on_new_peer(peer))
|
||||
self.update()
|
||||
|
||||
def on_lost_peer(self, peer):
|
||||
self.set('face', faces.LONELY)
|
||||
self.set('status', self._voice.on_lost_peer(peer))
|
||||
self.update()
|
||||
|
||||
def on_free_channel(self, channel):
|
||||
self.set('face', faces.SMART)
|
||||
self.set('status', self._voice.on_free_channel(channel))
|
||||
self.update()
|
||||
|
||||
def wait(self, secs, sleeping=True):
|
||||
was_normal = self.is_normal()
|
||||
part = secs / 10.0
|
||||
|
||||
for step in range(0, 10):
|
||||
# if we weren't in a normal state before goin
|
||||
# to sleep, keep that face and status on for
|
||||
# a while, otherwise the sleep animation will
|
||||
# always override any minor state change before it
|
||||
if was_normal or step > 5:
|
||||
if sleeping:
|
||||
if secs > 1:
|
||||
self.set('face', faces.SLEEP)
|
||||
self.set('status', self._voice.on_napping(int(secs)))
|
||||
else:
|
||||
self.set('face', faces.SLEEP2)
|
||||
self.set('status', self._voice.on_awakening())
|
||||
else:
|
||||
self.set('status', self._voice.on_waiting(int(secs)))
|
||||
if step % 2 == 0:
|
||||
self.set('face', faces.LOOK_R)
|
||||
else:
|
||||
self.set('face', faces.LOOK_L)
|
||||
|
||||
time.sleep(part)
|
||||
secs -= part
|
||||
|
||||
self.on_normal()
|
||||
|
||||
def on_bored(self):
|
||||
self.set('face', faces.BORED)
|
||||
self.set('status', self._voice.on_bored())
|
||||
self.update()
|
||||
|
||||
def on_sad(self):
|
||||
self.set('face', faces.SAD)
|
||||
self.set('status', self._voice.on_sad())
|
||||
self.update()
|
||||
|
||||
def on_motivated(self, reward):
|
||||
self.set('face', faces.MOTIVATED)
|
||||
self.set('status', self._voice.on_motivated(reward))
|
||||
self.update()
|
||||
|
||||
def on_demotivated(self, reward):
|
||||
self.set('face', faces.DEMOTIVATED)
|
||||
self.set('status', self._voice.on_demotivated(reward))
|
||||
self.update()
|
||||
|
||||
def on_excited(self):
|
||||
self.set('face', faces.EXCITED)
|
||||
self.set('status', self._voice.on_excited())
|
||||
self.update()
|
||||
|
||||
def on_assoc(self, ap):
|
||||
self.set('face', faces.INTENSE)
|
||||
self.set('status', self._voice.on_assoc(ap))
|
||||
self.update()
|
||||
|
||||
def on_deauth(self, sta):
|
||||
self.set('face', faces.COOL)
|
||||
self.set('status', self._voice.on_deauth(sta))
|
||||
self.update()
|
||||
|
||||
def on_miss(self, who):
|
||||
self.set('face', faces.SAD)
|
||||
self.set('status', self._voice.on_miss(who))
|
||||
self.update()
|
||||
|
||||
def on_lonely(self):
|
||||
self.set('face', faces.LONELY)
|
||||
self.set('status', self._voice.on_lonely())
|
||||
self.update()
|
||||
|
||||
def on_handshakes(self, new_shakes):
|
||||
self.set('face', faces.HAPPY)
|
||||
self.set('status', self._voice.on_handshakes(new_shakes))
|
||||
self.update()
|
||||
|
||||
def on_rebooting(self):
|
||||
self.set('face', faces.BROKEN)
|
||||
self.set('status', self._voice.on_rebooting())
|
||||
self.update()
|
||||
|
||||
def on_custom(self, text):
|
||||
self.set('face', faces.DEBUG)
|
||||
self.set('status', self._voice.custom(text))
|
||||
self.update()
|
||||
|
||||
def update(self, force=False):
|
||||
with self._lock:
|
||||
changes = self._state.changes(ignore=self._ignore_changes)
|
||||
if force or len(changes):
|
||||
self._canvas = Image.new('1', (self._width, self._height), WHITE)
|
||||
drawer = ImageDraw.Draw(self._canvas)
|
||||
|
||||
plugins.on('ui_update', self)
|
||||
|
||||
for key, lv in self._state.items():
|
||||
lv.draw(self._canvas, drawer)
|
||||
|
||||
for cb in self._render_cbs:
|
||||
cb(self._canvas)
|
||||
|
||||
self._state.reset()
|
0
pwnagotchi/ui/waveshare/__init__.py
Normal file
0
pwnagotchi/ui/waveshare/__init__.py
Normal file
0
pwnagotchi/ui/waveshare/v1/__init__.py
Normal file
0
pwnagotchi/ui/waveshare/v1/__init__.py
Normal file
225
pwnagotchi/ui/waveshare/v1/epd2in13.py
Normal file
225
pwnagotchi/ui/waveshare/v1/epd2in13.py
Normal file
@ -0,0 +1,225 @@
|
||||
# *****************************************************************************
|
||||
# * | File : epd2in13.py
|
||||
# * | Author : Waveshare team
|
||||
# * | Function : Electronic paper driver
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V4.0
|
||||
# * | Date : 2019-06-20
|
||||
# # | Info : python demo
|
||||
# -----------------------------------------------------------------------------
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documnetation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
from . import epdconfig
|
||||
import numpy as np
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 122
|
||||
EPD_HEIGHT = 250
|
||||
|
||||
class EPD:
|
||||
def __init__(self):
|
||||
self.reset_pin = epdconfig.RST_PIN
|
||||
self.dc_pin = epdconfig.DC_PIN
|
||||
self.busy_pin = epdconfig.BUSY_PIN
|
||||
self.cs_pin = epdconfig.CS_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
|
||||
lut_full_update = [
|
||||
0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
]
|
||||
|
||||
lut_partial_update = [
|
||||
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
]
|
||||
|
||||
# Hardware reset
|
||||
def reset(self):
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(200)
|
||||
epdconfig.digital_write(self.reset_pin, 0)
|
||||
epdconfig.delay_ms(10)
|
||||
epdconfig.digital_write(self.reset_pin, 1)
|
||||
epdconfig.delay_ms(200)
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_command(self, command):
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.digital_write(self.dc_pin, 0)
|
||||
epdconfig.spi_writebyte([command])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def send_data(self, data):
|
||||
epdconfig.digital_write(self.cs_pin, 0)
|
||||
epdconfig.digital_write(self.dc_pin, 1)
|
||||
epdconfig.spi_writebyte([data])
|
||||
epdconfig.digital_write(self.cs_pin, 1)
|
||||
|
||||
def ReadBusy(self):
|
||||
while(epdconfig.digital_read(self.busy_pin) == 1): # 0: idle, 1: busy
|
||||
epdconfig.delay_ms(100)
|
||||
|
||||
def TurnOnDisplay(self):
|
||||
self.send_command(0x22) # DISPLAY_UPDATE_CONTROL_2
|
||||
self.send_data(0xC4)
|
||||
self.send_command(0x20) # MASTER_ACTIVATION
|
||||
self.send_command(0xFF) # TERMINATE_FRAME_READ_WRITE
|
||||
|
||||
logging.debug("e-Paper busy")
|
||||
self.ReadBusy()
|
||||
logging.debug("e-Paper busy release")
|
||||
|
||||
def init(self, lut):
|
||||
if (epdconfig.module_init() != 0):
|
||||
return -1
|
||||
# EPD hardware init start
|
||||
self.reset()
|
||||
self.send_command(0x01) # DRIVER_OUTPUT_CONTROL
|
||||
self.send_data((EPD_HEIGHT - 1) & 0xFF)
|
||||
self.send_data(((EPD_HEIGHT - 1) >> 8) & 0xFF)
|
||||
self.send_data(0x00) # GD = 0 SM = 0 TB = 0
|
||||
|
||||
self.send_command(0x0C) # BOOSTER_SOFT_START_CONTROL
|
||||
self.send_data(0xD7)
|
||||
self.send_data(0xD6)
|
||||
self.send_data(0x9D)
|
||||
|
||||
self.send_command(0x2C) # WRITE_VCOM_REGISTER
|
||||
self.send_data(0xA8) # VCOM 7C
|
||||
|
||||
self.send_command(0x3A) # SET_DUMMY_LINE_PERIOD
|
||||
self.send_data(0x1A) # 4 dummy lines per gate
|
||||
|
||||
self.send_command(0x3B) # SET_GATE_TIME
|
||||
self.send_data(0x08) # 2us per line
|
||||
|
||||
self.send_command(0X3C) # BORDER_WAVEFORM_CONTROL
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0X11) # DATA_ENTRY_MODE_SETTING
|
||||
self.send_data(0x03) # X increment; Y increment
|
||||
|
||||
# WRITE_LUT_REGISTER
|
||||
self.send_command(0x32)
|
||||
for count in range(30):
|
||||
self.send_data(lut[count])
|
||||
|
||||
return 0
|
||||
|
||||
##
|
||||
# @brief: specify the memory area for data R/W
|
||||
##
|
||||
def SetWindows(self, x_start, y_start, x_end, y_end):
|
||||
self.send_command(0x44) # SET_RAM_X_ADDRESS_START_END_POSITION
|
||||
self.send_data((x_start >> 3) & 0xFF)
|
||||
self.send_data((x_end >> 3) & 0xFF)
|
||||
self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION
|
||||
self.send_data(y_start & 0xFF)
|
||||
self.send_data((y_start >> 8) & 0xFF)
|
||||
self.send_data(y_end & 0xFF)
|
||||
self.send_data((y_end >> 8) & 0xFF)
|
||||
|
||||
##
|
||||
# @brief: specify the start point for data R/W
|
||||
##
|
||||
def SetCursor(self, x, y):
|
||||
self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER
|
||||
# x point must be the multiple of 8 or the last 3 bits will be ignored
|
||||
self.send_data((x >> 3) & 0xFF)
|
||||
self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER
|
||||
self.send_data(y & 0xFF)
|
||||
self.send_data((y >> 8) & 0xFF)
|
||||
self.ReadBusy()
|
||||
|
||||
def getbuffer(self, image):
|
||||
if self.width%8 == 0:
|
||||
linewidth = int(self.width/8)
|
||||
else:
|
||||
linewidth = int(self.width/8) + 1
|
||||
|
||||
buf = [0xFF] * (linewidth * self.height)
|
||||
image_monocolor = image.convert('1')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
|
||||
if(imwidth == self.width and imheight == self.height):
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
if pixels[x, y] == 0:
|
||||
# x = imwidth - x
|
||||
buf[int(x / 8) + y * linewidth] &= ~(0x80 >> (x % 8))
|
||||
elif(imwidth == self.height and imheight == self.width):
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if pixels[x, y] == 0:
|
||||
# newy = imwidth - newy - 1
|
||||
buf[int(newx / 8) + newy*linewidth] &= ~(0x80 >> (y % 8))
|
||||
return buf
|
||||
|
||||
|
||||
def display(self, image):
|
||||
if self.width%8 == 0:
|
||||
linewidth = int(self.width/8)
|
||||
else:
|
||||
linewidth = int(self.width/8) + 1
|
||||
|
||||
self.SetWindows(0, 0, self.width, self.height);
|
||||
for j in range(0, self.height):
|
||||
self.SetCursor(0, j);
|
||||
self.send_command(0x24);
|
||||
for i in range(0, linewidth):
|
||||
self.send_data(image[i + j * linewidth])
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def Clear(self, color):
|
||||
if self.width%8 == 0:
|
||||
linewidth = int(self.width/8)
|
||||
else:
|
||||
linewidth = int(self.width/8) + 1
|
||||
|
||||
self.SetWindows(0, 0, self.width, self.height);
|
||||
for j in range(0, self.height):
|
||||
self.SetCursor(0, j);
|
||||
self.send_command(0x24);
|
||||
for i in range(0, linewidth):
|
||||
self.send_data(color)
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def sleep(self):
|
||||
self.send_command(0x10) #enter deep sleep
|
||||
self.send_data(0x01)
|
||||
epdconfig.delay_ms(100)
|
||||
|
||||
epdconfig.module_exit()
|
||||
|
||||
### END OF FILE ###
|
||||
|
154
pwnagotchi/ui/waveshare/v1/epdconfig.py
Normal file
154
pwnagotchi/ui/waveshare/v1/epdconfig.py
Normal file
@ -0,0 +1,154 @@
|
||||
# /*****************************************************************************
|
||||
# * | File : epdconfig.py
|
||||
# * | Author : Waveshare team
|
||||
# * | Function : Hardware underlying interface
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V1.0
|
||||
# * | Date : 2019-06-21
|
||||
# * | Info :
|
||||
# ******************************************************************************
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documnetation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
class RaspberryPi:
|
||||
# Pin definition
|
||||
RST_PIN = 17
|
||||
DC_PIN = 25
|
||||
CS_PIN = 8
|
||||
BUSY_PIN = 24
|
||||
|
||||
def __init__(self):
|
||||
import spidev
|
||||
import RPi.GPIO
|
||||
|
||||
self.GPIO = RPi.GPIO
|
||||
|
||||
# SPI device, bus = 0, device = 0
|
||||
self.SPI = spidev.SpiDev(0, 0)
|
||||
|
||||
def digital_write(self, pin, value):
|
||||
self.GPIO.output(pin, value)
|
||||
|
||||
def digital_read(self, pin):
|
||||
return self.GPIO.input(self.BUSY_PIN)
|
||||
|
||||
def delay_ms(self, delaytime):
|
||||
time.sleep(delaytime / 1000.0)
|
||||
|
||||
def spi_writebyte(self, data):
|
||||
self.SPI.writebytes(data)
|
||||
|
||||
def module_init(self):
|
||||
self.GPIO.setmode(self.GPIO.BCM)
|
||||
self.GPIO.setwarnings(False)
|
||||
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
|
||||
self.SPI.max_speed_hz = 4000000
|
||||
self.SPI.mode = 0b00
|
||||
return 0
|
||||
|
||||
def module_exit(self):
|
||||
logging.debug("spi end")
|
||||
self.SPI.close()
|
||||
|
||||
logging.debug("close 5V, Module enters 0 power consumption ...")
|
||||
self.GPIO.output(self.RST_PIN, 0)
|
||||
self.GPIO.output(self.DC_PIN, 0)
|
||||
|
||||
self.GPIO.cleanup()
|
||||
|
||||
|
||||
class JetsonNano:
|
||||
# Pin definition
|
||||
RST_PIN = 17
|
||||
DC_PIN = 25
|
||||
CS_PIN = 8
|
||||
BUSY_PIN = 24
|
||||
|
||||
def __init__(self):
|
||||
import ctypes
|
||||
find_dirs = [
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'/usr/local/lib',
|
||||
'/usr/lib',
|
||||
]
|
||||
self.SPI = None
|
||||
for find_dir in find_dirs:
|
||||
so_filename = os.path.join(find_dir, 'sysfs_software_spi.so')
|
||||
if os.path.exists(so_filename):
|
||||
self.SPI = ctypes.cdll.LoadLibrary(so_filename)
|
||||
break
|
||||
if self.SPI is None:
|
||||
raise RuntimeError('Cannot find sysfs_software_spi.so')
|
||||
|
||||
import Jetson.GPIO
|
||||
self.GPIO = Jetson.GPIO
|
||||
|
||||
def digital_write(self, pin, value):
|
||||
self.GPIO.output(pin, value)
|
||||
|
||||
def digital_read(self, pin):
|
||||
return self.GPIO.input(self.BUSY_PIN)
|
||||
|
||||
def delay_ms(self, delaytime):
|
||||
time.sleep(delaytime / 1000.0)
|
||||
|
||||
def spi_writebyte(self, data):
|
||||
self.SPI.SYSFS_software_spi_transfer(data[0])
|
||||
|
||||
def module_init(self):
|
||||
self.GPIO.setmode(self.GPIO.BCM)
|
||||
self.GPIO.setwarnings(False)
|
||||
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
|
||||
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
|
||||
self.SPI.SYSFS_software_spi_begin()
|
||||
return 0
|
||||
|
||||
def module_exit(self):
|
||||
logging.debug("spi end")
|
||||
self.SPI.SYSFS_software_spi_end()
|
||||
|
||||
logging.debug("close 5V, Module enters 0 power consumption ...")
|
||||
self.GPIO.output(self.RST_PIN, 0)
|
||||
self.GPIO.output(self.DC_PIN, 0)
|
||||
|
||||
self.GPIO.cleanup()
|
||||
|
||||
|
||||
if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
|
||||
implementation = RaspberryPi()
|
||||
else:
|
||||
implementation = JetsonNano()
|
||||
|
||||
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
||||
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
||||
|
||||
|
||||
### END OF FILE ###
|
0
pwnagotchi/ui/waveshare/v2/__init__.py
Normal file
0
pwnagotchi/ui/waveshare/v2/__init__.py
Normal file
338
pwnagotchi/ui/waveshare/v2/waveshare.py
Normal file
338
pwnagotchi/ui/waveshare/v2/waveshare.py
Normal file
@ -0,0 +1,338 @@
|
||||
# //*****************************************************************************
|
||||
# * | File : epd2in13.py
|
||||
# * | Author : Waveshare team
|
||||
# * | Function : Electronic paper driver
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V3.0
|
||||
# * | Date : 2018-11-01
|
||||
# * | Info : python2 demo
|
||||
# * 1.Remove:
|
||||
# digital_write(self, pin, value)
|
||||
# digital_read(self, pin)
|
||||
# delay_ms(self, delaytime)
|
||||
# set_lut(self, lut)
|
||||
# self.lut = self.lut_full_update
|
||||
# * 2.Change:
|
||||
# display_frame -> TurnOnDisplay
|
||||
# set_memory_area -> SetWindow
|
||||
# set_memory_pointer -> SetCursor
|
||||
# * 3.How to use
|
||||
# epd = epd2in13.EPD()
|
||||
# epd.init(epd.lut_full_update)
|
||||
# image = Image.new('1', (epd2in13.EPD_WIDTH, epd2in13.EPD_HEIGHT), 255)
|
||||
# ...
|
||||
# drawing ......
|
||||
# ...
|
||||
# epd.display(getbuffer(image))
|
||||
# ******************************************************************************//
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documnetation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and//or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
import time
|
||||
import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
from PIL import Image
|
||||
|
||||
# Pin definition
|
||||
RST_PIN = 17
|
||||
DC_PIN = 25
|
||||
CS_PIN = 8
|
||||
BUSY_PIN = 24
|
||||
|
||||
# SPI device, bus = 0, device = 0
|
||||
SPI = spidev.SpiDev(0, 0)
|
||||
|
||||
|
||||
def digital_write(pin, value):
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
def digital_read(pin):
|
||||
return GPIO.input(BUSY_PIN)
|
||||
|
||||
|
||||
def delay_ms(delaytime):
|
||||
time.sleep(delaytime / 1000.0)
|
||||
|
||||
|
||||
def spi_writebyte(data):
|
||||
SPI.writebytes(data)
|
||||
|
||||
|
||||
def module_init():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(RST_PIN, GPIO.OUT)
|
||||
GPIO.setup(DC_PIN, GPIO.OUT)
|
||||
GPIO.setup(CS_PIN, GPIO.OUT)
|
||||
GPIO.setup(BUSY_PIN, GPIO.IN)
|
||||
SPI.max_speed_hz = 2000000
|
||||
SPI.mode = 0b00
|
||||
return 0;
|
||||
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 122
|
||||
EPD_HEIGHT = 250
|
||||
|
||||
|
||||
class EPD:
|
||||
def __init__(self):
|
||||
self.reset_pin = RST_PIN
|
||||
self.dc_pin = DC_PIN
|
||||
self.busy_pin = BUSY_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
|
||||
FULL_UPDATE = 0
|
||||
PART_UPDATE = 1
|
||||
lut_full_update = [
|
||||
0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, # LUT0: BB: VS 0 ~7
|
||||
0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, # LUT1: BW: VS 0 ~7
|
||||
0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, # LUT2: WB: VS 0 ~7
|
||||
0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, # LUT3: WW: VS 0 ~7
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # LUT4: VCOM: VS 0 ~7
|
||||
|
||||
0x03, 0x03, 0x00, 0x00, 0x02, # TP0 A~D RP0
|
||||
0x09, 0x09, 0x00, 0x00, 0x02, # TP1 A~D RP1
|
||||
0x03, 0x03, 0x00, 0x00, 0x02, # TP2 A~D RP2
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP3 A~D RP3
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP4 A~D RP4
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP5 A~D RP5
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP6 A~D RP6
|
||||
|
||||
0x15, 0x41, 0xA8, 0x32, 0x30, 0x0A,
|
||||
]
|
||||
|
||||
lut_partial_update = [ # 20 bytes
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # LUT0: BB: VS 0 ~7
|
||||
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # LUT1: BW: VS 0 ~7
|
||||
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # LUT2: WB: VS 0 ~7
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # LUT3: WW: VS 0 ~7
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # LUT4: VCOM: VS 0 ~7
|
||||
|
||||
0x0A, 0x00, 0x00, 0x00, 0x00, # TP0 A~D RP0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP1 A~D RP1
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP2 A~D RP2
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP3 A~D RP3
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP4 A~D RP4
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP5 A~D RP5
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, # TP6 A~D RP6
|
||||
|
||||
0x15, 0x41, 0xA8, 0x32, 0x30, 0x0A,
|
||||
]
|
||||
|
||||
# Hardware reset
|
||||
def reset(self):
|
||||
digital_write(self.reset_pin, GPIO.HIGH)
|
||||
delay_ms(200)
|
||||
digital_write(self.reset_pin, GPIO.LOW) # module reset
|
||||
delay_ms(200)
|
||||
digital_write(self.reset_pin, GPIO.HIGH)
|
||||
delay_ms(200)
|
||||
|
||||
def send_command(self, command):
|
||||
digital_write(self.dc_pin, GPIO.LOW)
|
||||
spi_writebyte([command])
|
||||
|
||||
def send_data(self, data):
|
||||
digital_write(self.dc_pin, GPIO.HIGH)
|
||||
spi_writebyte([data])
|
||||
|
||||
def wait_until_idle(self):
|
||||
while (digital_read(self.busy_pin) == 1): # 0: idle, 1: busy
|
||||
delay_ms(100)
|
||||
|
||||
def TurnOnDisplay(self):
|
||||
self.send_command(0x22)
|
||||
self.send_data(0xC7)
|
||||
self.send_command(0x20)
|
||||
self.wait_until_idle()
|
||||
|
||||
def init(self, update):
|
||||
if (module_init() != 0):
|
||||
return -1
|
||||
# EPD hardware init start
|
||||
self.reset()
|
||||
if (update == self.FULL_UPDATE):
|
||||
self.wait_until_idle()
|
||||
self.send_command(0x12) # soft reset
|
||||
self.wait_until_idle()
|
||||
|
||||
self.send_command(0x74) # set analog block control
|
||||
self.send_data(0x54)
|
||||
self.send_command(0x7E) # set digital block control
|
||||
self.send_data(0x3B)
|
||||
|
||||
self.send_command(0x01) # Driver output control
|
||||
self.send_data(0xF9)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x11) # data entry mode
|
||||
self.send_data(0x01)
|
||||
|
||||
self.send_command(0x44) # set Ram-X address start//end position
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x0F) # 0x0C-->(15+1)*8=128
|
||||
|
||||
self.send_command(0x45) # set Ram-Y address start//end position
|
||||
self.send_data(0xF9) # 0xF9-->(249+1)=250
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x3C) # BorderWavefrom
|
||||
self.send_data(0x03)
|
||||
|
||||
self.send_command(0x2C) # VCOM Voltage
|
||||
self.send_data(0x55) #
|
||||
|
||||
self.send_command(0x03)
|
||||
self.send_data(self.lut_full_update[70])
|
||||
|
||||
self.send_command(0x04) #
|
||||
self.send_data(self.lut_full_update[71])
|
||||
self.send_data(self.lut_full_update[72])
|
||||
self.send_data(self.lut_full_update[73])
|
||||
|
||||
self.send_command(0x3A) # Dummy Line
|
||||
self.send_data(self.lut_full_update[74])
|
||||
self.send_command(0x3B) # Gate time
|
||||
self.send_data(self.lut_full_update[75])
|
||||
|
||||
self.send_command(0x32)
|
||||
for count in range(70):
|
||||
self.send_data(self.lut_full_update[count])
|
||||
|
||||
self.send_command(0x4E) # set RAM x address count to 0
|
||||
self.send_data(0x00)
|
||||
self.send_command(0x4F) # set RAM y address count to 0X127
|
||||
self.send_data(0xF9)
|
||||
self.send_data(0x00)
|
||||
self.wait_until_idle()
|
||||
else:
|
||||
self.send_command(0x2C) # VCOM Voltage
|
||||
self.send_data(0x26)
|
||||
|
||||
self.wait_until_idle()
|
||||
|
||||
self.send_command(0x32)
|
||||
for count in range(70):
|
||||
self.send_data(self.lut_partial_update[count])
|
||||
|
||||
self.send_command(0x37)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x40)
|
||||
self.send_data(0x00)
|
||||
self.send_data(0x00)
|
||||
|
||||
self.send_command(0x22)
|
||||
self.send_data(0xC0)
|
||||
self.send_command(0x20)
|
||||
self.wait_until_idle()
|
||||
|
||||
self.send_command(0x3C) # BorderWavefrom
|
||||
self.send_data(0x01)
|
||||
return 0
|
||||
|
||||
def getbuffer(self, image):
|
||||
if self.width % 8 == 0:
|
||||
linewidth = self.width // 8
|
||||
else:
|
||||
linewidth = self.width // 8 + 1
|
||||
|
||||
buf = [0xFF] * (linewidth * self.height)
|
||||
image_monocolor = image.convert('1')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
|
||||
if (imwidth == self.width and imheight == self.height):
|
||||
# print("Vertical")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
if pixels[x, y] == 0:
|
||||
x = imwidth - x
|
||||
buf[x // 8 + y * linewidth] &= ~(0x80 >> (x % 8))
|
||||
elif (imwidth == self.height and imheight == self.width):
|
||||
# print("Horizontal")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if pixels[x, y] == 0:
|
||||
newy = imwidth - newy - 1
|
||||
buf[newx // 8 + newy * linewidth] &= ~(0x80 >> (y % 8))
|
||||
return buf
|
||||
|
||||
def display(self, image):
|
||||
if self.width % 8 == 0:
|
||||
linewidth = self.width // 8
|
||||
else:
|
||||
linewidth = self.width // 8 + 1
|
||||
|
||||
self.send_command(0x24)
|
||||
for j in range(0, self.height):
|
||||
for i in range(0, linewidth):
|
||||
self.send_data(image[i + j * linewidth])
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def displayPartial(self, image):
|
||||
if self.width % 8 == 0:
|
||||
linewidth = self.width // 8
|
||||
else:
|
||||
linewidth = self.width // 8 + 1
|
||||
|
||||
self.send_command(0x24)
|
||||
for j in range(0, self.height):
|
||||
for i in range(0, linewidth):
|
||||
self.send_data(image[i + j * linewidth])
|
||||
self.send_command(0x26)
|
||||
for j in range(0, self.height):
|
||||
for i in range(0, linewidth):
|
||||
self.send_data(~image[i + j * linewidth])
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def Clear(self, color):
|
||||
if self.width % 8 == 0:
|
||||
linewidth = self.width // 8
|
||||
else:
|
||||
linewidth = self.width // 8 + 1
|
||||
# print(linewidth)
|
||||
|
||||
self.send_command(0x24)
|
||||
for j in range(0, self.height):
|
||||
for i in range(0, linewidth):
|
||||
self.send_data(color)
|
||||
self.TurnOnDisplay()
|
||||
|
||||
def sleep(self):
|
||||
self.send_command(0x22) # POWER OFF
|
||||
self.send_data(0xC3)
|
||||
self.send_command(0x20)
|
||||
|
||||
self.send_command(0x10) # enter deep sleep
|
||||
self.send_data(0x01)
|
||||
delay_ms(100)
|
||||
|
||||
### END OF FILE ###
|
98
pwnagotchi/utils.py
Normal file
98
pwnagotchi/utils.py
Normal file
@ -0,0 +1,98 @@
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/823196/yaml-merge-in-python
|
||||
def merge_config(user, default):
|
||||
if isinstance(user, dict) and isinstance(default, dict):
|
||||
for k, v in default.items():
|
||||
if k not in user:
|
||||
user[k] = v
|
||||
else:
|
||||
user[k] = merge_config(user[k], v)
|
||||
return user
|
||||
|
||||
|
||||
def load_config(args):
|
||||
with open(args.config, 'rt') as fp:
|
||||
config = yaml.safe_load(fp)
|
||||
|
||||
if os.path.exists(args.user_config):
|
||||
with open(args.user_config, 'rt') as fp:
|
||||
user_config = yaml.safe_load(fp)
|
||||
config = merge_config(user_config, config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def setup_logging(args, config):
|
||||
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
|
||||
root = logging.getLogger()
|
||||
|
||||
root.setLevel(logging.DEBUG if args.debug else logging.INFO)
|
||||
|
||||
if config['main']['log']:
|
||||
file_handler = logging.FileHandler(config['main']['log'])
|
||||
file_handler.setFormatter(formatter)
|
||||
root.addHandler(file_handler)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
root.addHandler(console_handler)
|
||||
|
||||
|
||||
def secs_to_hhmmss(secs):
|
||||
mins, secs = divmod(secs, 60)
|
||||
hours, mins = divmod(mins, 60)
|
||||
return '%02d:%02d:%02d' % (hours, mins, secs)
|
||||
|
||||
|
||||
def total_unique_handshakes(path):
|
||||
expr = os.path.join(path, "*.pcap")
|
||||
return len(glob.glob(expr))
|
||||
|
||||
|
||||
def iface_channels(ifname):
|
||||
channels = []
|
||||
output = subprocess.getoutput("/sbin/iwlist %s freq" % ifname)
|
||||
for line in output.split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("Channel "):
|
||||
channels.append(int(line.split()[1]))
|
||||
return channels
|
||||
|
||||
|
||||
def led(on=True):
|
||||
with open('/sys/class/leds/led0/brightness', 'w+t') as fp:
|
||||
fp.write("%d" % (0 if on is True else 1))
|
||||
|
||||
|
||||
def blink(times=1, delay=0.3):
|
||||
for t in range(0, times):
|
||||
led(True)
|
||||
time.sleep(delay)
|
||||
led(False)
|
||||
time.sleep(delay)
|
||||
led(True)
|
||||
|
||||
|
||||
class StatusFile(object):
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
self._updated = None
|
||||
|
||||
if os.path.exists(path):
|
||||
self._updated = datetime.fromtimestamp(os.path.getmtime(path))
|
||||
|
||||
def newer_then_days(self, days):
|
||||
return self._updated is not None and (datetime.now() - self._updated).days < days
|
||||
|
||||
def update(self, data=None):
|
||||
self._updated = datetime.now()
|
||||
with open(self._path, 'w') as fp:
|
||||
fp.write(str(self._updated) if data is None else data)
|
158
pwnagotchi/voice.py
Normal file
158
pwnagotchi/voice.py
Normal file
@ -0,0 +1,158 @@
|
||||
import random
|
||||
import gettext
|
||||
import os
|
||||
|
||||
|
||||
class Voice:
|
||||
def __init__(self, lang):
|
||||
localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale')
|
||||
translation = gettext.translation(
|
||||
'voice', localedir,
|
||||
languages=[lang],
|
||||
fallback=True,
|
||||
)
|
||||
translation.install()
|
||||
self._ = translation.gettext
|
||||
|
||||
def default(self):
|
||||
return self._('ZzzzZZzzzzZzzz')
|
||||
|
||||
def on_starting(self):
|
||||
return random.choice([
|
||||
self._('Hi, I\'m Pwnagotchi! Starting ...'),
|
||||
self._('New day, new hunt, new pwns!'),
|
||||
self._('Hack the Planet!')])
|
||||
|
||||
def on_ai_ready(self):
|
||||
return random.choice([
|
||||
self._('AI ready.'),
|
||||
self._('The neural network is ready.')])
|
||||
|
||||
def on_normal(self):
|
||||
return random.choice([
|
||||
'',
|
||||
'...'])
|
||||
|
||||
def on_free_channel(self, channel):
|
||||
return self._('Hey, channel {channel} is free! Your AP will say thanks.').format(channel=channel)
|
||||
|
||||
def on_bored(self):
|
||||
return random.choice([
|
||||
self._('I\'m bored ...'),
|
||||
self._('Let\'s go for a walk!')])
|
||||
|
||||
def on_motivated(self, reward):
|
||||
return self._('This is the best day of my life!')
|
||||
|
||||
def on_demotivated(self, reward):
|
||||
return self._('Shitty day :/')
|
||||
|
||||
def on_sad(self):
|
||||
return random.choice([
|
||||
self._('I\'m extremely bored ...'),
|
||||
self._('I\'m very sad ...'),
|
||||
self._('I\'m sad'),
|
||||
'...'])
|
||||
|
||||
def on_excited(self):
|
||||
return random.choice([
|
||||
self._('I\'m living the life!'),
|
||||
self._('I pwn therefore I am.'),
|
||||
self._('So many networks!!!'),
|
||||
self._('I\'m having so much fun!'),
|
||||
self._('My crime is that of curiosity ...')])
|
||||
|
||||
def on_new_peer(self, peer):
|
||||
return random.choice([
|
||||
self._('Hello {name}! Nice to meet you. {name}').format(name=peer.name()),
|
||||
self._('Unit {name} is nearby! {name}').format(name=peer.name())])
|
||||
|
||||
def on_lost_peer(self, peer):
|
||||
return random.choice([
|
||||
self._('Uhm ... goodbye {name}').format(name=peer.name()),
|
||||
self._('{name} is gone ...').format(name=peer.name())])
|
||||
|
||||
def on_miss(self, who):
|
||||
return random.choice([
|
||||
self._('Whoops ... {name} is gone.').format(name=who),
|
||||
self._('{name} missed!').format(name=who),
|
||||
self._('Missed!')])
|
||||
|
||||
def on_lonely(self):
|
||||
return random.choice([
|
||||
self._('Nobody wants to play with me ...'),
|
||||
self._('I feel so alone ...'),
|
||||
self._('Where\'s everybody?!')])
|
||||
|
||||
def on_napping(self, secs):
|
||||
return random.choice([
|
||||
self._('Napping for {secs}s ...').format(secs=secs),
|
||||
self._('Zzzzz'),
|
||||
self._('ZzzZzzz ({secs}s)').format(secs=secs)])
|
||||
|
||||
def on_awakening(self):
|
||||
return random.choice(['...', '!'])
|
||||
|
||||
def on_waiting(self, secs):
|
||||
return random.choice([
|
||||
self._('Waiting for {secs}s ...').format(secs=secs),
|
||||
'...',
|
||||
self._('Looking around ({secs}s)').format(secs=secs)])
|
||||
|
||||
def on_assoc(self, ap):
|
||||
ssid, bssid = ap['hostname'], ap['mac']
|
||||
what = ssid if ssid != '' and ssid != '<hidden>' else bssid
|
||||
return random.choice([
|
||||
self._('Hey {what} let\'s be friends!').format(what=what),
|
||||
self._('Associating to {what}').format(what=what),
|
||||
self._('Yo {what}!').format(what=what)])
|
||||
|
||||
def on_deauth(self, sta):
|
||||
return random.choice([
|
||||
self._('Just decided that {mac} needs no WiFi!').format(mac=sta['mac']),
|
||||
self._('Deauthenticating {mac}').format(mac=sta['mac']),
|
||||
self._('Kickbanning {mac}!').format(mac=sta['mac'])])
|
||||
|
||||
def on_handshakes(self, new_shakes):
|
||||
s = 's' if new_shakes > 1 else ''
|
||||
return self._('Cool, we got {num} new handshake{plural}!').format(num=new_shakes, plural=s)
|
||||
|
||||
def on_rebooting(self):
|
||||
return self._("Ops, something went wrong ... Rebooting ...")
|
||||
|
||||
def on_log(self, log):
|
||||
status = self._('Kicked {num} stations\n').format(num=log.deauthed)
|
||||
status += self._('Made {num} new friends\n').format(num=log.associated)
|
||||
status += self._('Got {num} handshakes\n').format(num=log.handshakes)
|
||||
if log.peers == 1:
|
||||
status += self._('Met 1 peer')
|
||||
elif log.peers > 0:
|
||||
status += self._('Met {num} peers').format(num=log.peers)
|
||||
return status
|
||||
|
||||
def on_log_tweet(self, log):
|
||||
return self._(
|
||||
'I\'ve been pwning for {duration} and kicked {deauthed} clients! I\'ve also met {associated} new friends and ate {handshakes} handshakes! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet').format(
|
||||
duration=log.duration_human,
|
||||
deauthed=log.deauthed,
|
||||
associated=log.associated,
|
||||
handshakes=log.handshakes)
|
||||
|
||||
def hhmmss(self, count, fmt):
|
||||
if count > 1:
|
||||
# plural
|
||||
if fmt == "h":
|
||||
return self._("hours")
|
||||
if fmt == "m":
|
||||
return self._("minutes")
|
||||
if fmt == "s":
|
||||
return self._("seconds")
|
||||
else:
|
||||
# sing
|
||||
if fmt == "h":
|
||||
return self._("hour")
|
||||
if fmt == "m":
|
||||
return self._("minute")
|
||||
if fmt == "s":
|
||||
return self._("second")
|
||||
return fmt
|
Reference in New Issue
Block a user