Updating with master

This commit is contained in:
Michael V. Swisher
2019-10-09 21:55:56 -07:00
122 changed files with 2535 additions and 2740 deletions

62
pwnagotchi/__init__.py Normal file
View File

@ -0,0 +1,62 @@
import subprocess
import os
import logging
import time
import pwnagotchi.ui.view as view
version = '1.0.0RC2'
_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)
def shutdown():
logging.warning("shutting down ...")
if view.ROOT:
view.ROOT.on_shutdown()
# give it some time to refresh the ui
time.sleep(5)
os.system("sync")
os.system("halt")

558
pwnagotchi/agent.py Normal file
View File

@ -0,0 +1,558 @@
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.log import LastSession
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, keypair):
Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'],
config['bettercap']['port'],
config['bettercap']['username'],
config['bettercap']['password'])
AsyncAdvertiser.__init__(self, config, view, keypair)
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 = {}
self.last_session = LastSession(self._config)
if not os.path.exists(config['bettercap']['handshakes']):
os.makedirs(config['bettercap']['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 view(self):
return self._view
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()
tot = self._advertiser.num_peers()
self._view.set_closest_peer(peer, tot)
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
View 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
View 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

View 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
View 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)

View 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
View 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
View 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
View 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
View 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)

192
pwnagotchi/defaults.yml Normal file
View File

@ -0,0 +1,192 @@
# main algorithm configuration
main:
# currently implemented: en (default), de, el, fr, it, mk, nl, ru, se, pt-BR, es
lang: en
# custom plugins path, if null only default plugins with be loaded
custom_plugins:
# which plugins to load and enable
plugins:
grid:
enabled: true
report: false # don't report pwned networks by default!
exclude: # do not report the following networks (accepts both ESSIDs and BSSIDs)
- YourHomeNetworkHere
auto-update:
enabled: false
system: false # set to true to also enable system updates via apt
interval: 1 # every day
auto-backup:
enabled: false
interval: 1 # every day
files:
- /root/brain.nn
- /root/brain.json
- /root/handshakes/
- /etc/pwnagotchi/
- /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'
net-pos:
enabled: false
api_key: 'test'
gps:
enabled: false
speed: 19200
device: /dev/ttyUSB0
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: ~
wigle:
enabled: false
api_key: ~
screen_refresh:
enabled: false
refresh_interval: 50
# 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

47
pwnagotchi/identity.py Normal file
View File

@ -0,0 +1,47 @@
from Crypto.Signature import PKCS1_PSS
from Crypto.PublicKey import RSA
import Crypto.Hash.SHA256 as SHA256
import base64
import hashlib
import os
import logging
DefaultPath = "/etc/pwnagotchi/"
class KeyPair(object):
def __init__(self, path=DefaultPath):
self.path = path
self.priv_path = os.path.join(path, "id_rsa")
self.priv_key = None
self.pub_path = "%s.pub" % self.priv_path
self.pub_key = None
if not os.path.exists(self.path):
os.makedirs(self.path)
if not os.path.exists(self.priv_path) or not os.path.exists(self.pub_path):
logging.info("generating %s ..." % self.priv_path)
os.system("/usr/bin/ssh-keygen -t rsa -m PEM -b 4096 -N '' -f '%s'" % self.priv_path)
with open(self.priv_path) as fp:
self.priv_key = RSA.importKey(fp.read())
with open(self.pub_path) as fp:
self.pub_key = RSA.importKey(fp.read())
self.pub_key_pem = self.pub_key.exportKey('PEM').decode("ascii")
# python is special
if 'RSA PUBLIC KEY' not in self.pub_key_pem:
self.pub_key_pem = self.pub_key_pem.replace('PUBLIC KEY', 'RSA PUBLIC KEY')
pem = self.pub_key_pem.encode("ascii")
self.pub_key_pem_b64 = base64.b64encode(pem).decode("ascii")
self.fingerprint = hashlib.sha256(pem).hexdigest()
def sign(self, message):
hasher = SHA256.new(message.encode("ascii"))
signer = PKCS1_PSS.new(self.priv_key, saltLen=16)
signature = signer.sign(hasher)
signature_b64 = base64.b64encode(signature).decode("ascii")
return signature, signature_b64

Binary file not shown.

View File

@ -0,0 +1,214 @@
# 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-09 17:42+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 ""
msgid "Good night."
msgstr "Gute Nacht."
msgid "Zzz"
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 "Jo {what}!"
#, 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"

Binary file not shown.

View 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 ""

Binary file not shown.

View File

@ -0,0 +1,214 @@
# pwnagotchi voice data
# Copyright (C) 2019
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR diegopastor <dpastor29@alumnos.uaq.mx>, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-09 17:42+0200\n"
"PO-Revision-Date: 2019-10-09 21:07+0000\n"
"Last-Translator: diegopastor <dpastor29@alumnos.uaq.mx>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: spanish\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 "Hola, soy Pwnagotchi! Empezando ..."
msgid "New day, new hunt, new pwns!"
msgstr "Nuevo día, nueva cazería, nuevos pwns!"
msgid "Hack the Planet!"
msgstr "Hackea el planeta!"
msgid "AI ready."
msgstr "IA lista."
msgid "The neural network is ready."
msgstr "La red neuronal está lista."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Oye, el canal {channel} está libre! Tú AP lo agradecerá."
msgid "I'm bored ..."
msgstr "Estoy aburrido ..."
msgid "Let's go for a walk!"
msgstr "Vamos por un paseo!"
msgid "This is the best day of my life!"
msgstr "Este es el mejor día de mi vida!"
msgid "Shitty day :/"
msgstr "Día de mierda :/"
msgid "I'm extremely bored ..."
msgstr "Estoy extremadamente aburrido ..."
msgid "I'm very sad ..."
msgstr "Estoy muy triste ..."
msgid "I'm sad"
msgstr "Estoy triste."
msgid "I'm living the life!"
msgstr "Estoy viviendo la vida!"
msgid "I pwn therefore I am."
msgstr "Pwneo, por lo tanto, existo"
msgid "So many networks!!!"
msgstr "Cuantas redes!!!"
msgid "I'm having so much fun!"
msgstr "Me estoy divirtiendo mucho!"
msgid "My crime is that of curiosity ..."
msgstr "Mi único crimen es la curiosidad ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you. {name}"
msgstr "Hola {name}! encantado de conocerte."
#, python-brace-format
msgid "Unit {name} is nearby! {name}"
msgstr "La unidad {name} está cerca!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ... adiós {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} se fue ..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Uy ... {name} se fue"
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} perdido!"
msgid "Missed!"
msgstr "Perdido!"
msgid "Nobody wants to play with me ..."
msgstr "Nadie quiere jugar conmigo ..."
msgid "I feel so alone ..."
msgstr "Me siento tan solo ..."
msgid "Where's everybody?!"
msgstr "Dónde está todo el mundo?"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Tomándo una siesta por {secs}s ..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s)"
msgid "Good night."
msgstr "Buenas noches."
msgid "Zzz"
msgstr "Zzz"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Esperando {secs}s .."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Mirando al rededor ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Oye {what} seamos amigos!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Asociando a {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Ey {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Acabo de decidir que {mac} no necesita WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "Desautenticando a {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Expulsando y banneando a {mac}!"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Genial, obtuvimos {num} nuevo{plural} handshake{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Oops, algo salió mal ... Reiniciándo ..."
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Expulsamos {num} estaciones\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Hicimos {num} nuevos amigos\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Obtuvimos {num} handshakes\n"
msgid "Met 1 peer"
msgstr "Conocí 1 igual"
#, python-brace-format
msgid "Met {num} peers"
msgstr "Conocí {num} iguales"
#, 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 ""
"He estado pwneando por {duration} y expulsé {deauthed} clientes! También conocí"
"{associated} nuevos amigos y me comí {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "horas"
msgid "minutes"
msgstr "minutos"
msgid "seconds"
msgstr "segundos"
msgid "hour"
msgstr "hora"
msgid "minute"
msgstr "minuto"
msgid "second"
msgstr "segundo"

Binary file not shown.

View 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 "Nouveau jour, nouvelle chasse, nouveaux pwns !"
msgid "Hack the Planet!"
msgstr "Hack la planète!"
msgid "AI ready."
msgstr "L'IA est 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 point d'accès va te remercier."
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 "Tellement 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, c'est la curiosité ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you. {name}"
msgstr "Bonjour {name}! Ravi 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 "Je viens de décider 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 "Je kick et je bannis {mac}!"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Cool, on a {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 et dévoré {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 ""

Binary file not shown.

View 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"

Binary file not shown.

View 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 ""

Binary file not shown.

View 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 ""

Binary file not shown.

View File

@ -0,0 +1,209 @@
# pwnagotchi Brazilian Portuguese translation file.
# Copyright (C) 2019 Cassiano Aquino
# This file is distributed under the same license as the pwnagotchi package.
# Cassiano Aquino <cassianoaquino@me.com>, 2019.
#
#, 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: Cassiano Aquino <cassianoaquino@me.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: Brazilian Portuguese\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 "Oi! Eu sou o Pwnagotchi! Iniciando ..."
msgid "New day, new hunt, new pwns!"
msgstr "Novo dia, Nova caça, Novos pwns!"
msgid "Hack the Planet!"
msgstr "Hackeie o Planeta!"
msgid "AI ready."
msgstr "AI pronta."
msgid "The neural network is ready."
msgstr "A rede neural está pronta."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Ei, o canal {channel} está livre! Seu AP ira agradecer."
msgid "I'm bored ..."
msgstr "Estou entediado ..."
msgid "Let's go for a walk!"
msgstr "Vamos dar uma caminhada!"
msgid "This is the best day of my life!"
msgstr "Este e o melhor dia da minha vida!"
msgid "Shitty day :/"
msgstr "Dia de merda :/"
msgid "I'm extremely bored ..."
msgstr "Estou extremamente entediado ..."
msgid "I'm very sad ..."
msgstr "Estou muito triste ..."
msgid "I'm sad"
msgstr "Estou triste"
msgid "I'm living the life!"
msgstr "Estou aproveitando a vida!"
msgid "I pwn therefore I am."
msgstr "pwn, logo existo."
msgid "So many networks!!!"
msgstr "Quantas redes!!!"
msgid "I'm having so much fun!"
msgstr "Estou me divertindo muito!"
msgid "My crime is that of curiosity ..."
msgstr "Meu crime é ser curioso ..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you. {name}"
msgstr "Olá {name}! Prazer em conhecê-lo. {name}"
#, python-brace-format
msgid "Unit {name} is nearby! {name}"
msgstr "Unidade {name} está próxima! {name}"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ... até logo {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} desapareceu ..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Oops ... {name} desapareceu."
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} perdido!"
msgid "Missed!"
msgstr "Perdido!"
msgid "Nobody wants to play with me ..."
msgstr "Ninguém quer brincar comigo ..."
msgid "I feel so alone ..."
msgstr "Estou tão sozinho ..."
msgid "Where's everybody?!"
msgstr "Aonde está todo mundo?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Cochilando por {secs}s ..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s)"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Aguardando por {secs}s ..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Olhando ao redor ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Ei {what} vamos ser amigos!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Associando com {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Oi {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Acabei de decidir que {mac} não precisa de WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "De-autenticando {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Kickbanning {mac}"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Legal, nos capturamos {num} handshake{plural} novo{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Ops, algo falhou ... Reiniciando ..."
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Kickei {num} estações\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Fiz {num} novos amigos\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Peguei {num} handshakes\n"
msgid "Met 1 peer"
msgstr "Conheci 1 peer"
#, python-brace-format
msgid "Met {num} peers"
msgstr "Conheci {num} peers"
#, 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 ""
"Eu estou pwning fazem {duration} e kickei {deauthed} clientes! Eu também conheci "
"{associated} novos amigos e comi {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "horas"
msgid "minutes"
msgstr "minutos"
msgid "seconds"
msgstr "segundos"
msgid "hour"
msgstr "hora"
msgid "minute"
msgstr "minuto"
msgid "second"
msgstr "segundo"

Binary file not shown.

View 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 "минуту"

Binary file not shown.

View 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"

212
pwnagotchi/locale/voice.pot Normal file
View File

@ -0,0 +1,212 @@
# 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-09 17:42+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 ""
msgid "Good night."
msgstr ""
msgid "Zzz"
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 ""

185
pwnagotchi/log.py Normal file
View File

@ -0,0 +1,185 @@
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 LastSession(object):
EPOCH_TOKEN = '[epoch '
EPOCH_PARSER = re.compile(r'^.+\[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 __init__(self, config):
self.config = config
self.voice = Voice(lang=config['main']['lang'])
self.path = config['main']['log']
self.last_session = []
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.epochs = 0
self.train_epochs = 0
self.min_reward = 1000
self.max_reward = -1000
self.avg_reward = 0
self._peer_parser = re.compile(
'detected unit (.+)@(.+) \(v.+\) on channel \d+ \(([\d\-]+) dBm\) \[sid:(.+) pwnd_tot:(\d+) uptime:(\d+)\]')
self.parsed = False
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 LastSession.DEAUTH_TOKEN in line and line not in cache:
self.deauthed += 1
cache[line] = 1
elif LastSession.ASSOC_TOKEN in line and line not in cache:
self.associated += 1
cache[line] = 1
elif LastSession.HANDSHAKE_TOKEN in line and line not in cache:
self.handshakes += 1
cache[line] = 1
elif LastSession.TRAINING_TOKEN in line:
self.train_epochs += 1
elif LastSession.EPOCH_TOKEN in line:
self.epochs += 1
m = LastSession.EPOCH_PARSER.findall(line)
if m:
epoch_num, epoch_data = m[0]
m = LastSession.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 LastSession.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 parse(self):
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 LastSession.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()
self.parsed = True
def is_new(self):
return self.last_session_id != self.last_saved_session_id

View File

@ -0,0 +1,4 @@
import os
def new_session_id():
return ':'.join(['%02x' % b for b in os.urandom(6)])

View 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_Whisper:
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
View 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

47
pwnagotchi/mesh/utils.py Normal file
View File

@ -0,0 +1,47 @@
import _thread
import logging
import pwnagotchi
import pwnagotchi.plugins as plugins
class AsyncAdvertiser(object):
def __init__(self, config, view, keypair):
self._config = config
self._view = view
self._keypair = keypair
self._advertiser = None
def keypair(self):
return self._keypair
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._keypair.fingerprint,
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
View File

@ -0,0 +1,37 @@
SignatureAddress = 'de:ad:be:ef:de:ad'
BroadcastAddress = 'ff:ff:ff:ff:ff:ff'
Dot11ElemID_Whisper = 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_Whisper, info=chunk, len=sz)
data_off += sz
data_left -= sz
return frame

View File

@ -0,0 +1,64 @@
import os
import glob
import importlib, importlib.util
import logging
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_name, plugin in loaded.items():
if cb_name in plugin.__dict__:
# print("calling %s %s(%s)" %(cb_name, args, kwargs))
try:
plugin.__dict__[cb_name](*args, **kwargs)
except Exception as e:
logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e))
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')

View File

@ -0,0 +1,65 @@
__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(agent):
global STATUS
if READY:
if STATUS.newer_then_days(OPTIONS['interval']):
return
files_to_backup = " ".join(OPTIONS['files'])
try:
display = agent.view()
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()

View File

@ -0,0 +1,60 @@
__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 run(cmd):
return subprocess.Popen(cmd, shell=True, stdin=None, stdout=open("/dev/null", "w"), stderr=None,
executable="/bin/bash")
def on_internet_available(agent):
global STATUS
if READY:
if STATUS.newer_then_days(OPTIONS['interval']):
return
display = agent.view()
try:
display.set('status', 'Updating ...')
display.update()
logging.info("auto-update: updating pwnagotchi ...")
run('pip3 install --upgrade --upgrade-strategy only-if-needed pwnagotchi').wait()
if OPTIONS['system']:
logging.info("auto-update: updating packages index ...")
run('apt update -y').wait()
logging.info("auto-update: updating packages ...")
run('apt upgrade -y').wait()
logging.info("auto-update: complete.")
STATUS.update()
except Exception as e:
logging.exception("auto-update ERROR")
display.set('status', 'Updated!')
display.update()

View 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(agent):
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

View File

@ -0,0 +1,45 @@
__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
running = False
OPTIONS = dict()
def on_loaded():
logging.info("gps plugin loaded for %s" % OPTIONS['device'])
def on_ready(agent):
global running
if os.path.exists(OPTIONS['device']):
logging.info("enabling gps bettercap's module for %s" % OPTIONS['device'])
try:
agent.run('gps off')
except:
pass
agent.run('set gps.device %s' % OPTIONS['device'])
agent.run('set gps.speed %d' % OPTIONS['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)

View File

@ -0,0 +1,172 @@
__author__ = 'evilsocket@gmail.com'
__version__ = '1.0.0'
__name__ = 'grid'
__license__ = 'GPL3'
__description__ = 'This plugin signals the unit cryptographic identity and list of pwned networks and list of pwned networks to api.pwnagotchi.ai'
import os
import logging
import requests
import glob
import json
import subprocess
import pwnagotchi
import pwnagotchi.utils as utils
from pwnagotchi.utils import WifiInfo, extract_from_pcap
OPTIONS = dict()
AUTH = utils.StatusFile('/root/.api-enrollment.json', data_format='json')
REPORT = utils.StatusFile('/root/.api-report.json', data_format='json')
def on_loaded():
logging.info("grid plugin loaded.")
def get_api_token(last_session, keys):
global AUTH
if AUTH.newer_then_minutes(25) and AUTH.data is not None and 'token' in AUTH.data:
return AUTH.data['token']
if AUTH.data is None:
logging.info("grid: enrolling unit ...")
else:
logging.info("grid: refreshing token ...")
identity = "%s@%s" % (pwnagotchi.name(), keys.fingerprint)
# sign the identity string to prove we own both keys
_, signature_b64 = keys.sign(identity)
brain = {}
try:
with open('/root/brain.json') as fp:
brain = json.load(fp)
except:
pass
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll'
enrollment = {
'identity': identity,
'public_key': keys.pub_key_pem_b64,
'signature': signature_b64,
'data': {
'duration': last_session.duration,
'epochs': last_session.epochs,
'train_epochs': last_session.train_epochs,
'avg_reward': last_session.avg_reward,
'min_reward': last_session.min_reward,
'max_reward': last_session.max_reward,
'deauthed': last_session.deauthed,
'associated': last_session.associated,
'handshakes': last_session.handshakes,
'peers': last_session.peers,
'uname': subprocess.getoutput("uname -a"),
'brain': brain
}
}
r = requests.post(api_address, json=enrollment)
if r.status_code != 200:
raise Exception("(status %d) %s" % (r.status_code, r.json()))
AUTH.update(data=r.json())
logging.info("grid: done")
return AUTH.data["token"]
def parse_pcap(filename):
logging.info("grid: parsing %s ..." % filename)
net_id = os.path.basename(filename).replace('.pcap', '')
if '_' in net_id:
# /root/handshakes/ESSID_BSSID.pcap
essid, bssid = net_id.split('_')
else:
# /root/handshakes/BSSID.pcap
essid, bssid = '', net_id
it = iter(bssid)
bssid = ':'.join([a + b for a, b in zip(it, it)])
info = {
WifiInfo.ESSID: essid,
WifiInfo.BSSID: bssid,
}
try:
info = extract_from_pcap(filename, [WifiInfo.BSSID, WifiInfo.ESSID])
except Exception as e:
logging.error("grid: %s" % e)
return info[WifiInfo.ESSID], info[WifiInfo.BSSID]
def api_report_ap(last_session, keys, token, essid, bssid):
while True:
token = AUTH.data['token']
logging.info("grid: reporting %s (%s)" % (essid, bssid))
try:
api_address = 'https://api.pwnagotchi.ai/api/v1/unit/report/ap'
headers = {'Authorization': 'access_token %s' % token}
report = {
'essid': essid,
'bssid': bssid,
}
r = requests.post(api_address, headers=headers, json=report)
if r.status_code != 200:
if r.status_code == 401:
logging.warning("token expired")
token = get_api_token(last_session, keys)
continue
else:
raise Exception("(status %d) %s" % (r.status_code, r.text))
else:
return True
except Exception as e:
logging.error("grid: %s" % e)
return False
def on_internet_available(agent):
global REPORT
try:
config = agent.config()
keys = agent.keypair()
pcap_files = glob.glob(os.path.join(config['bettercap']['handshakes'], "*.pcap"))
num_networks = len(pcap_files)
reported = REPORT.data_field_or('reported', default=[])
num_reported = len(reported)
num_new = num_networks - num_reported
token = get_api_token(agent.last_session, agent.keypair())
if num_new > 0:
if OPTIONS['report']:
logging.info("grid: %d new networks to report" % num_new)
for pcap_file in pcap_files:
net_id = os.path.basename(pcap_file).replace('.pcap', '')
do_skip = False
for skip in OPTIONS['exclude']:
skip = skip.lower()
net = net_id.lower()
if skip in net or skip.replace(':', '') in net:
do_skip = True
break
if net_id not in reported and not do_skip:
essid, bssid = parse_pcap(pcap_file)
if bssid:
if api_report_ap(agent.last_session, keys, token, essid, bssid):
reported.append(net_id)
REPORT.update(data={'reported': reported})
else:
logging.debug("grid: reporting disabled")
except Exception as e:
logging.exception("error while enrolling the unit")

View 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()

View File

@ -0,0 +1,144 @@
__author__ = 'zenzen san'
__version__ = '1.0.0'
__name__ = 'net-pos'
__license__ = 'GPL3'
__description__ = """Saves a json file with the access points with more signal
whenever a handshake is captured.
When internet is available the files are converted in geo locations
using Mozilla LocationService """
import logging
import json
import os
import requests
MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
ALREADY_SAVED = None
SKIP = None
READY = False
OPTIONS = {}
def on_loaded():
global ALREADY_SAVED
global SKIP
global READY
SKIP = list()
if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
logging.error("NET-POS: api_key isn't set. Can't use mozilla's api.")
return
try:
with open('/root/.net_pos_saved', 'r') as f:
ALREADY_SAVED = f.read().splitlines()
except OSError:
logging.warning('NET-POS: No net-pos-file found.')
ALREADY_SAVED = []
READY = True
logging.info("net-pos plugin loaded.")
def _append_saved(path):
to_save = list()
if isinstance(path, str):
to_save.append(path)
elif isinstance(path, list):
to_save += path
else:
raise TypeError("Expected list or str, got %s" % type(path))
with open('/root/.net_pos_saved', 'a') as saved_file:
for x in to_save:
saved_file.write(x + "\n")
def on_internet_available(agent):
global SKIP
if READY:
config = agent.config()
display = agent.view()
handshake_dir = config['bettercap']['handshakes']
all_files = os.listdir(handshake_dir)
all_np_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.net-pos.json')]
new_np_files = set(all_np_files) - set(ALREADY_SAVED) - set(SKIP)
if new_np_files:
logging.info("NET-POS: Found {num} new net-pos files. Fetching positions ...", len(new_np_files))
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
display.update(force=True)
for idx, np_file in enumerate(new_np_files):
geo_file = np_file.replace('.net-pos.json', '.geo.json')
if os.path.exists(geo_file):
# got already the position
ALREADY_SAVED.append(np_file)
_append_saved(np_file)
continue
try:
geo_data = _get_geo_data(np_file) # returns json obj
except requests.exceptions.RequestException as req_e:
logging.error("NET-POS: %s", req_e)
SKIP += np_file
except json.JSONDecodeError as js_e:
logging.error("NET-POS: %s", js_e)
SKIP += np_file
except OSError as os_e:
logging.error("NET-POS: %s", os_e)
SKIP += np_file
with open(geo_file, 'w+t') as sf:
json.dump(geo_data, sf)
ALREADY_SAVED.append(np_file)
_append_saved(np_file)
display.set('status', f"Fetching positions ({idx+1}/{len(new_np_files)})")
display.update(force=True)
def on_handshake(agent, filename, access_point, client_station):
netpos = _get_netpos(agent)
netpos_filename = filename.replace('.pcap', '.net-pos.json')
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
try:
with open(netpos_filename, 'w+t') as fp:
json.dump(netpos, fp)
except OSError as os_e:
logging.error("NET-POS: %s", os_e)
def _get_netpos(agent):
aps = agent.get_access_points()
netpos = {}
netpos['wifiAccessPoints'] = list()
# 6 seems a good number to save a wifi networks location
for access_point in sorted(aps, key=lambda i: i['rssi'], reverse=True)[:6]:
netpos['wifiAccessPoints'].append({'macAddress': access_point['mac'],
'signalStrength': access_point['rssi']})
return netpos
def _get_geo_data(path, timeout=30):
geourl = MOZILLA_API_URL.format(api=OPTIONS['api_key'])
try:
with open(path, "r") as json_file:
data = json.load(json_file)
except json.JSONDecodeError as js_e:
raise js_e
except OSError as os_e:
raise os_e
try:
result = requests.post(geourl,
json=data,
timeout=timeout)
return result.json()
except requests.exceptions.RequestException as req_e:
raise req_e

View File

@ -0,0 +1,87 @@
__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(agent):
"""
Called in manual mode when there's internet connectivity
"""
if READY:
display = agent.view()
config = agent.config()
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap')]
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}")

View File

@ -0,0 +1,24 @@
__author__ = 'pwnagotcchi [at] rossmarks [dot] uk'
__version__ = '1.0.0'
__name__ = 'screen_refresh'
__license__ = 'GPL3'
__description__ = 'Refresh he e-ink display after X amount of updates'
import logging
OPTIONS = dict()
update_count = 0;
def on_loaded():
logging.info("Screen refresh plugin loaded")
def on_ui_update(ui):
global update_count
update_count += 1
if update_count == OPTIONS['refresh_interval']:
ui._init_display()
ui.set('status', "Screen cleaned")
logging.info("Screen refreshing")
update_count = 0

View File

@ -0,0 +1,50 @@
__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(agent):
config = agent.config()
display = agent.view()
last_session = agent.last_session
if last_session.is_new() and last_session.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'
display.on_manual_mode(last_session)
display.update(force=True)
display.image().save(picture, 'png')
display.set('status', 'Tweeting...')
display.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_last_session_tweet(last_session)
api.update_with_media(filename=picture, status=tweet)
last_session.save_session_id()
logging.info("tweeted: %s" % tweet)
except Exception as e:
logging.exception("error while tweeting")

View 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()))

View File

@ -0,0 +1,284 @@
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '1.0.0'
__name__ = 'wigle'
__license__ = 'GPL3'
__description__ = 'This plugin automatically uploades collected wifis to wigle.net'
import os
import logging
import json
from io import StringIO
import csv
from datetime import datetime
import requests
from pwnagotchi.mesh.wifi import freq_to_channel
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap
READY = False
ALREADY_UPLOADED = None
SKIP = None
OPTIONS = dict()
AKMSUITE_TYPES = {
0x00: "Reserved",
0x01: "802.1X",
0x02: "PSK",
}
def _handle_packet(packet, result):
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
"""
Analyze each packet and extract the data from Dot11 layers
"""
if hasattr(packet, 'cap') and 'privacy' in packet.cap:
# packet is encrypted
if 'encryption' not in result:
result['encryption'] = set()
if packet.haslayer(Dot11Beacon):
if packet.haslayer(Dot11Beacon)\
or packet.haslayer(Dot11ProbeResp)\
or packet.haslayer(Dot11AssoReq)\
or packet.haslayer(Dot11ReassoReq):
if 'bssid' not in result and hasattr(packet[Dot11], 'addr3'):
result['bssid'] = packet[Dot11].addr3
if 'essid' not in result and hasattr(packet[Dot11Elt], 'info'):
result['essid'] = packet[Dot11Elt].info
if 'channel' not in result and hasattr(packet[Dot11Elt:3], 'info'):
result['channel'] = int(ord(packet[Dot11Elt:3].info))
if packet.haslayer(RadioTap):
if 'rssi' not in result and hasattr(packet[RadioTap], 'dBm_AntSignal'):
result['rssi'] = packet[RadioTap].dBm_AntSignal
if 'channel' not in result and hasattr(packet[RadioTap], 'ChannelFrequency'):
result['channel'] = freq_to_channel(packet[RadioTap].ChannelFrequency)
# see: https://fossies.org/linux/scapy/scapy/layers/dot11.py
if packet.haslayer(Dot11EltRSN):
if hasattr(packet[Dot11EltRSN], 'akm_suites'):
auth = AKMSUITE_TYPES.get(packet[Dot11EltRSN].akm_suites[0].suite)
result['encryption'].add(f"WPA2/{auth}")
else:
result['encryption'].add("WPA2")
if packet.haslayer(Dot11EltVendorSpecific)\
and (packet.haslayer(Dot11EltMicrosoftWPA)
or packet.info.startswith(b'\x00P\xf2\x01\x01\x00')):
if hasattr(packet, 'akm_suites'):
auth = AKMSUITE_TYPES.get(packet.akm_suites[0].suite)
result['encryption'].add(f"WPA2/{auth}")
else:
result['encryption'].add("WPA2")
# end see
return result
def _analyze_pcap(pcap):
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
"""
Iterate over the packets and extract data
"""
result = dict()
try:
packets = rdpcap(pcap)
for packet in packets:
result = _handle_packet(packet, result)
except Scapy_Exception as sc_e:
raise sc_e
return result
def on_loaded():
"""
Gets called when the plugin gets loaded
"""
global READY
global ALREADY_UPLOADED
global SKIP
SKIP = list()
if 'api_key' not in OPTIONS or ('api_key' in OPTIONS and OPTIONS['api_key'] is None):
logging.error("WIGLE: api_key isn't set. Can't upload to wigle.net")
return
try:
with open('/root/.wigle_uploads', 'r') as f:
ALREADY_UPLOADED = f.read().splitlines()
except OSError:
logging.warning('WIGLE: No upload-file found.')
ALREADY_UPLOADED = []
READY = True
def _extract_gps_data(path):
"""
Extract data from gps-file
return json-obj
"""
try:
with open(path, 'r') as json_file:
return json.load(json_file)
except OSError as os_err:
raise os_err
except json.JSONDecodeError as json_err:
raise json_err
def _format_auth(data):
out = ""
for auth in data:
out = f"{out}[{auth}]"
return out
def _transform_wigle_entry(gps_data, pcap_data):
"""
Transform to wigle entry in file
"""
dummy = StringIO()
# write kismet header
dummy.write("WigleWifi-1.4,appRelease=20190201,model=Kismet,release=2019.02.01.{},device=kismet,display=kismet,board=kismet,brand=kismet\n")
dummy.write("MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type")
writer = csv.writer(dummy, delimiter=",", quoting=csv.QUOTE_NONE)
writer.writerow([
pcap_data[WifiInfo.BSSID],
pcap_data[WifiInfo.ESSID],
_format_auth(pcap_data[WifiInfo.ENCRYPTION]),
datetime.strptime(gps_data['Updated'].rsplit('.')[0],
"%Y-%m-%dT%H:%M:%S").strftime('%Y-%m-%d %H:%M:%S'),
pcap_data[WifiInfo.CHANNEL],
pcap_data[WifiInfo.RSSI],
gps_data['Latitude'],
gps_data['Longitude'],
gps_data['Altitude'],
0, # accuracy?
'WIFI'])
return dummy.getvalue()
def _send_to_wigle(lines, api_key, timeout=30):
"""
Uploads the file to wigle-net
"""
dummy = StringIO()
for line in lines:
dummy.write(f"{line}")
dummy.seek(0)
headers = {'Authorization': f"Basic {api_key}",
'Accept': 'application/json'}
data = {'donate': 'false'}
payload = {'file': dummy, 'type': 'text/csv'}
try:
res = requests.post('https://api.wigle.net/api/v2/file/upload',
data=data,
headers=headers,
files=payload,
timeout=timeout)
json_res = res.json()
if not json_res['success']:
raise requests.exceptions.RequestException(json_res['message'])
except requests.exceptions.RequestException as re_e:
raise re_e
def on_internet_available(agent):
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
"""
Called in manual mode when there's internet connectivity
"""
global ALREADY_UPLOADED
global SKIP
if READY:
config = agent.config()
display = agent.view()
handshake_dir = config['bettercap']['handshakes']
all_files = os.listdir(handshake_dir)
all_gps_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.gps.json')]
new_gps_files = set(all_gps_files) - set(ALREADY_UPLOADED) - set(SKIP)
if new_gps_files:
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
csv_entries = list()
no_err_entries = list()
for gps_file in new_gps_files:
pcap_filename = gps_file.replace('.gps.json', '.pcap')
if not os.path.exists(pcap_filename):
logging.error("WIGLE: Can't find pcap for %s", gps_file)
SKIP.append(gps_file)
continue
try:
gps_data = _extract_gps_data(gps_file)
except OSError as os_err:
logging.error("WIGLE: %s", os_err)
SKIP.append(gps_file)
continue
except json.JSONDecodeError as json_err:
logging.error("WIGLE: %s", json_err)
SKIP.append(gps_file)
continue
if gps_data['Latitude'] == 0 and gps_data['Longitude'] == 0:
logging.warning("WIGLE: Not enough gps-informations for %s. Trying again next time.", gps_file)
SKIP.append(gps_file)
continue
try:
pcap_data = extract_from_pcap(pcap_filename, [WifiInfo.BSSID,
WifiInfo.ESSID,
WifiInfo.ENCRYPTION,
WifiInfo.CHANNEL,
WifiInfo.RSSI])
except FieldNotFoundError:
logging.error("WIGLE: Could not extract all informations. Skip %s", gps_file)
SKIP.append(gps_file)
continue
except Scapy_Exception as sc_e:
logging.error("WIGLE: %s", sc_e)
SKIP.append(gps_file)
continue
new_entry = _transform_wigle_entry(gps_data, pcap_data)
csv_entries.append(new_entry)
no_err_entries.append(gps_file)
if csv_entries:
display.set('status', "Uploading gps-data to wigle.net ...")
display.update(force=True)
try:
_send_to_wigle(csv_entries, OPTIONS['api_key'])
ALREADY_UPLOADED += no_err_entries
with open('/root/.wigle_uploads', 'a') as up_file:
for gps in no_err_entries:
up_file.write(gps + "\n")
logging.info("WIGLE: Successfuly uploaded %d files", len(no_err_entries))
except requests.exceptions.RequestException as re_e:
SKIP += no_err_entries
logging.error("WIGLE: Got an exception while uploading %s", re_e)
except OSError as os_e:
SKIP += no_err_entries
logging.error("WIGLE: Got the following error: %s", os_e)

View File

@ -0,0 +1,86 @@
__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(agent):
"""
Called in manual mode when there's internet connectivity
"""
if READY:
config = agent.config()
display = agent.view()
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if filename.endswith('.pcap')]
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}")

View File

@ -0,0 +1 @@

View 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)

261
pwnagotchi/ui/display.py Normal file
View File

@ -0,0 +1,261 @@
import _thread
from threading import Lock
from PIL import Image
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>
<style>
.block {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
display: block;
cursor: pointer;
text-align: center;
}
</style>
</head>
<body>
<div style="position: absolute; top:0; left:0; width:100%%;">
<img src="/ui" id="ui" style="width:100%%"/>
<br/>
<hr/>
<form action="/shutdown" onsubmit="return confirm('This will halt the unit, continue?');">
<input type="submit" class="block" value="Shutdown"/>
</form>
</div>
<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('/shutdown'):
pwnagotchi.shutdown()
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")
if self._display_color == 'black':
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
else:
from pwnagotchi.ui.waveshare.v1.epd2in13bc import EPD
self._display = EPD()
self._display.init()
self._display.Clear()
self._render_cb = self._waveshare_bc_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 _waveshare_bc_render(self):
buf_black = self._display.getbuffer(self.canvas)
# emptyImage = Image.new('1', (self._display.height, self._display.width), 255)
# buf_color = self._display.getbuffer(emptyImage)
# self._display.display(buf_black,buf_color)
# Custom display function that only handles black
# Was included in epd2in13bc.py
self._display.displayBlack(buf_black)
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
View 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
View 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)

View File

View 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))

View 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
View 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)

362
pwnagotchi/ui/view.py Normal file
View File

@ -0,0 +1,362 @@
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
ROOT = None
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 = (5, int(height * .15))
status_pos = (int(width / 2) - 15, int(height * .15))
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 = (5, int(height * .15))
status_pos = (int(width / 2) - 15, int(height * .15))
elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1',
'ws_2', 'ws2', 'waveshare_2', 'waveshare2'):
if config['ui']['display']['color'] == 'black':
fonts.setup(10, 9, 10, 35)
width = 250
height = 122
face_pos = (0, 40)
name_pos = (5, 20)
status_pos = (125, 20)
else:
fonts.setup(10, 8, 10, 25)
width = 212
height = 104
face_pos = (0, int(height / 4))
name_pos = (5, int(height * .15))
status_pos = (int(width / 2) - 15, int(height * .15))
return width, height, face_pos, name_pos, status_pos
class View(object):
def __init__(self, config, state={}):
global ROOT
self._render_cbs = []
self._config = config
self._canvas = None
self._frozen = False
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')
ROOT = self
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', ' AI')
self.set('face', faces.HAPPY)
self.set('status', self._voice.on_ai_ready())
self.update()
def on_manual_mode(self, last_session):
self.set('mode', 'MANU')
self.set('face', faces.SAD if last_session.handshakes == 0 else faces.HAPPY)
self.set('status', self._voice.on_last_session_data(last_session))
self.set('epoch', "%04d" % last_session.epochs)
self.set('uptime', last_session.duration)
self.set('channel', '-')
self.set('aps', "%d" % last_session.associated)
self.set('shakes', '%d (%s)' % (last_session.handshakes, \
utils.total_unique_handshakes(self._config['bettercap']['handshakes'])))
self.set_closest_peer(last_session.last_peer, last_session.peers)
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, num_total):
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())
if num_total > 1:
if num_total > 9000:
name += ' of over 9000'
else:
name += ' of %d' % num_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_shutdown(self):
self.set('face', faces.SLEEP)
self.set('status', self._voice.on_shutdown())
self.update(force=True)
self._frozen = True
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:
if self._frozen:
return
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()

View File

View File

View 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 ###

View File

@ -0,0 +1,165 @@
# *****************************************************************************
# * | File : epd2in13bc.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
from PIL import Image
import RPi.GPIO as GPIO
# import numpy as np
# Display resolution
EPD_WIDTH = 104
EPD_HEIGHT = 212
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
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, GPIO.HIGH)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, GPIO.LOW) # module reset
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, GPIO.HIGH)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, GPIO.LOW)
epdconfig.digital_write(self.cs_pin, GPIO.LOW)
epdconfig.spi_writebyte([command])
epdconfig.digital_write(self.cs_pin, GPIO.HIGH)
def send_data(self, data):
epdconfig.digital_write(self.dc_pin, GPIO.HIGH)
epdconfig.digital_write(self.cs_pin, GPIO.LOW)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, GPIO.HIGH)
def ReadBusy(self):
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
def init(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data(0x17)
self.send_data(0x17)
self.send_data(0x17)
self.send_command(0x04) # POWER_ON
self.ReadBusy()
self.send_command(0x00) # PANEL_SETTING
self.send_data(0x8F)
self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING
self.send_data(0xF0)
self.send_command(0x61) # RESOLUTION_SETTING
self.send_data(self.width & 0xff)
self.send_data(self.height >> 8)
self.send_data(self.height & 0xff)
return 0
def getbuffer(self, image):
buf = [0xFF] * (int(self.width/8) * 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):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(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:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def displayBlack(self, imageblack):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imageblack[i])
self.send_command(0x92)
self.send_command(0x12) # REFRESH
self.ReadBusy()
def display(self, imageblack, imagecolor):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imageblack[i])
self.send_command(0x92)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imagecolor[i])
self.send_command(0x92)
self.send_command(0x12) # REFRESH
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x92)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x92)
self.send_command(0x12) # REFRESH
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0xA5) # check code
# epdconfig.module_exit()
### END OF FILE ###

View 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 ###

View File

View 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 ###

246
pwnagotchi/utils.py Normal file
View File

@ -0,0 +1,246 @@
from datetime import datetime
from enum import Enum
import logging
import glob
import os
import time
import subprocess
import yaml
import json
import shutil
import pwnagotchi
# 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):
default_config_path = os.path.dirname(args.config)
if not os.path.exists(default_config_path):
os.makedirs(default_config_path)
ref_defaults_file = os.path.join(os.path.dirname(pwnagotchi.__file__), 'defaults.yml')
ref_defaults_data = None
if not os.path.exists(args.config):
# logging not configured here yet
print("copying %s to %s ..." % (ref_defaults_file, args.config))
shutil.copy(ref_defaults_file, args.config)
else:
with open(ref_defaults_file) as fp:
ref_defaults_data = fp.read()
with open(args.config) as fp:
defaults_data = fp.read()
if ref_defaults_data != defaults_data:
print("!!! file in %s is different than release defaults, overwriting !!!" % args.config)
shutil.copy(ref_defaults_file, args.config)
with open(args.config) as fp:
config = yaml.safe_load(fp)
if os.path.exists(args.user_config):
with open(args.user_config) as fp:
user_config = yaml.safe_load(fp)
# if the file is empty, safe_load will return None and merge_config will boom.
if user_config:
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 WifiInfo(Enum):
"""
Fields you can extract from a pcap file
"""
BSSID = 0
ESSID = 1
ENCRYPTION = 2
CHANNEL = 3
RSSI = 4
class FieldNotFoundError(Exception):
pass
def extract_from_pcap(path, fields):
"""
Search in pcap-file for specified information
path: Path to pcap file
fields: Array of fields that should be extracted
If a field is not found, FieldNotFoundError is raised
"""
results = dict()
for field in fields:
if not isinstance(field, WifiInfo):
raise TypeError("Invalid field")
subtypes = set()
if field == WifiInfo.BSSID:
from scapy.all import Dot11Beacon, Dot11ProbeResp, Dot11AssoReq, Dot11ReassoReq, Dot11, sniff
subtypes.add('beacon')
bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
packets = sniff(offline=path, filter=bpf_filter)
try:
for packet in packets:
if packet.haslayer(Dot11Beacon):
if hasattr(packet[Dot11], 'addr3'):
results[field] = packet[Dot11].addr3
break
else: # magic
raise FieldNotFoundError("Could not find field [BSSID]")
except Exception:
raise FieldNotFoundError("Could not find field [BSSID]")
elif field == WifiInfo.ESSID:
from scapy.all import Dot11Beacon, Dot11ReassoReq, Dot11AssoReq, Dot11, sniff, Dot11Elt
subtypes.add('beacon')
subtypes.add('assoc-req')
subtypes.add('reassoc-req')
bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
packets = sniff(offline=path, filter=bpf_filter)
try:
for packet in packets:
if packet.haslayer(Dot11Elt) and hasattr(packet[Dot11Elt], 'info'):
results[field] = packet[Dot11Elt].info.decode('utf-8')
break
else: # magic
raise FieldNotFoundError("Could not find field [ESSID]")
except Exception:
raise FieldNotFoundError("Could not find field [ESSID]")
elif field == WifiInfo.ENCRYPTION:
from scapy.all import Dot11Beacon, sniff
subtypes.add('beacon')
bpf_filter = " or ".join([f"wlan type mgt subtype {subtype}" for subtype in subtypes])
packets = sniff(offline=path, filter=bpf_filter)
try:
for packet in packets:
if packet.haslayer(Dot11Beacon) and hasattr(packet[Dot11Beacon], 'network_stats'):
stats = packet[Dot11Beacon].network_stats()
if 'crypto' in stats:
results[field] = stats['crypto'] # set with encryption types
break
else: # magic
raise FieldNotFoundError("Could not find field [ENCRYPTION]")
except Exception:
raise FieldNotFoundError("Could not find field [ENCRYPTION]")
elif field == WifiInfo.CHANNEL:
from scapy.all import sniff, RadioTap
from pwnagotchi.mesh.wifi import freq_to_channel
packets = sniff(offline=path, count=1)
try:
results[field] = freq_to_channel(packets[0][RadioTap].ChannelFrequency)
except Exception:
raise FieldNotFoundError("Could not find field [CHANNEL]")
elif field == WifiInfo.RSSI:
from scapy.all import sniff, RadioTap
from pwnagotchi.mesh.wifi import freq_to_channel
packets = sniff(offline=path, count=1)
try:
results[field] = packets[0][RadioTap].dBm_AntSignal
except Exception:
raise FieldNotFoundError("Could not find field [RSSI]")
return results
class StatusFile(object):
def __init__(self, path, data_format='raw'):
self._path = path
self._updated = None
self._format = data_format
self.data = None
if os.path.exists(path):
self._updated = datetime.fromtimestamp(os.path.getmtime(path))
with open(path) as fp:
if data_format == 'json':
self.data = json.load(fp)
else:
self.data = fp.read()
def data_field_or(self, name, default=""):
if self.data is not None and name in self.data:
return self.data[name]
return default
def newer_then_minutes(self, minutes):
return self._updated is not None and ((datetime.now() - self._updated).seconds / 60) < minutes
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()
self.data = data
with open(self._path, 'w') as fp:
if data is None:
fp.write(str(self._updated))
elif self._format == 'json':
json.dump(self.data, fp)
else:
fp.write(data)

163
pwnagotchi/voice.py Normal file
View File

@ -0,0 +1,163 @@
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_shutdown(self):
return random.choice([
self._('Good night.'),
self._('Zzz')])
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_last_session_data(self, last_session):
status = self._('Kicked {num} stations\n').format(num=last_session.deauthed)
status += self._('Made {num} new friends\n').format(num=last_session.associated)
status += self._('Got {num} handshakes\n').format(num=last_session.handshakes)
if last_session.peers == 1:
status += self._('Met 1 peer')
elif last_session.peers > 0:
status += self._('Met {num} peers').format(num=last_session.peers)
return status
def on_last_session_tweet(self, last_session):
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=last_session.duration_human,
deauthed=last_session.deauthed,
associated=last_session.associated,
handshakes=last_session.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