Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
95b871aade | ||
|
7b05f10c6f | ||
|
6b1bca7cb2 | ||
|
79688305fd | ||
|
13b1fb6d14 | ||
|
8a1aad1a99 | ||
|
aeb6536959 | ||
|
970b6922b7 | ||
|
18dd71b989 | ||
|
256ccab05c | ||
|
0aa80d2307 | ||
|
5987f93009 | ||
|
ebeb22081b | ||
|
f97b106858 | ||
|
c449c77ef9 | ||
|
a05ea2f48a | ||
|
beb2fedf02 | ||
|
1936c309f0 | ||
|
ee3fb285be | ||
|
aa60a369a9 | ||
|
0e9f9c0f2e | ||
|
6645c80db3 | ||
|
13d68c7c24 | ||
|
df33d20cb2 | ||
|
f3eb208c6a | ||
|
ae5ca2a05e | ||
|
a9b9c6677e | ||
|
f85d80d3fd | ||
|
4be54cf3ee | ||
|
c8953d4654 | ||
|
26ff5c95f2 | ||
|
2f0f0edab0 | ||
|
b81c80cf99 | ||
|
baf20a9ac8 | ||
|
280ca22261 | ||
|
277dbd5a16 | ||
|
5b29f65042 | ||
|
9f66d7ab96 | ||
|
9625cf1122 | ||
|
6b42e48dff | ||
|
d814de75ab | ||
|
35b442f941 | ||
|
0f2ad47c17 | ||
|
748dbea13e | ||
|
b66f1b66e5 | ||
|
1642663c84 | ||
|
54a8fd81a5 | ||
|
2fe7ac0a71 | ||
|
4b563398f4 | ||
|
84be7c0d34 | ||
|
82bf9b9853 | ||
|
d9d38e7a1e | ||
|
5b78a13e95 | ||
|
a8a0f842a3 | ||
|
f8a28d375b | ||
|
cfa8a02abc | ||
|
e32be6ff27 | ||
|
ab74395602 | ||
|
b7a806c8ad | ||
|
e146a87b44 | ||
|
dfb4bcaf21 | ||
|
9aca3a3a5b | ||
|
d15f8c18b5 | ||
|
87a3fb5f0c | ||
|
8b366ca736 | ||
|
2bbcc36f2a | ||
|
308746a7de | ||
|
d648f7cdf5 | ||
|
5a53670133 | ||
|
e15d0f3323 | ||
|
1ce361a839 | ||
|
e9899dda94 | ||
|
2430b4a134 | ||
|
b539a76124 | ||
|
ba7c0ee4e6 | ||
|
f0c5ad4b74 | ||
|
8d2cbee8df | ||
|
6793312691 | ||
|
5989d2571c | ||
|
ad2fbdb9dd | ||
|
1215fda459 | ||
|
1808392a1d | ||
|
77efeafd65 | ||
|
ac5ee1ba7b | ||
|
ef31366078 | ||
|
5d28557608 | ||
|
3a14d1d87f | ||
|
1691896531 | ||
|
e08b633c88 | ||
|
77c16c38f4 | ||
|
3773f96901 |
@@ -3,7 +3,6 @@ maintainers:
|
||||
- caquino
|
||||
- dadav
|
||||
- justin-p
|
||||
- hexwaxwing
|
||||
|
||||
features:
|
||||
- comments
|
||||
|
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
||||
PWN_HOSTNAME=pwnagotchi
|
||||
PWN_VERSION=master
|
||||
|
||||
all: install image clean
|
||||
all: clean install image
|
||||
|
||||
install:
|
||||
curl https://releases.hashicorp.com/packer/1.3.5/packer_1.3.5_linux_amd64.zip -o /tmp/packer.zip
|
||||
|
18
README.md
18
README.md
@@ -11,30 +11,20 @@
|
||||
</p>
|
||||
</p>
|
||||
|
||||
[Pwnagotchi](https://twitter.com/pwnagotchi) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment in order to maximize the crackable WPA key material it captures (either passively, or by performing deauthentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/),
|
||||
[Pwnagotchi](https://pwnagotchi.ai/) is an [A2C](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752)-based "AI" leveraging [bettercap](https://www.bettercap.org/) that learns from its surrounding WiFi environment to maximize the crackable WPA key material it captures (either passively, or by performing authentication and association attacks). This material is collected as PCAP files containing any form of handshake supported by [hashcat](https://hashcat.net/hashcat/), including [PMKIDs](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/),
|
||||
full and half WPA handshakes.
|
||||
|
||||

|
||||
|
||||
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning based "AI" *(yawn)*, Pwnagotchi tunes [its own parameters](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/defaults.yml#L73) over time to **get better at pwning WiFi things** in the environments you expose it to.
|
||||
Instead of merely playing [Super Mario or Atari games](https://becominghuman.ai/getting-mario-back-into-the-gym-setting-up-super-mario-bros-in-openais-gym-8e39a96c1e41?gi=c4b66c3d5ced) like most reinforcement learning-based "AI" *(yawn)*, Pwnagotchi tunes [its parameters](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/defaults.yml#L73) over time to **get better at pwning WiFi things to** in the environments you expose it to.
|
||||
|
||||
More specifically, Pwnagotchi is using an [LSTM with MLP feature extractor](https://stable-baselines.readthedocs.io/en/master/modules/policies.html#stable_baselines.common.policies.MlpLstmPolicy) as its policy network for the [A2C agent](https://stable-baselines.readthedocs.io/en/master/modules/a2c.html). If you're unfamiliar with A2C, here is [a very good introductory explanation](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) (in comic form!) of the basic principles behind how Pwnagotchi learns. (You can read more about how Pwnagotchi learns in the [Usage](https://www.pwnagotchi.ai/usage/#training-the-ai) doc.)
|
||||
|
||||
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi actually learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://www.pwnagotchi.ai/usage/#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but **definitely listen to your Pwnagotchi when it tells you it's bored!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
|
||||
**Keep in mind:** Unlike the usual RL simulations, Pwnagotchi learns over time. Time for a Pwnagotchi is measured in epochs; a single epoch can last from a few seconds to minutes, depending on how many access points and client stations are visible. Do not expect your Pwnagotchi to perform amazingly well at the very beginning, as it will be [exploring](https://hackernoon.com/intuitive-rl-intro-to-advantage-actor-critic-a2c-4ff545978752) several combinations of [key parameters](https://www.pwnagotchi.ai/usage/#training-the-ai) to determine ideal adjustments for pwning the particular environment you are exposing it to during its beginning epochs ... but ** listen to your Pwnagotchi when it tells you it's boring!** Bring it into novel WiFi environments with you and have it observe new networks and capture new handshakes—and you'll see. :)
|
||||
|
||||
Multiple units within close physical proximity can "talk" to each other, advertising their own presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
|
||||
|
||||
## Why does Pwnagotchi exist?
|
||||
|
||||
For hackers to learn reinforcement learning, WiFi networking, and have an excuse to get out for more walks. Also? **It's cute as f---**.
|
||||
|
||||
**In case you're curious about the name:** *Pwnagotchi* is a portmanteau of *pwn* (which we shouldn't have to explain if you are interested in this project :kissing_heart:) and *-gotchi*. It is a nostalgic reference made in homage to a very popular children's toy from the 1990s called the [Tamagotchi](https://en.wikipedia.org/wiki/Tamagotchi). The Tamagotchi (たまごっち, derived from *tamago* (たまご) "egg" + *uotchi* (ウオッチ) "watch") is a cultural touchstone for many Millennial hackers as a formative electronic toy from our collective childhoods. Were you lucky enough to possess a Tamagotchi as a kid? Well, with your Pwnagotchi, you too can enjoy the nostalgic delight of being strangely emotionally attached to a handheld automata *yet again!* Except, this time around...you get to #HackThePlanet. >:D
|
||||
Multiple units within close physical proximity can "talk" to each other, advertising their presence to each other by broadcasting custom information elements using a parasite protocol I've built on top of the existing dot11 standard. Over time, two or more units trained together will learn to cooperate upon detecting each other's presence by dividing the available channels among them for optimal pwnage.
|
||||
|
||||
## Documentation
|
||||
---
|
||||
:warning: **THE FOLLOWING DOCUMENTATION IS BEING PREPARED FOR THE v1.0 RELEASE OF PWNAGOTCHI. Since this effort is an active (and unstable) work-in-progress, the docs displayed here are in various stages of [in]completion. There will be dead links and placeholders throughout as we are still building things out in preparation for the v1.0 release.** :warning:
|
||||
|
||||
**IMPORTANT NOTE:** If you'd like to alphatest Pwnagotchi and are trying to get yours up and running while the project is still very unstable, please understand that the documentation here may not reflect what is currently implemented. If you have questions, ask the community of alphatesters in the [official Pwnagotchi Slack](https://pwnagotchi.herokuapp.com). The Pwnagotchi dev team is entirely focused on the v1.0 release and will NOT be providing support for alphatesters trying to get their Pwnagotchis working in the meantime. All technical support during this period of development is being provided by your fellow alphatesters in the Slack (thanks, everybody! :heart:).
|
||||
|
||||
https://www.pwnagotchi.ai
|
||||
|
||||
|
@@ -2,10 +2,10 @@
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import time
|
||||
import os
|
||||
import logging
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
|
||||
@@ -37,7 +37,7 @@ if __name__ == '__main__':
|
||||
keypair = KeyPair(view=display)
|
||||
agent = Agent(view=display, config=config, keypair=keypair)
|
||||
|
||||
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent._keypair.fingerprint, pwnagotchi.version))
|
||||
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent.fingerprint(), pwnagotchi.version))
|
||||
|
||||
for _, plugin in plugins.loaded.items():
|
||||
logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__))
|
||||
@@ -64,7 +64,7 @@ if __name__ == '__main__':
|
||||
display.on_manual_mode(agent.last_session)
|
||||
time.sleep(1)
|
||||
|
||||
if Agent.is_connected():
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
else:
|
||||
@@ -102,7 +102,7 @@ if __name__ == '__main__':
|
||||
# affect ours ... neat ^_^
|
||||
agent.next_epoch()
|
||||
|
||||
if Agent.is_connected():
|
||||
if grid.is_connected():
|
||||
plugins.on('internet_available', agent)
|
||||
|
||||
except Exception as e:
|
||||
|
@@ -34,7 +34,7 @@
|
||||
url: "https://github.com/bettercap/bettercap/releases/download/v2.25/bettercap_linux_armv6l_2.25.zip"
|
||||
ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip"
|
||||
pwngrid:
|
||||
url: "https://github.com/evilsocket/pwngrid/releases/download/v1.6.3/pwngrid_linux_armv6l_v1.6.3.zip"
|
||||
url: "https://github.com/evilsocket/pwngrid/releases/download/v1.7.6/pwngrid_linux_armv6l_1.7.6.zip"
|
||||
apt:
|
||||
hold:
|
||||
- firmware-atheros
|
||||
@@ -379,7 +379,7 @@
|
||||
copy:
|
||||
dest: /etc/pwnagotchi/config.yml
|
||||
content: |
|
||||
# Add your configuration overrides on this file any configuration changes done to defaults.yml will be lost!
|
||||
# Add your configuration overrides on this file any configuration changes done to default.yml will be lost!
|
||||
# Example:
|
||||
#
|
||||
# ui:
|
||||
@@ -461,7 +461,7 @@
|
||||
|
||||
If you want to change my configuration, use /etc/pwnagotchi/config.yml
|
||||
|
||||
All the configuration options can be found on /etc/pwnagotchi/defaults.yml,
|
||||
All the configuration options can be found on /etc/pwnagotchi/default.yml,
|
||||
but don't change this file because I will recreate it every time I'm restarted!
|
||||
|
||||
I'm managed by systemd. Here are some basic commands.
|
||||
@@ -497,12 +497,12 @@
|
||||
Description=pwngrid peer service.
|
||||
Documentation=https://pwnagotchi.ai
|
||||
Wants=network.target
|
||||
After=network.target
|
||||
After=bettercap.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PermissionsStartOnly=true
|
||||
ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -wait -log /var/log/pwngrid-peer.log
|
||||
ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -client-token /root/.api-enrollment.json -wait -log /var/log/pwngrid-peer.log -iface mon0
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import logging
|
||||
import time
|
||||
import pwnagotchi.ui.view as view
|
||||
|
||||
version = '1.0.0RC3'
|
||||
version = '1.0.0RC5'
|
||||
|
||||
_name = None
|
||||
|
||||
@@ -17,6 +17,11 @@ def name():
|
||||
return _name
|
||||
|
||||
|
||||
def uptime():
|
||||
with open('/proc/uptime') as fp:
|
||||
return int(fp.read().split('.')[0])
|
||||
|
||||
|
||||
def mem_usage():
|
||||
out = subprocess.getoutput("free -m")
|
||||
for line in out.split("\n"):
|
||||
@@ -60,3 +65,9 @@ def shutdown():
|
||||
time.sleep(5)
|
||||
os.system("sync")
|
||||
os.system("halt")
|
||||
|
||||
|
||||
def reboot():
|
||||
logging.warning("rebooting ...")
|
||||
os.system("sync")
|
||||
os.system("shutdown -r now")
|
||||
|
@@ -2,11 +2,10 @@ import time
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import _thread
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.plugins as plugins
|
||||
from pwnagotchi.log import LastSession
|
||||
@@ -41,15 +40,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
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
|
||||
|
||||
@@ -192,7 +182,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
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 ())
|
||||
self._epoch.observe(aps, list(self._peers.values()))
|
||||
return self._access_points
|
||||
|
||||
def get_access_points(self):
|
||||
@@ -200,6 +190,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
aps = []
|
||||
try:
|
||||
s = self.session()
|
||||
plugins.on("unfiltered_ap_list", self, s['wifi']['aps'])
|
||||
for ap in s['wifi']['aps']:
|
||||
if ap['hostname'] not in whitelist:
|
||||
if self._filter_included(ap):
|
||||
@@ -241,9 +232,9 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
return None
|
||||
|
||||
def _update_uptime(self, s):
|
||||
secs = time.time() - self._started_at
|
||||
secs = pwnagotchi.uptime()
|
||||
self._view.set('uptime', utils.secs_to_hhmmss(secs))
|
||||
self._view.set('epoch', '%04d' % self._epoch.epoch)
|
||||
# self._view.set('epoch', '%04d' % self._epoch.epoch)
|
||||
|
||||
def _update_counters(self):
|
||||
tot_aps = len(self._access_points)
|
||||
@@ -273,22 +264,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
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)
|
||||
self._view.set_closest_peer(self._closest_peer, len(self._peers))
|
||||
|
||||
def _save_recovery_data(self):
|
||||
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
|
||||
@@ -333,10 +310,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
s = self.session()
|
||||
self._update_uptime(s)
|
||||
|
||||
if self._advertiser is not None:
|
||||
self._update_advertisement(s)
|
||||
self._update_peers()
|
||||
|
||||
self._update_advertisement(s)
|
||||
self._update_peers()
|
||||
self._update_counters()
|
||||
|
||||
try:
|
||||
@@ -506,9 +481,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer):
|
||||
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")
|
||||
pwnagotchi.reboot()
|
||||
|
||||
def next_epoch(self):
|
||||
was_stale = self.is_stale()
|
||||
|
@@ -27,6 +27,7 @@ main:
|
||||
files:
|
||||
- /root/brain.nn
|
||||
- /root/brain.json
|
||||
- /root/.api-report.json
|
||||
- /root/handshakes/
|
||||
- /etc/pwnagotchi/
|
||||
- /etc/hostname
|
||||
@@ -66,6 +67,13 @@ main:
|
||||
wordlist_folder: /opt/wordlists/
|
||||
AircrackOnly:
|
||||
enabled: false
|
||||
bt-tether:
|
||||
enabled: false # if you want to use this, set ui.display.video.address to 0.0.0.0
|
||||
mac: ~ # mac of your bluetooth device
|
||||
ip: '192.168.44.44' # ip from which your pwnagotchi should be reachable
|
||||
netmask: 24
|
||||
interval: 1 # check every x minutes for device
|
||||
share_internet: false
|
||||
# monitor interface to use
|
||||
iface: mon0
|
||||
# command to run to bring the mon interface up in case it's not up already
|
||||
@@ -161,13 +169,13 @@ ui:
|
||||
display:
|
||||
enabled: true
|
||||
rotation: 180
|
||||
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2
|
||||
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat
|
||||
type: 'waveshare_2'
|
||||
# Possible options red/yellow/black (black used for monocromatic displays)
|
||||
color: 'black'
|
||||
video:
|
||||
enabled: true
|
||||
address: '10.0.0.2'
|
||||
address: '0.0.0.0'
|
||||
port: 8080
|
||||
|
||||
|
||||
|
97
pwnagotchi/grid.py
Normal file
97
pwnagotchi/grid.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import subprocess
|
||||
import socket
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
import pwnagotchi
|
||||
|
||||
# pwngrid-peer is running on port 8666
|
||||
API_ADDRESS = "http://127.0.0.1:8666/api/v1"
|
||||
|
||||
|
||||
def is_connected():
|
||||
try:
|
||||
socket.create_connection(("www.google.com", 80))
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def call(path, obj=None):
|
||||
url = '%s%s' % (API_ADDRESS, path)
|
||||
if obj is None:
|
||||
r = requests.get(url, headers=None)
|
||||
else:
|
||||
r = requests.post(url, headers=None, json=obj)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise Exception("(status %d) %s" % (r.status_code, r.text))
|
||||
return r.json()
|
||||
|
||||
|
||||
def advertise(enabled=True):
|
||||
return call("/mesh/%s" % 'true' if enabled else 'false')
|
||||
|
||||
|
||||
def set_advertisement_data(data):
|
||||
return call("/mesh/data", obj=data)
|
||||
|
||||
|
||||
def peers():
|
||||
return call("/mesh/peers")
|
||||
|
||||
|
||||
def closest_peer():
|
||||
all = peers()
|
||||
return all[0] if len(all) else None
|
||||
|
||||
|
||||
def update_data(last_session):
|
||||
brain = {}
|
||||
try:
|
||||
with open('/root/brain.json') as fp:
|
||||
brain = json.load(fp)
|
||||
except:
|
||||
pass
|
||||
|
||||
data = {
|
||||
'session': {
|
||||
'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,
|
||||
'version': pwnagotchi.version
|
||||
}
|
||||
|
||||
logging.debug("updating grid data: %s" % data)
|
||||
|
||||
call("/data", data)
|
||||
|
||||
|
||||
def report_ap(essid, bssid):
|
||||
try:
|
||||
call("/report/ap", {
|
||||
'essid': essid,
|
||||
'bssid': bssid,
|
||||
})
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception("error while reporting ap %s(%s)" % (essid, bssid))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def inbox(page=1, with_pager=False):
|
||||
obj = call("/inbox?p=%d" % page)
|
||||
return obj["messages"] if not with_pager else obj
|
@@ -16,6 +16,7 @@ class KeyPair(object):
|
||||
self.priv_key = None
|
||||
self.pub_path = "%s.pub" % self.priv_path
|
||||
self.pub_key = None
|
||||
self.fingerprint_path = os.path.join(path, "fingerprint")
|
||||
self._view = view
|
||||
|
||||
if not os.path.exists(self.path):
|
||||
@@ -46,6 +47,9 @@ class KeyPair(object):
|
||||
self.pub_key_pem_b64 = base64.b64encode(pem_ascii).decode("ascii")
|
||||
self.fingerprint = hashlib.sha256(pem_ascii).hexdigest()
|
||||
|
||||
with open(self.fingerprint_path, 'w+t') as fp:
|
||||
fp.write(self.fingerprint)
|
||||
|
||||
# no exception, keys loaded correctly.
|
||||
self._view.on_starting()
|
||||
return
|
||||
|
@@ -192,19 +192,19 @@ msgstr ""
|
||||
"#pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
msgstr "heures"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
msgstr "minutes"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
msgstr "secondes"
|
||||
|
||||
msgid "hour"
|
||||
msgstr ""
|
||||
msgstr "heure"
|
||||
|
||||
msgid "minute"
|
||||
msgstr ""
|
||||
msgstr "minute"
|
||||
|
||||
msgid "second"
|
||||
msgstr ""
|
||||
msgstr "seconde"
|
||||
|
BIN
pwnagotchi/locale/ga/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/ga/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
215
pwnagotchi/locale/ga/LC_MESSAGES/voice.po
Normal file
215
pwnagotchi/locale/ga/LC_MESSAGES/voice.po
Normal file
@@ -0,0 +1,215 @@
|
||||
# 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.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-10-09 17:42+0200\n"
|
||||
"PO-Revision-Date: 2019-10-15 23:46+0100\n"
|
||||
"Language: ga\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
|
||||
msgid "ZzzzZZzzzzZzzz"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hi, I'm Pwnagotchi! Starting ..."
|
||||
msgstr "Dia Duit, Pwnagotchi is ainm dom! Ag tosú ..."
|
||||
|
||||
msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Lá nua, seilg nua, pwns nua!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Haic An Phláinéid!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "AI réidh."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Tá an líonra néarach réidh."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey, channel {channel} is free! Your AP will say thanks."
|
||||
msgstr "Hé, tá cainéal {channel} ar fail! Déarfaidh do PR go raibh maith agat."
|
||||
|
||||
msgid "I'm bored ..."
|
||||
msgstr "Tá leadrán orm ..."
|
||||
|
||||
msgid "Let's go for a walk!"
|
||||
msgstr "Siúil liom, le do thoil!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Tá sé an lá is fearr i mo shaol!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Tá lá damanta agam :/"
|
||||
|
||||
msgid "I'm extremely bored ..."
|
||||
msgstr "Tá mé ag dul as mo mheabhair le leadrán ..."
|
||||
|
||||
msgid "I'm very sad ..."
|
||||
msgstr "Ta brón an domhain orm ..."
|
||||
|
||||
msgid "I'm sad"
|
||||
msgstr "Tá brón orm"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Tá an saol ar a thoil agam!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Déanaim pwnáil, dá bhrí sin táim ann."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Gréasáin - Tá an iliomad acu ann!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Tá craic iontach agam!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Ní haon pheaca é bheith fiosrach ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
msgstr "Dia Duit {name}! Is deas bualadh leat. {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Unit {name} is nearby! {name}"
|
||||
msgstr "Aonad {name} in aice láimhe! {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Uhm ... goodbye {name}"
|
||||
msgstr "Uhm... slán leat {name}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} is gone ..."
|
||||
msgstr "Tá {name} imithe ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Whoops ... {name} is gone."
|
||||
msgstr "Hoips … Tá {name} imithe."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{name} missed!"
|
||||
msgstr "Chaill mé ar {name}!"
|
||||
|
||||
msgid "Missed!"
|
||||
msgstr "Chaill mé é sin !"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Níl aon duine ag iarraidh imirt liom ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Tá uaigneas an domhain orm ..."
|
||||
|
||||
msgid "Where's everybody?!"
|
||||
msgstr "Cá bhfuil gach duine?!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Napping for {secs}s ..."
|
||||
msgstr "Néal a chodladh ar {secs}s ..."
|
||||
|
||||
msgid "Zzzzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "ZzzZzzz ({secs}s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "Oíche mhaith."
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Waiting for {secs}s ..."
|
||||
msgstr "Fan ar {secs}s ..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Looking around ({secs}s)"
|
||||
msgstr "Ag amharc uaim ar ({secs}s)"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hey {what} let's be friends!"
|
||||
msgstr "Hé {what} déanaimis síocháin!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Associating to {what}"
|
||||
msgstr "Ag coinneáil le {what}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "Hé {what}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Just decided that {mac} needs no WiFi!"
|
||||
msgstr "Tá cinneadh déanta agam. Níl {mac} sin de dhíth air WiFi!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Deauthenticating {mac}"
|
||||
msgstr "Bain fíordheimhniúde {mac}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kickbanning {mac}!"
|
||||
msgstr "Chiceáil mé agus cosc mé ar {mac}!"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Cool, we got {num} new handshake{plural}!"
|
||||
msgstr "Go hiontach, fuaireamar {num} handshake{plural}!"
|
||||
|
||||
msgid "Ops, something went wrong ... Rebooting ..."
|
||||
msgstr "Hoips...Tháinig ainghléas éigin..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Kicked {num} stations\n"
|
||||
msgstr "{num} stáisiún kick\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Made {num} new friends\n"
|
||||
msgstr "Rinne mé {num} cairde nua\n"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Got {num} handshakes\n"
|
||||
msgstr "Fuair me {num} cumarsáid thionscantach\n"
|
||||
|
||||
msgid "Met 1 peer"
|
||||
msgstr "Bhuail mé piara amháin"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Met {num} peers"
|
||||
msgstr "Bhuail me {num} piara"
|
||||
|
||||
#, 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 ""
|
||||
"Bhí me ag pwnáil ar {duration} agus chiceáil me ar {deauthed} cliaint! Chomh "
|
||||
"maith, bhuail me {associated} cairde nua and d'ith mé {handshakes}! "
|
||||
"#pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
|
||||
|
||||
msgid "hours"
|
||||
msgstr "uair on chloig"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "nóiméad"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "soicind"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "uair an chloig"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "nóiméad"
|
||||
|
||||
msgid "second"
|
||||
msgstr "soicind"
|
BIN
pwnagotchi/locale/jp/LC_MESSAGES/voice.mo
Normal file
BIN
pwnagotchi/locale/jp/LC_MESSAGES/voice.mo
Normal file
Binary file not shown.
212
pwnagotchi/locale/jp/LC_MESSAGES/voice.po
Normal file
212
pwnagotchi/locale/jp/LC_MESSAGES/voice.po
Normal 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 24534649+wytshadow@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-16 15:05+0200\n"
|
||||
"PO-Revision-Date: 2019-10-16 15:05+0200\n"
|
||||
"Last-Translator: wytshadow <24534649+wytshadow@users.noreply.github.com>\n"
|
||||
"Language-Team: pwnagotchi <24534649+wytshadow@users.noreply.github.com>\n"
|
||||
"Language: jp\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 "こんにちは、ポウナゴッチです!始めている。。。"
|
||||
|
||||
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}は無料です! キミの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 ""
|
||||
|
||||
#, 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})"
|
||||
|
||||
msgid "Good night."
|
||||
msgstr "お休みなさい。"
|
||||
|
||||
msgid "Zzz"
|
||||
msgstr "す〜"
|
||||
|
||||
#, 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 ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Yo {what}!"
|
||||
msgstr "よー{what}!"
|
||||
|
||||
#, 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 "よし、{num}新しいハンドシェイクがある!"
|
||||
|
||||
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 "{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 ""
|
||||
|
||||
msgid "hours"
|
||||
msgstr "時間"
|
||||
|
||||
msgid "minutes"
|
||||
msgstr "分"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "hour"
|
||||
msgstr "時間"
|
||||
|
||||
msgid "minute"
|
||||
msgstr "分"
|
||||
|
||||
msgid "second"
|
||||
msgstr "秒"
|
@@ -29,10 +29,10 @@ msgid "New day, new hunt, new pwns!"
|
||||
msgstr "Новый день, новая охота, новые взломы!"
|
||||
|
||||
msgid "Hack the Planet!"
|
||||
msgstr "Взломаем всю планету!"
|
||||
msgstr "Хак зе планет!"
|
||||
|
||||
msgid "AI ready."
|
||||
msgstr "Искусственный интеллект готов."
|
||||
msgstr "AI готов."
|
||||
|
||||
msgid "The neural network is ready."
|
||||
msgstr "Нейронная сеть готова."
|
||||
@@ -48,7 +48,7 @@ msgid "Let's go for a walk!"
|
||||
msgstr "Пойдем прогуляемся!"
|
||||
|
||||
msgid "This is the best day of my life!"
|
||||
msgstr "Это лучший день в моей жизни!"
|
||||
msgstr "Лучший день в моей жизни!"
|
||||
|
||||
msgid "Shitty day :/"
|
||||
msgstr "Дерьмовый день :/"
|
||||
@@ -63,19 +63,19 @@ msgid "I'm sad"
|
||||
msgstr "Мне грустно"
|
||||
|
||||
msgid "I'm living the life!"
|
||||
msgstr "Я живу своей жизнью!"
|
||||
msgstr "Угараю по полной!"
|
||||
|
||||
msgid "I pwn therefore I am."
|
||||
msgstr "Я взламываю, поэтому я существую."
|
||||
|
||||
msgid "So many networks!!!"
|
||||
msgstr "Так, много сетей!!!"
|
||||
msgstr "Так много сетей!!!"
|
||||
|
||||
msgid "I'm having so much fun!"
|
||||
msgstr "Мне так весело!"
|
||||
|
||||
msgid "My crime is that of curiosity ..."
|
||||
msgstr "Моё преступление - это любопытство …"
|
||||
msgstr "Моe преступление - это любопытство …"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Hello {name}! Nice to meet you. {name}"
|
||||
@@ -105,7 +105,7 @@ msgid "Missed!"
|
||||
msgstr "Промахнулся!"
|
||||
|
||||
msgid "Nobody wants to play with me ..."
|
||||
msgstr "Никто не хочет играть со мной …"
|
||||
msgstr "Никто не хочет со мной играть ..."
|
||||
|
||||
msgid "I feel so alone ..."
|
||||
msgstr "Мне так одиноко …"
|
||||
|
@@ -129,10 +129,15 @@ class LastSession(object):
|
||||
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.last_peer = Peer({
|
||||
'session_id': sid,
|
||||
'channel': 1,
|
||||
'rssi': int(rssi),
|
||||
'identity': pubkey,
|
||||
'advertisement':{
|
||||
'name': name,
|
||||
'pwnd_tot': int(pwnd_tot)
|
||||
}})
|
||||
self.peers += 1
|
||||
cache[pubkey] = self.last_peer
|
||||
else:
|
||||
|
@@ -1,4 +0,0 @@
|
||||
import os
|
||||
|
||||
def new_session_id():
|
||||
return ':'.join(['%02x' % b for b in os.urandom(6)])
|
||||
|
@@ -1,176 +0,0 @@
|
||||
import time
|
||||
import json
|
||||
import _thread
|
||||
import threading
|
||||
import logging
|
||||
from scapy.all import Dot11, 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):
|
||||
dot11 = p.getlayer(Dot11)
|
||||
|
||||
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]
|
@@ -1,32 +1,28 @@
|
||||
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):
|
||||
def __init__(self, obj):
|
||||
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
|
||||
self.session_id = obj['session_id']
|
||||
self.last_channel = obj['channel']
|
||||
self.rssi = obj['rssi']
|
||||
self.adv = obj['advertisement']
|
||||
|
||||
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']))
|
||||
def update(self, new):
|
||||
if self.name() != new.name():
|
||||
logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), new.name()))
|
||||
|
||||
if self.session_id != sid:
|
||||
logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, sid))
|
||||
if self.session_id != new.session_id:
|
||||
logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, new.session_id))
|
||||
|
||||
self.presence[channel - 1] += 1
|
||||
self.adv = adv
|
||||
self.rssi = rssi
|
||||
self.session_id = sid
|
||||
self.adv = new.adv
|
||||
self.rssi = new.rssi
|
||||
self.session_id = new.session_id
|
||||
self.last_seen = time.time()
|
||||
|
||||
def inactive_for(self):
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import _thread
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pwnagotchi
|
||||
import pwnagotchi.utils as utils
|
||||
import pwnagotchi.ui.faces as faces
|
||||
import pwnagotchi.plugins as plugins
|
||||
import pwnagotchi.grid as grid
|
||||
from pwnagotchi.mesh.peer import Peer
|
||||
|
||||
|
||||
class AsyncAdvertiser(object):
|
||||
@@ -10,38 +15,81 @@ class AsyncAdvertiser(object):
|
||||
self._config = config
|
||||
self._view = view
|
||||
self._keypair = keypair
|
||||
self._advertiser = None
|
||||
self._advertisement = {
|
||||
'name': pwnagotchi.name(),
|
||||
'version': pwnagotchi.version,
|
||||
'identity': self._keypair.fingerprint,
|
||||
'face': faces.FRIEND,
|
||||
'pwnd_run': 0,
|
||||
'pwnd_tot': 0,
|
||||
'uptime': 0,
|
||||
'epoch': 0,
|
||||
'policy': self._config['personality']
|
||||
}
|
||||
self._peers = {}
|
||||
self._closest_peer = None
|
||||
|
||||
def keypair(self):
|
||||
return self._keypair
|
||||
def fingerprint(self):
|
||||
return self._keypair.fingerprint
|
||||
|
||||
def _update_advertisement(self, s):
|
||||
self._advertisement['pwnd_run'] = len(self._handshakes)
|
||||
self._advertisement['pwnd_tot'] = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
|
||||
self._advertisement['uptime'] = pwnagotchi.uptime()
|
||||
self._advertisement['epoch'] = self._epoch.epoch
|
||||
grid.set_advertisement_data(self._advertisement)
|
||||
|
||||
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)
|
||||
_thread.start_new_thread(self._adv_poller, ())
|
||||
|
||||
grid.set_advertisement_data(self._advertisement)
|
||||
grid.advertise(True)
|
||||
self._view.on_state_change('face', self._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_face_change(self, old, new):
|
||||
self._advertisement['face'] = new
|
||||
grid.set_advertisement_data(self._advertisement)
|
||||
|
||||
def _on_lost_unit(self, peer):
|
||||
self._view.on_lost_peer(peer)
|
||||
plugins.on('peer_lost', self, peer)
|
||||
def _adv_poller(self):
|
||||
while True:
|
||||
logging.debug("polling pwngrid-peer for peers ...")
|
||||
|
||||
try:
|
||||
grid_peers = grid.peers()
|
||||
new_peers = {}
|
||||
|
||||
self._closest_peer = None
|
||||
for obj in grid_peers:
|
||||
peer = Peer(obj)
|
||||
new_peers[peer.identity()] = peer
|
||||
if self._closest_peer is None:
|
||||
self._closest_peer = peer
|
||||
|
||||
# check who's gone
|
||||
to_delete = []
|
||||
for ident, peer in self._peers.items():
|
||||
if ident not in new_peers:
|
||||
self._view.on_lost_peer(peer)
|
||||
plugins.on('peer_lost', self, peer)
|
||||
to_delete.append(ident)
|
||||
|
||||
for ident in to_delete:
|
||||
del self._peers[ident]
|
||||
|
||||
for ident, peer in new_peers.items():
|
||||
# check who's new
|
||||
if ident not in self._peers:
|
||||
self._peers[ident] = peer
|
||||
self._view.on_new_peer(peer)
|
||||
plugins.on('peer_detected', self, peer)
|
||||
# update the rest
|
||||
else:
|
||||
self._peers[ident].update(peer)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("error while polling pwngrid-peer")
|
||||
|
||||
time.sleep(1)
|
||||
|
@@ -1,8 +1,6 @@
|
||||
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)
|
||||
@@ -12,26 +10,3 @@ def freq_to_channel(freq):
|
||||
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
|
||||
|
495
pwnagotchi/plugins/default/bt-tether.py
Normal file
495
pwnagotchi/plugins/default/bt-tether.py
Normal file
@@ -0,0 +1,495 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'bt-tether'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This makes the display reachable over bluetooth'
|
||||
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import logging
|
||||
import subprocess
|
||||
import dbus
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
READY = False
|
||||
INTERVAL = StatusFile('/root/.bt-tether')
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
class BTError(Exception):
|
||||
"""
|
||||
Custom bluetooth exception
|
||||
"""
|
||||
pass
|
||||
|
||||
class BTNap:
|
||||
"""
|
||||
This class creates a bluetooth connection to the specified bt-mac
|
||||
|
||||
see https://github.com/bablokb/pi-btnap/blob/master/files/usr/local/sbin/btnap.service.py
|
||||
"""
|
||||
|
||||
IFACE_BASE = 'org.bluez'
|
||||
IFACE_DEV = 'org.bluez.Device1'
|
||||
IFACE_ADAPTER = 'org.bluez.Adapter1'
|
||||
IFACE_PROPS = 'org.freedesktop.DBus.Properties'
|
||||
|
||||
def __init__(self, mac):
|
||||
self._mac = mac
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_bus():
|
||||
"""
|
||||
Get systembus obj
|
||||
"""
|
||||
bus = getattr(BTNap.get_bus, 'cached_obj', None)
|
||||
if not bus:
|
||||
bus = BTNap.get_bus.cached_obj = dbus.SystemBus()
|
||||
return bus
|
||||
|
||||
@staticmethod
|
||||
def get_manager():
|
||||
"""
|
||||
Get manager obj
|
||||
"""
|
||||
manager = getattr(BTNap.get_manager, 'cached_obj', None)
|
||||
if not manager:
|
||||
manager = BTNap.get_manager.cached_obj = dbus.Interface(
|
||||
BTNap.get_bus().get_object(BTNap.IFACE_BASE, '/'),
|
||||
'org.freedesktop.DBus.ObjectManager' )
|
||||
return manager
|
||||
|
||||
@staticmethod
|
||||
def prop_get(obj, k, iface=None):
|
||||
"""
|
||||
Get a property of the obj
|
||||
"""
|
||||
if iface is None:
|
||||
iface = obj.dbus_interface
|
||||
return obj.Get(iface, k, dbus_interface=BTNap.IFACE_PROPS)
|
||||
|
||||
@staticmethod
|
||||
def prop_set(obj, k, v, iface=None):
|
||||
"""
|
||||
Set a property of the obj
|
||||
"""
|
||||
if iface is None:
|
||||
iface = obj.dbus_interface
|
||||
return obj.Set(iface, k, v, dbus_interface=BTNap.IFACE_PROPS)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def find_adapter(pattern=None):
|
||||
"""
|
||||
Find the bt adapter
|
||||
"""
|
||||
|
||||
return BTNap.find_adapter_in_objects(BTNap.get_manager().GetManagedObjects(), pattern)
|
||||
|
||||
@staticmethod
|
||||
def find_adapter_in_objects(objects, pattern=None):
|
||||
"""
|
||||
Finds the obj with a pattern
|
||||
"""
|
||||
bus, obj = BTNap.get_bus(), None
|
||||
for path, ifaces in objects.items():
|
||||
adapter = ifaces.get(BTNap.IFACE_ADAPTER)
|
||||
if adapter is None:
|
||||
continue
|
||||
if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
yield dbus.Interface(obj, BTNap.IFACE_ADAPTER)
|
||||
if obj is None:
|
||||
raise BTError('Bluetooth adapter not found')
|
||||
|
||||
@staticmethod
|
||||
def find_device(device_address, adapter_pattern=None):
|
||||
"""
|
||||
Finds the device
|
||||
"""
|
||||
return BTNap.find_device_in_objects(BTNap.get_manager().GetManagedObjects(),
|
||||
device_address, adapter_pattern)
|
||||
|
||||
@staticmethod
|
||||
def find_device_in_objects(objects, device_address, adapter_pattern=None):
|
||||
"""
|
||||
Finds the device in objects
|
||||
"""
|
||||
bus = BTNap.get_bus()
|
||||
path_prefix = ''
|
||||
if adapter_pattern:
|
||||
if not isinstance(adapter_pattern, str):
|
||||
adapter = adapter_pattern
|
||||
else:
|
||||
adapter = BTNap.find_adapter_in_objects(objects, adapter_pattern)
|
||||
path_prefix = adapter.object_path
|
||||
for path, ifaces in objects.items():
|
||||
device = ifaces.get(BTNap.IFACE_DEV)
|
||||
if device is None:
|
||||
continue
|
||||
if device['Address'] == device_address and path.startswith(path_prefix):
|
||||
obj = bus.get_object(BTNap.IFACE_BASE, path)
|
||||
return dbus.Interface(obj, BTNap.IFACE_DEV)
|
||||
raise BTError('Bluetooth device not found')
|
||||
|
||||
def power(self, on=True):
|
||||
"""
|
||||
Set power of devices to on/off
|
||||
"""
|
||||
|
||||
devs = list(BTNap.find_adapter())
|
||||
devs = dict((BTNap.prop_get(dev, 'Address'), dev) for dev in devs)
|
||||
|
||||
for dev_addr, dev in devs.items():
|
||||
BTNap.prop_set(dev, 'Powered', on)
|
||||
logging.debug('Set power of %s (addr %s) to %s', dev.object_path, dev_addr, str(on))
|
||||
|
||||
if devs:
|
||||
return list(devs.values())[0]
|
||||
|
||||
return None
|
||||
|
||||
def is_connected(self):
|
||||
"""
|
||||
Check if already connected
|
||||
"""
|
||||
bt_dev = self.power(True)
|
||||
|
||||
if not bt_dev:
|
||||
return False
|
||||
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
return bool(BTNap.prop_get(dev_remote, 'Connected'))
|
||||
except BTError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def is_paired(self):
|
||||
"""
|
||||
Check if already connected
|
||||
"""
|
||||
bt_dev = self.power(True)
|
||||
|
||||
if not bt_dev:
|
||||
return False
|
||||
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
return bool(BTNap.prop_get(dev_remote, 'Paired'))
|
||||
except BTError:
|
||||
pass
|
||||
return False
|
||||
|
||||
def wait_for_device(self, timeout=15):
|
||||
"""
|
||||
Wait for device
|
||||
|
||||
returns device if found None if not
|
||||
"""
|
||||
bt_dev = self.power(True)
|
||||
|
||||
if not bt_dev:
|
||||
return None
|
||||
|
||||
try:
|
||||
bt_dev.StartDiscovery()
|
||||
except Exception:
|
||||
# can fail with org.bluez.Error.NotReady / org.bluez.Error.Failed
|
||||
# TODO: add loop?
|
||||
pass
|
||||
|
||||
dev_remote = None
|
||||
|
||||
# could be set to 0, so check if > -1
|
||||
while timeout > -1:
|
||||
try:
|
||||
dev_remote = BTNap.find_device(self._mac, bt_dev)
|
||||
logging.debug('Using remote device (addr: %s): %s',
|
||||
BTNap.prop_get(dev_remote, 'Address'), dev_remote.object_path )
|
||||
break
|
||||
except BTError:
|
||||
pass
|
||||
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
|
||||
try:
|
||||
bt_dev.StopDiscovery()
|
||||
except Exception:
|
||||
# can fail with org.bluez.Error.NotReady / org.bluez.Error.Failed / org.bluez.Error.NotAuthorized
|
||||
pass
|
||||
|
||||
return dev_remote
|
||||
|
||||
|
||||
def connect(self, reconnect=False):
|
||||
"""
|
||||
Connect to device
|
||||
|
||||
return True if connected; False if failed
|
||||
"""
|
||||
|
||||
# check if device is close
|
||||
dev_remote = self.wait_for_device()
|
||||
|
||||
if not dev_remote:
|
||||
return False
|
||||
|
||||
try:
|
||||
dev_remote.Pair()
|
||||
logging.info('BT-TETHER: Successful paired with device ;)')
|
||||
except Exception:
|
||||
# can fail because of AlreadyExists etc.
|
||||
pass
|
||||
|
||||
try:
|
||||
dev_remote.ConnectProfile('nap')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
net = dbus.Interface(dev_remote, 'org.bluez.Network1')
|
||||
|
||||
try:
|
||||
net.Connect('nap')
|
||||
except dbus.exceptions.DBusException as err:
|
||||
if err.get_dbus_name() != 'org.bluez.Error.Failed':
|
||||
raise
|
||||
|
||||
connected = BTNap.prop_get(net, 'Connected')
|
||||
|
||||
if not connected:
|
||||
return False
|
||||
|
||||
if reconnect:
|
||||
net.Disconnect()
|
||||
return self.connect(reconnect=False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
#################################################
|
||||
#################################################
|
||||
#################################################
|
||||
|
||||
class SystemdUnitWrapper:
|
||||
"""
|
||||
systemd wrapper
|
||||
"""
|
||||
|
||||
def __init__(self, unit):
|
||||
self.unit = unit
|
||||
|
||||
@staticmethod
|
||||
def _action_on_unit(action, unit):
|
||||
process = subprocess.Popen(f"systemctl {action} {unit}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def daemon_reload():
|
||||
"""
|
||||
Calls systemctl daemon-reload
|
||||
"""
|
||||
process = subprocess.Popen("systemctl daemon-reload", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Checks if unit is active
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('is-active', self.unit)
|
||||
|
||||
def is_enabled(self):
|
||||
"""
|
||||
Checks if unit is enabled
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('is-enabled', self.unit)
|
||||
|
||||
def is_failed(self):
|
||||
"""
|
||||
Checks if unit is failed
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('is-failed', self.unit)
|
||||
|
||||
def enable(self):
|
||||
"""
|
||||
Enables the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('enable', self.unit)
|
||||
|
||||
def disable(self):
|
||||
"""
|
||||
Disables the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('disable', self.unit)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('start', self.unit)
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('stop', self.unit)
|
||||
|
||||
def restart(self):
|
||||
"""
|
||||
Restarts the unit
|
||||
"""
|
||||
return SystemdUnitWrapper._action_on_unit('restart', self.unit)
|
||||
|
||||
|
||||
class IfaceWrapper:
|
||||
"""
|
||||
Small wrapper to check and manage ifaces
|
||||
|
||||
see: https://github.com/rlisagor/pynetlinux/blob/master/pynetlinux/ifconfig.py
|
||||
"""
|
||||
|
||||
def __init__(self, iface):
|
||||
self.iface = iface
|
||||
self.path = f"/sys/class/net/{iface}"
|
||||
|
||||
def exists(self):
|
||||
"""
|
||||
Checks if iface exists
|
||||
"""
|
||||
return os.path.exists(self.path)
|
||||
|
||||
def is_up(self):
|
||||
"""
|
||||
Checks if iface is ip
|
||||
"""
|
||||
return open(f"{self.path}/operstate", 'r').read().rsplit('\n') == 'up'
|
||||
|
||||
|
||||
def set_addr(self, addr):
|
||||
"""
|
||||
Set the netmask
|
||||
"""
|
||||
process = subprocess.Popen(f"ip addr add {addr} dev {self.iface}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 2 or process.returncode == 0: # 2 = already set
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def set_route(addr):
|
||||
process = subprocess.Popen(f"ip route replace default via {addr}", shell=True, stdin=None,
|
||||
stdout=open("/dev/null", "w"), stderr=None, executable="/bin/bash")
|
||||
process.wait()
|
||||
|
||||
if process.returncode > 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def on_loaded():
|
||||
"""
|
||||
Gets called when the plugin gets loaded
|
||||
"""
|
||||
global READY
|
||||
global INTERVAL
|
||||
|
||||
for opt in ['share_internet', 'mac', 'ip', 'netmask', 'interval']:
|
||||
if opt not in OPTIONS or (opt in OPTIONS and OPTIONS[opt] is None):
|
||||
logging.error("BT-TET: Pleace specify the %s in your config.yml.", opt)
|
||||
return
|
||||
|
||||
# ensure bluetooth is running
|
||||
bt_unit = SystemdUnitWrapper('bluetooth.service')
|
||||
if not bt_unit.is_active():
|
||||
if not bt_unit.start():
|
||||
logging.error("BT-TET: Can't start bluetooth.service")
|
||||
return
|
||||
|
||||
INTERVAL.update()
|
||||
READY = True
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
"""
|
||||
Try to connect to device
|
||||
"""
|
||||
|
||||
if READY:
|
||||
global INTERVAL
|
||||
if INTERVAL.newer_then_minutes(OPTIONS['interval']):
|
||||
return
|
||||
|
||||
INTERVAL.update()
|
||||
|
||||
bt = BTNap(OPTIONS['mac'])
|
||||
|
||||
logging.debug('BT-TETHER: Check if already connected and paired')
|
||||
if bt.is_connected() and bt.is_paired():
|
||||
logging.debug('BT-TETHER: Already connected and paired')
|
||||
ui.set('bluetooth', 'CON')
|
||||
else:
|
||||
logging.debug('BT-TETHER: Try to connect to mac')
|
||||
if bt.connect():
|
||||
logging.info('BT-TETHER: Successfuly connected')
|
||||
else:
|
||||
logging.error('BT-TETHER: Could not connect')
|
||||
ui.set('bluetooth', 'NF')
|
||||
return
|
||||
|
||||
btnap_iface = IfaceWrapper('bnep0')
|
||||
logging.debug('BT-TETHER: Check interface')
|
||||
if btnap_iface.exists():
|
||||
logging.debug('BT-TETHER: Interface found')
|
||||
|
||||
# check ip
|
||||
addr = f"{OPTIONS['ip']}/{OPTIONS['netmask']}"
|
||||
|
||||
logging.debug('BT-TETHER: Try to set ADDR to interface')
|
||||
if not btnap_iface.set_addr(addr):
|
||||
ui.set('bluetooth', 'ERR1')
|
||||
logging.error("BT-TETHER: Could not set ip of bnep0 to %s", addr)
|
||||
return
|
||||
else:
|
||||
logging.debug('BT-TETHER: Set ADDR to interface')
|
||||
|
||||
# change route if sharking
|
||||
if OPTIONS['share_internet']:
|
||||
logging.debug('BT-TETHER: Set routing and change resolv.conf')
|
||||
IfaceWrapper.set_route(".".join(OPTIONS['ip'].split('.')[:-1] + ['1'])) # im not proud about that
|
||||
# fix resolv.conf; dns over https ftw!
|
||||
with open('/etc/resolv.conf', 'r+') as resolv:
|
||||
nameserver = resolv.read()
|
||||
if 'nameserver 9.9.9.9' not in nameserver:
|
||||
logging.info('BT-TETHER: Added nameserver')
|
||||
resolv.seek(0)
|
||||
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
|
||||
|
||||
ui.set('bluetooth', 'CON')
|
||||
else:
|
||||
logging.error('BT-TETHER: bnep0 not found')
|
||||
ui.set('bluetooth', 'ERR2')
|
||||
|
||||
|
||||
def on_ui_setup(ui):
|
||||
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 30, 0),
|
||||
label_font=fonts.Bold, text_font=fonts.Medium))
|
@@ -6,11 +6,9 @@ __description__ = 'This plugin signals the unit cryptographic identity and list
|
||||
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
import glob
|
||||
import json
|
||||
import subprocess
|
||||
import pwnagotchi
|
||||
|
||||
import pwnagotchi.grid as grid
|
||||
import pwnagotchi.utils as utils
|
||||
from pwnagotchi.ui.components import LabeledValue
|
||||
from pwnagotchi.ui.view import BLACK
|
||||
@@ -65,74 +63,13 @@ def is_excluded(what):
|
||||
return False
|
||||
|
||||
|
||||
def grid_call(path, obj=None):
|
||||
# pwngrid-peer is running on port 8666
|
||||
api_address = 'http://127.0.0.1:8666/api/v1%s' % path
|
||||
if obj is None:
|
||||
r = requests.get(api_address, headers=None)
|
||||
else:
|
||||
r = requests.post(api_address, headers=None, json=obj)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise Exception("(status %d) %s" % (r.status_code, r.text))
|
||||
return r.json()
|
||||
|
||||
|
||||
def grid_update_data(last_session):
|
||||
brain = {}
|
||||
try:
|
||||
with open('/root/brain.json') as fp:
|
||||
brain = json.load(fp)
|
||||
except:
|
||||
pass
|
||||
|
||||
data = {
|
||||
'session': {
|
||||
'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,
|
||||
'version': pwnagotchi.version
|
||||
}
|
||||
|
||||
logging.debug("updating grid data: %s" % data)
|
||||
|
||||
grid_call("/data", data)
|
||||
|
||||
|
||||
def grid_report_ap(essid, bssid):
|
||||
try:
|
||||
grid_call("/report/ap", {
|
||||
'essid': essid,
|
||||
'bssid': bssid,
|
||||
})
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.exception("error while reporting ap %s(%s)" % (essid, bssid))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def grid_inbox():
|
||||
return grid_call("/inbox")["messages"]
|
||||
|
||||
|
||||
def on_ui_update(ui):
|
||||
new_value = ' %d (%d)' % (UNREAD_MESSAGES, TOTAL_MESSAGES)
|
||||
if not ui.has_element('mailbox') and TOTAL_MESSAGES > 0:
|
||||
if ui.is_inky():
|
||||
pos=(80, 0)
|
||||
pos = (80, 0)
|
||||
else:
|
||||
pos=(100,0)
|
||||
pos = (100, 0)
|
||||
ui.add_element('mailbox',
|
||||
LabeledValue(color=BLACK, label='MSG', value=new_value,
|
||||
position=pos,
|
||||
@@ -141,13 +78,19 @@ def on_ui_update(ui):
|
||||
ui.set('mailbox', new_value)
|
||||
|
||||
|
||||
def set_reported(reported, net_id):
|
||||
global REPORT
|
||||
reported.append(net_id)
|
||||
REPORT.update(data={'reported': reported})
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
global REPORT, UNREAD_MESSAGES, TOTAL_MESSAGES
|
||||
|
||||
logging.debug("internet available")
|
||||
|
||||
try:
|
||||
grid_update_data(agent.last_session)
|
||||
grid.update_data(agent.last_session)
|
||||
except Exception as e:
|
||||
logging.error("error connecting to the pwngrid-peer service: %s" % e)
|
||||
return
|
||||
@@ -155,13 +98,13 @@ def on_internet_available(agent):
|
||||
try:
|
||||
logging.debug("checking mailbox ...")
|
||||
|
||||
messages = grid_inbox()
|
||||
messages = grid.inbox()
|
||||
TOTAL_MESSAGES = len(messages)
|
||||
UNREAD_MESSAGES = len([m for m in messages if m['seen_at'] is None])
|
||||
|
||||
if TOTAL_MESSAGES:
|
||||
on_ui_update(agent.view())
|
||||
logging.debug( " %d unread messages of %d total" % (UNREAD_MESSAGES, TOTAL_MESSAGES))
|
||||
logging.debug(" %d unread messages of %d total" % (UNREAD_MESSAGES, TOTAL_MESSAGES))
|
||||
|
||||
logging.debug("checking pcaps")
|
||||
|
||||
@@ -181,17 +124,19 @@ def on_internet_available(agent):
|
||||
net_id = os.path.basename(pcap_file).replace('.pcap', '')
|
||||
if net_id not in reported:
|
||||
if is_excluded(net_id):
|
||||
logging.info("skipping %s due to exclusion filter" % pcap_file)
|
||||
logging.debug("skipping %s due to exclusion filter" % pcap_file)
|
||||
set_reported(reported, net_id)
|
||||
continue
|
||||
|
||||
essid, bssid = parse_pcap(pcap_file)
|
||||
if bssid:
|
||||
add_as_reported = False
|
||||
if is_excluded(essid) or is_excluded(bssid):
|
||||
logging.debug("not reporting %s due to exclusion filter" % pcap_file)
|
||||
|
||||
elif grid_report_ap(essid, bssid):
|
||||
reported.append(net_id)
|
||||
REPORT.update(data={'reported': reported})
|
||||
set_reported(reported, net_id)
|
||||
else:
|
||||
if grid.report_ap(essid, bssid):
|
||||
set_reported(reported, net_id)
|
||||
else:
|
||||
logging.warning("no bssid found?!")
|
||||
else:
|
||||
|
@@ -1,5 +1,5 @@
|
||||
__author__ = 'zenzen san'
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '2.0.0'
|
||||
__name__ = 'net-pos'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = """Saves a json file with the access points with more signal
|
||||
@@ -11,33 +11,24 @@ import logging
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
|
||||
ALREADY_SAVED = None
|
||||
SKIP = None
|
||||
REPORT = StatusFile('/root/.net_pos_saved', data_format='json')
|
||||
SKIP = list()
|
||||
READY = False
|
||||
OPTIONS = {}
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
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):
|
||||
@@ -55,20 +46,22 @@ def _append_saved(path):
|
||||
|
||||
def on_internet_available(agent):
|
||||
global SKIP
|
||||
global REPORT
|
||||
|
||||
if READY:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
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)
|
||||
new_np_files = set(all_np_files) - set(reported) - set(SKIP)
|
||||
|
||||
if new_np_files:
|
||||
logging.info("NET-POS: Found {num} new net-pos files. Fetching positions ...", len(new_np_files))
|
||||
logging.info("NET-POS: Found %d 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):
|
||||
@@ -76,8 +69,8 @@ def on_internet_available(agent):
|
||||
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)
|
||||
reported.append(np_file)
|
||||
REPORT.update(data={'reported': reported})
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -85,18 +78,21 @@ def on_internet_available(agent):
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
logging.error("NET-POS: %s", req_e)
|
||||
SKIP += np_file
|
||||
continue
|
||||
except json.JSONDecodeError as js_e:
|
||||
logging.error("NET-POS: %s", js_e)
|
||||
SKIP += np_file
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
SKIP += np_file
|
||||
continue
|
||||
|
||||
with open(geo_file, 'w+t') as sf:
|
||||
json.dump(geo_data, sf)
|
||||
|
||||
ALREADY_SAVED.append(np_file)
|
||||
_append_saved(np_file)
|
||||
reported.append(np_file)
|
||||
REPORT.update(data={'reported': reported})
|
||||
|
||||
display.set('status', f"Fetching positions ({idx+1}/{len(new_np_files)})")
|
||||
display.update(force=True)
|
||||
@@ -108,15 +104,15 @@ def on_handshake(agent, filename, access_point, client_station):
|
||||
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
|
||||
|
||||
try:
|
||||
with open(netpos_filename, 'w+t') as fp:
|
||||
json.dump(netpos, fp)
|
||||
with open(netpos_filename, 'w+t') as net_pos_file:
|
||||
json.dump(netpos, net_pos_file)
|
||||
except OSError as os_e:
|
||||
logging.error("NET-POS: %s", os_e)
|
||||
|
||||
|
||||
def _get_netpos(agent):
|
||||
aps = agent.get_access_points()
|
||||
netpos = {}
|
||||
netpos = dict()
|
||||
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]:
|
||||
|
@@ -1,5 +1,5 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '2.0.0'
|
||||
__name__ = 'onlinehashcrack'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploades handshakes to https://onlinehashcrack.com'
|
||||
@@ -7,9 +7,11 @@ __description__ = 'This plugin automatically uploades handshakes to https://onli
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
READY = False
|
||||
ALREADY_UPLOADED = None
|
||||
REPORT = StatusFile('/root/.ohc_uploads', data_format='json')
|
||||
SKIP = list()
|
||||
OPTIONS = dict()
|
||||
|
||||
|
||||
@@ -18,20 +20,11 @@ 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):
|
||||
if 'email' not 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
|
||||
|
||||
|
||||
@@ -59,14 +52,17 @@ def on_internet_available(agent):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
global REPORT
|
||||
global SKIP
|
||||
if READY:
|
||||
display = agent.view()
|
||||
config = agent.config()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
|
||||
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)
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(SKIP)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
|
||||
@@ -76,12 +72,15 @@ def on_internet_available(agent):
|
||||
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")
|
||||
reported.append(handshake)
|
||||
REPORT.update(data={'reported': reported})
|
||||
logging.info(f"OHC: Successfuly uploaded {handshake}")
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
SKIP.append(handshake)
|
||||
logging.error("OHC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error(f"OHC: Got the following error: {os_e}")
|
||||
SKIP.append(handshake)
|
||||
logging.error("OHC: %s", os_e)
|
||||
continue
|
||||
|
||||
|
@@ -18,7 +18,7 @@ def on_ui_update(ui):
|
||||
global update_count
|
||||
update_count += 1
|
||||
if update_count == OPTIONS['refresh_interval']:
|
||||
ui._init_display()
|
||||
ui.init_display()
|
||||
ui.set('status', "Screen cleaned")
|
||||
logging.info("Screen refreshing")
|
||||
update_count = 0
|
||||
|
22
pwnagotchi/plugins/default/unfiltered_example.py
Normal file
22
pwnagotchi/plugins/default/unfiltered_example.py
Normal file
@@ -0,0 +1,22 @@
|
||||
__author__ = 'diemelcw@gmail.com'
|
||||
__version__ = '1.0.0'
|
||||
__name__ = 'unfiltered_example'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'An example plugin for pwnagotchi that implements on_unfiltered_ap_list(agent,aps)'
|
||||
|
||||
import logging
|
||||
|
||||
# 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("%s plugin loaded" % __name__)
|
||||
|
||||
# called when AP list is ready, before whitelist filtering has occured
|
||||
def on_unfiltered_ap_list(agent,aps):
|
||||
logging.info("Unfiltered AP list to follow")
|
||||
for ap in aps:
|
||||
logging.info(ap['hostname'])
|
||||
|
||||
## Additional logic here ##
|
@@ -1,5 +1,5 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '2.0.0'
|
||||
__name__ = 'wigle'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploades collected wifis to wigle.net'
|
||||
@@ -11,111 +11,24 @@ 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
|
||||
from pwnagotchi.utils import WifiInfo, FieldNotFoundError, extract_from_pcap, StatusFile
|
||||
|
||||
READY = False
|
||||
ALREADY_UPLOADED = None
|
||||
SKIP = None
|
||||
REPORT = StatusFile('/root/.wigle_uploads', data_format='json')
|
||||
SKIP = list()
|
||||
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
|
||||
|
||||
|
||||
@@ -197,24 +110,24 @@ def _send_to_wigle(lines, api_key, timeout=30):
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \
|
||||
Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA
|
||||
from scapy.all import Scapy_Exception
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
global ALREADY_UPLOADED
|
||||
global REPORT
|
||||
global SKIP
|
||||
|
||||
if READY:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
|
||||
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)
|
||||
new_gps_files = set(all_gps_files) - set(reported) - set(SKIP)
|
||||
|
||||
if new_gps_files:
|
||||
logging.info("WIGLE: Internet connectivity detected. Uploading new handshakes to wigle.net")
|
||||
@@ -271,10 +184,8 @@ def on_internet_available(agent):
|
||||
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")
|
||||
reported += no_err_entries
|
||||
REPORT.update(data={'reported': reported})
|
||||
logging.info("WIGLE: Successfuly uploaded %d files", len(no_err_entries))
|
||||
except requests.exceptions.RequestException as re_e:
|
||||
SKIP += no_err_entries
|
||||
|
@@ -1,5 +1,5 @@
|
||||
__author__ = '33197631+dadav@users.noreply.github.com'
|
||||
__version__ = '1.0.0'
|
||||
__version__ = '2.0.1'
|
||||
__name__ = 'wpa-sec'
|
||||
__license__ = 'GPL3'
|
||||
__description__ = 'This plugin automatically uploades handshakes to https://wpa-sec.stanev.org'
|
||||
@@ -7,9 +7,12 @@ __description__ = 'This plugin automatically uploades handshakes to https://wpa-
|
||||
import os
|
||||
import logging
|
||||
import requests
|
||||
from pwnagotchi.utils import StatusFile
|
||||
|
||||
READY = False
|
||||
ALREADY_UPLOADED = None
|
||||
REPORT = StatusFile('/root/.wpa_sec_uploads', data_format='json')
|
||||
OPTIONS = dict()
|
||||
SKIP = list()
|
||||
|
||||
|
||||
def on_loaded():
|
||||
@@ -17,20 +20,11 @@ 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):
|
||||
if 'api_key' not 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
|
||||
|
||||
|
||||
@@ -39,48 +33,51 @@ 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']}
|
||||
cookie = {'key': OPTIONS['api_key']}
|
||||
payload = {'file': file_to_upload}
|
||||
|
||||
try:
|
||||
result = requests.post('https://wpa-sec.stanev.org/?submit',
|
||||
headers=headers,
|
||||
result = requests.post('https://wpa-sec.stanev.org',
|
||||
cookies=cookie,
|
||||
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
|
||||
logging.warning("%s was already submitted.", path)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
raise req_e
|
||||
|
||||
|
||||
def on_internet_available(agent):
|
||||
"""
|
||||
Called in manual mode when there's internet connectivity
|
||||
"""
|
||||
global REPORT
|
||||
global SKIP
|
||||
if READY:
|
||||
config = agent.config()
|
||||
display = agent.view()
|
||||
reported = REPORT.data_field_or('reported', default=list())
|
||||
|
||||
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)
|
||||
handshake_new = set(handshake_paths) - set(reported) - set(SKIP)
|
||||
|
||||
if handshake_new:
|
||||
logging.info("WPA_SEC: Internet connectivity detected.\
|
||||
Uploading new handshakes to wpa-sec.stanev.org")
|
||||
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
|
||||
reported.append(handshake)
|
||||
REPORT.update(data={'reported': reported})
|
||||
logging.info("WPA_SEC: Successfuly uploaded %s", handshake)
|
||||
except requests.exceptions.RequestException as req_e:
|
||||
SKIP.append(handshake)
|
||||
logging.error("WPA_SEC: %s", req_e)
|
||||
continue
|
||||
except OSError as os_e:
|
||||
logging.error(f"WPA_SEC: Got the following error: {os_e}")
|
||||
logging.error("WPA_SEC: %s", os_e)
|
||||
continue
|
||||
|
@@ -1,13 +1,12 @@
|
||||
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
|
||||
import pwnagotchi.ui.hw as hw
|
||||
from pwnagotchi.ui.view import View
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
@@ -88,24 +87,15 @@ class VideoHandler(BaseHTTPRequestHandler):
|
||||
|
||||
class Display(View):
|
||||
def __init__(self, config, state={}):
|
||||
super(Display, self).__init__(config, state)
|
||||
super(Display, self).__init__(config, hw.display_for(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")
|
||||
self.init_display()
|
||||
|
||||
if self._video_enabled:
|
||||
_thread.start_new_thread(self._http_serve, ())
|
||||
@@ -119,133 +109,33 @@ class Display(View):
|
||||
logging.info("could not get ip of usb0, video server not starting")
|
||||
|
||||
def is_inky(self):
|
||||
return self._display_type in ('inkyphat', 'inky')
|
||||
return self._implementation.name == 'inky'
|
||||
|
||||
def is_papirus(self):
|
||||
return self._display_type in ('papirus', 'papi')
|
||||
return self._implementation.name == 'papirus'
|
||||
|
||||
def is_waveshare_v1(self):
|
||||
return self._display_type in ('waveshare_1', 'ws_1', 'waveshare1', 'ws1')
|
||||
return self._implementation.name == 'waveshare_1'
|
||||
|
||||
def is_waveshare_v2(self):
|
||||
return self._display_type in ('waveshare_2', 'ws_2', 'waveshare2', 'ws2')
|
||||
return self._implementation.name == 'waveshare_2'
|
||||
|
||||
def is_oledhat(self):
|
||||
return self._implementation.name == 'oledhat'
|
||||
|
||||
def is_waveshare_any(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():
|
||||
if self._display_color == 'black':
|
||||
logging.info("initializing waveshare v1 display in monochromatic mode")
|
||||
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:
|
||||
logging.info("initializing waveshare v1 display 3-color mode")
|
||||
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
|
||||
|
||||
def init_display(self):
|
||||
if self._enabled:
|
||||
self._implementation.initialize()
|
||||
plugins.on('display_setup', self._implementation)
|
||||
else:
|
||||
logging.critical("unknown display type %s" % self._display_type)
|
||||
|
||||
plugins.on('display_setup', self._display)
|
||||
|
||||
logging.warning("display module is disabled")
|
||||
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_any():
|
||||
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:
|
||||
logging.exception("error while rendering on inky")
|
||||
|
||||
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)
|
||||
self._implementation.clear()
|
||||
|
||||
def image(self):
|
||||
img = None
|
||||
@@ -255,8 +145,7 @@ class Display(View):
|
||||
|
||||
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()
|
||||
if self._implementation is not None:
|
||||
self._implementation.render(self._canvas)
|
||||
|
@@ -1,11 +1,11 @@
|
||||
LOOK_R = '(⌐■_■)'
|
||||
LOOK_L = '(■_■¬)'
|
||||
LOOK_R = '( ⚆_⚆)'
|
||||
LOOK_L = '(☉_☉ )'
|
||||
SLEEP = '(⇀‿‿↼)'
|
||||
SLEEP2 = '(≖‿‿≖)'
|
||||
AWAKE = '(◕‿‿◕)'
|
||||
BORED = '(-__-)'
|
||||
INTENSE = '(°▃▃°)'
|
||||
COOL = '(⊙☁◉┐)'
|
||||
COOL = '(⌐■_■)'
|
||||
HAPPY = '(•‿‿•)'
|
||||
EXCITED = '(ᵔ◡◡ᵔ)'
|
||||
MOTIVATED = '(☼‿‿☼)'
|
||||
|
23
pwnagotchi/ui/hw/__init__.py
Normal file
23
pwnagotchi/ui/hw/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from pwnagotchi.ui.hw.inky import Inky
|
||||
from pwnagotchi.ui.hw.papirus import Papirus
|
||||
from pwnagotchi.ui.hw.oledhat import OledHat
|
||||
from pwnagotchi.ui.hw.waveshare1 import WaveshareV1
|
||||
from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
|
||||
|
||||
|
||||
def display_for(config):
|
||||
# config has been normalized already in utils.load_config
|
||||
if config['ui']['display']['type'] == 'inky':
|
||||
return Inky(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'papirus':
|
||||
return Papirus(config)
|
||||
|
||||
if config['ui']['display']['type'] == 'oledhat':
|
||||
return OledHat(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare_1':
|
||||
return WaveshareV1(config)
|
||||
|
||||
elif config['ui']['display']['type'] == 'waveshare_2':
|
||||
return WaveshareV2(config)
|
40
pwnagotchi/ui/hw/base.py
Normal file
40
pwnagotchi/ui/hw/base.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
|
||||
|
||||
class DisplayImpl(object):
|
||||
def __init__(self, config, name):
|
||||
self.name = name
|
||||
self.config = config['ui']['display']
|
||||
self._layout = {
|
||||
'width': 0,
|
||||
'height': 0,
|
||||
'face': (0, 0),
|
||||
'name': (0, 0),
|
||||
'channel': (0, 0),
|
||||
'aps': (0, 0),
|
||||
'uptime': (0, 0),
|
||||
'line1': (0, 0),
|
||||
'line2': (0, 0),
|
||||
'friend_face': (0, 0),
|
||||
'friend_name': (0, 0),
|
||||
'shakes': (0, 0),
|
||||
'mode': (0, 0),
|
||||
# status is special :D
|
||||
'status': {
|
||||
'pos': (0, 0),
|
||||
'font': fonts.Medium,
|
||||
'max': 20
|
||||
}
|
||||
}
|
||||
|
||||
def layout(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def initialize(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def render(self, canvas):
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError
|
72
pwnagotchi/ui/hw/inky.py
Normal file
72
pwnagotchi/ui/hw/inky.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class Inky(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(Inky, self).__init__(config, 'inky')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 8, 10, 28)
|
||||
self._layout['width'] = 212
|
||||
self._layout['height'] = 104
|
||||
self._layout['face'] = (0, 37)
|
||||
self._layout['name'] = (5, 18)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (25, 0)
|
||||
self._layout['uptime'] = (147, 0)
|
||||
self._layout['line1'] = [0, 12, 212, 12]
|
||||
self._layout['line2'] = [0, 92, 212, 92]
|
||||
self._layout['friend_face'] = (0, 76)
|
||||
self._layout['friend_name'] = (40, 78)
|
||||
self._layout['shakes'] = (0, 93)
|
||||
self._layout['mode'] = (187, 93)
|
||||
self._layout['status'] = {
|
||||
'pos': (102, 18),
|
||||
'font': fonts.Small,
|
||||
'max': 20
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
logging.info("initializing inky display")
|
||||
from pwnagotchi.ui.hw.libs.inkyphat.inkyphatfast import InkyPHATFast
|
||||
self._display = InkyPHATFast(self.config['color'])
|
||||
self._display.set_border(InkyPHATFast.BLACK)
|
||||
|
||||
def render(self, canvas):
|
||||
if self.config['color'] != 'mono':
|
||||
display_colors = 3
|
||||
else:
|
||||
display_colors = 2
|
||||
|
||||
img_buffer = canvas.convert('RGB').convert('P', palette=1, colors=display_colors)
|
||||
if self.config['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.config['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:
|
||||
logging.exception("error while rendering on inky")
|
||||
|
||||
def clear(self):
|
||||
self._display.Clear()
|
24
pwnagotchi/ui/hw/libs/inkyphat/inkyfast.py
Normal file
24
pwnagotchi/ui/hw/libs/inkyphat/inkyfast.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from inky.inky import Inky, CS0_PIN, DC_PIN, RESET_PIN, BUSY_PIN
|
||||
|
||||
|
||||
class InkyFast(Inky):
|
||||
|
||||
def __init__(self, resolution=(400, 300), colour='black', cs_pin=CS0_PIN, dc_pin=DC_PIN, reset_pin=RESET_PIN,
|
||||
busy_pin=BUSY_PIN, h_flip=False, v_flip=False):
|
||||
super(InkyFast, self).__init__(resolution, colour, cs_pin, dc_pin, reset_pin, busy_pin, h_flip, v_flip)
|
||||
|
||||
self._luts['black'] = [
|
||||
0b01001000, 0b10100000, 0b00010000, 0b00010000, 0b00010011, 0b00000000, 0b00000000,
|
||||
0b01001000, 0b10100000, 0b10000000, 0b00000000, 0b00000011, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b01001000, 0b10100101, 0b00000000, 0b10111011, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
# The following timings have been reduced to avoid the fade to black
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x10, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x08, 0x08, 0x10, 0x10,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]
|
27
pwnagotchi/ui/hw/libs/inkyphat/inkyphatfast.py
Normal file
27
pwnagotchi/ui/hw/libs/inkyphat/inkyphatfast.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Inky pHAT e-Ink Display Driver."""
|
||||
from . import inkyfast
|
||||
|
||||
|
||||
class InkyPHATFast(inkyfast.InkyFast):
|
||||
"""Inky wHAT e-Ink Display Driver."""
|
||||
|
||||
WIDTH = 212
|
||||
HEIGHT = 104
|
||||
|
||||
WHITE = 0
|
||||
BLACK = 1
|
||||
RED = 2
|
||||
YELLOW = 2
|
||||
|
||||
def __init__(self, colour):
|
||||
"""Initialise an Inky pHAT Display.
|
||||
|
||||
:param colour: one of red, black or yellow, default: black
|
||||
|
||||
"""
|
||||
inkyfast.InkyFast.__init__(
|
||||
self,
|
||||
resolution=(self.WIDTH, self.HEIGHT),
|
||||
colour=colour,
|
||||
h_flip=False,
|
||||
v_flip=False)
|
@@ -15,7 +15,7 @@
|
||||
|
||||
from PIL import Image
|
||||
from PIL import ImageOps
|
||||
from pwnagotchi.ui.papirus.lm75b import LM75B
|
||||
from pwnagotchi.ui.hw.libs.papirus import LM75B
|
||||
import re
|
||||
import os
|
||||
import sys
|
135
pwnagotchi/ui/hw/libs/waveshare/oledhat/SH1106.py
Normal file
135
pwnagotchi/ui/hw/libs/waveshare/oledhat/SH1106.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from . import config
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
Device_SPI = config.Device_SPI
|
||||
Device_I2C = config.Device_I2C
|
||||
|
||||
LCD_WIDTH = 128 #LCD width
|
||||
LCD_HEIGHT = 64 #LCD height
|
||||
|
||||
class SH1106(object):
|
||||
def __init__(self):
|
||||
self.width = LCD_WIDTH
|
||||
self.height = LCD_HEIGHT
|
||||
#Initialize DC RST pin
|
||||
self._dc = config.DC_PIN
|
||||
self._rst = config.RST_PIN
|
||||
self._bl = config.BL_PIN
|
||||
self.Device = config.Device
|
||||
|
||||
|
||||
""" Write register address and data """
|
||||
def command(self, cmd):
|
||||
if(self.Device == Device_SPI):
|
||||
GPIO.output(self._dc, GPIO.LOW)
|
||||
config.spi_writebyte([cmd])
|
||||
else:
|
||||
config.i2c_writebyte(0x00, cmd)
|
||||
|
||||
# def data(self, val):
|
||||
# GPIO.output(self._dc, GPIO.HIGH)
|
||||
# config.spi_writebyte([val])
|
||||
|
||||
def Init(self):
|
||||
if (config.module_init() != 0):
|
||||
return -1
|
||||
"""Initialize dispaly"""
|
||||
self.reset()
|
||||
self.command(0xAE);#--turn off oled panel
|
||||
self.command(0x02);#---set low column address
|
||||
self.command(0x10);#---set high column address
|
||||
self.command(0x40);#--set start line address Set Mapping RAM Display Start Line (0x00~0x3F)
|
||||
self.command(0x81);#--set contrast control register
|
||||
self.command(0xA0);#--Set SEG/Column Mapping
|
||||
self.command(0xC0);#Set COM/Row Scan Direction
|
||||
self.command(0xA6);#--set normal display
|
||||
self.command(0xA8);#--set multiplex ratio(1 to 64)
|
||||
self.command(0x3F);#--1/64 duty
|
||||
self.command(0xD3);#-set display offset Shift Mapping RAM Counter (0x00~0x3F)
|
||||
self.command(0x00);#-not offset
|
||||
self.command(0xd5);#--set display clock divide ratio/oscillator frequency
|
||||
self.command(0x80);#--set divide ratio, Set Clock as 100 Frames/Sec
|
||||
self.command(0xD9);#--set pre-charge period
|
||||
self.command(0xF1);#Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
|
||||
self.command(0xDA);#--set com pins hardware configuration
|
||||
self.command(0x12);
|
||||
self.command(0xDB);#--set vcomh
|
||||
self.command(0x40);#Set VCOM Deselect Level
|
||||
self.command(0x20);#-Set Page Addressing Mode (0x00/0x01/0x02)
|
||||
self.command(0x02);#
|
||||
self.command(0xA4);# Disable Entire Display On (0xa4/0xa5)
|
||||
self.command(0xA6);# Disable Inverse Display On (0xa6/a7)
|
||||
time.sleep(0.1)
|
||||
self.command(0xAF);#--turn on oled panel
|
||||
|
||||
|
||||
def reset(self):
|
||||
"""Reset the display"""
|
||||
GPIO.output(self._rst,GPIO.HIGH)
|
||||
time.sleep(0.1)
|
||||
GPIO.output(self._rst,GPIO.LOW)
|
||||
time.sleep(0.1)
|
||||
GPIO.output(self._rst,GPIO.HIGH)
|
||||
time.sleep(0.1)
|
||||
|
||||
def getbuffer(self, image):
|
||||
# print "bufsiz = ",(self.width/8) * self.height
|
||||
buf = [0xFF] * ((self.width//8) * self.height)
|
||||
image_monocolor = image.convert('1')
|
||||
imwidth, imheight = image_monocolor.size
|
||||
pixels = image_monocolor.load()
|
||||
# print "imwidth = %d, imheight = %d",imwidth,imheight
|
||||
if(imwidth == self.width and imheight == self.height):
|
||||
#print ("Vertical")
|
||||
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[x + (y // 8) * self.width] &= ~(1 << (y % 8))
|
||||
# print x,y,x + (y * self.width)/8,buf[(x + y * self.width) / 8]
|
||||
|
||||
elif(imwidth == self.height and imheight == self.width):
|
||||
#print ("Vertical")
|
||||
for y in range(imheight):
|
||||
for x in range(imwidth):
|
||||
newx = y
|
||||
newy = self.height - x - 1
|
||||
if pixels[x, y] == 0:
|
||||
buf[(newx + (newy // 8 )*self.width) ] &= ~(1 << (y % 8))
|
||||
return buf
|
||||
|
||||
|
||||
# def ShowImage(self,Image):
|
||||
# self.SetWindows()
|
||||
# GPIO.output(self._dc, GPIO.HIGH);
|
||||
# for i in range(0,self.width * self.height/8):
|
||||
# config.spi_writebyte([~Image[i]])
|
||||
|
||||
def ShowImage(self, pBuf):
|
||||
for page in range(0,8):
|
||||
# set page address #
|
||||
self.command(0xB0 + page);
|
||||
# set low column address #
|
||||
self.command(0x02);
|
||||
# set high column address #
|
||||
self.command(0x10);
|
||||
# write data #
|
||||
time.sleep(0.01)
|
||||
if(self.Device == Device_SPI):
|
||||
GPIO.output(self._dc, GPIO.HIGH);
|
||||
for i in range(0,self.width):#for(int i=0;i<self.width; i++)
|
||||
if(self.Device == Device_SPI):
|
||||
config.spi_writebyte([~pBuf[i + self.width * page]]);
|
||||
else :
|
||||
config.i2c_writebyte(0x40, ~pBuf[i + self.width * page])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Clear contents of image buffer"""
|
||||
_buffer = [0xff]*(self.width * self.height//8)
|
||||
self.ShowImage(_buffer)
|
||||
#print "%d",_buffer[i:i+4096]
|
0
pwnagotchi/ui/hw/libs/waveshare/oledhat/__init__.py
Normal file
0
pwnagotchi/ui/hw/libs/waveshare/oledhat/__init__.py
Normal file
111
pwnagotchi/ui/hw/libs/waveshare/oledhat/config.py
Normal file
111
pwnagotchi/ui/hw/libs/waveshare/oledhat/config.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# /*****************************************************************************
|
||||
# * | File : config.py
|
||||
# * | Author : Waveshare team
|
||||
# * | Function : Hardware underlying interface,for Jetson nano
|
||||
# * | Info :
|
||||
# *----------------
|
||||
# * | This version: V1.0
|
||||
# * | Date : 2019-06-06
|
||||
# * | 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 RPi.GPIO as GPIO
|
||||
import time
|
||||
from smbus import SMBus
|
||||
import spidev
|
||||
|
||||
import ctypes
|
||||
# import spidev
|
||||
|
||||
# Pin definition
|
||||
RST_PIN = 25
|
||||
DC_PIN = 24
|
||||
CS_PIN = 8
|
||||
BL_PIN = 18
|
||||
BUSY_PIN = 18
|
||||
|
||||
Device_SPI = 1
|
||||
Device_I2C = 0
|
||||
|
||||
|
||||
|
||||
if(Device_SPI == 1):
|
||||
Device = Device_SPI
|
||||
spi = spidev.SpiDev(0, 0)
|
||||
else :
|
||||
Device = Device_I2C
|
||||
address = 0x3C
|
||||
bus = SMBus(1)
|
||||
|
||||
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)
|
||||
spi.writebytes([data[0]])
|
||||
|
||||
def i2c_writebyte(reg, value):
|
||||
bus.write_byte_data(address, reg, value)
|
||||
|
||||
# time.sleep(0.01)
|
||||
def module_init():
|
||||
# print("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(BL_PIN, GPIO.OUT)
|
||||
|
||||
|
||||
# SPI.max_speed_hz = 2000000
|
||||
# SPI.mode = 0b00
|
||||
# i2c_writebyte(0xff,0xff)
|
||||
if(Device == Device_SPI):
|
||||
# spi.SYSFS_software_spi_begin()
|
||||
# spi.SYSFS_software_spi_setDataMode(0);
|
||||
# spi.SYSFS_software_spi_setClockDivider(1);
|
||||
spi.max_speed_hz = 2000000
|
||||
spi.mode = 0b00
|
||||
|
||||
GPIO.output(CS_PIN, 0)
|
||||
GPIO.output(BL_PIN, 1)
|
||||
GPIO.output(DC_PIN, 0)
|
||||
return 0
|
||||
|
||||
def module_exit():
|
||||
if(Device == Device_SPI):
|
||||
spi.SYSFS_software_spi_end()
|
||||
else :
|
||||
bus.close()
|
||||
GPIO.output(RST_PIN, 0)
|
||||
GPIO.output(DC_PIN, 0)
|
||||
|
||||
|
||||
|
||||
### END OF FILE ###
|
27
pwnagotchi/ui/hw/libs/waveshare/oledhat/epd.py
Normal file
27
pwnagotchi/ui/hw/libs/waveshare/oledhat/epd.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from . import SH1106
|
||||
from . import config
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 64
|
||||
EPD_HEIGHT = 128
|
||||
|
||||
disp = SH1106.SH1106()
|
||||
|
||||
class EPD(object):
|
||||
|
||||
def __init__(self):
|
||||
self.reset_pin = config.RST_PIN
|
||||
self.dc_pin = config.DC_PIN
|
||||
self.busy_pin = config.BUSY_PIN
|
||||
self.cs_pin = config.CS_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
|
||||
def init(self):
|
||||
disp.Init()
|
||||
|
||||
def Clear(self):
|
||||
disp.clear()
|
||||
|
||||
def display(self, image):
|
||||
disp.ShowImage(disp.getbuffer(image))
|
0
pwnagotchi/ui/hw/libs/waveshare/v1/__init__.py
Normal file
0
pwnagotchi/ui/hw/libs/waveshare/v1/__init__.py
Normal file
@@ -30,7 +30,6 @@
|
||||
|
||||
import logging
|
||||
from . import epdconfig
|
||||
import numpy as np
|
||||
|
||||
# Display resolution
|
||||
EPD_WIDTH = 122
|
@@ -27,9 +27,7 @@
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
import logging
|
||||
from . import epdconfig
|
||||
from PIL import Image
|
||||
import RPi.GPIO as GPIO
|
||||
# import numpy as np
|
||||
|
0
pwnagotchi/ui/hw/libs/waveshare/v2/__init__.py
Normal file
0
pwnagotchi/ui/hw/libs/waveshare/v2/__init__.py
Normal file
45
pwnagotchi/ui/hw/oledhat.py
Normal file
45
pwnagotchi/ui/hw/oledhat.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class OledHat(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(OledHat, self).__init__(config, 'oledhat')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(8, 8, 8, 8)
|
||||
self._layout['width'] = 128
|
||||
self._layout['height'] = 64
|
||||
self._layout['face'] = (0, 32)
|
||||
self._layout['name'] = (0, 10)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (25, 0)
|
||||
self._layout['uptime'] = (65, 0)
|
||||
self._layout['line1'] = [0, 9, 128, 9]
|
||||
self._layout['line2'] = [0, 53, 128, 53]
|
||||
self._layout['friend_face'] = (0, 41)
|
||||
self._layout['friend_name'] = (40, 43)
|
||||
self._layout['shakes'] = (0, 53)
|
||||
self._layout['mode'] = (103, 10)
|
||||
self._layout['status'] = {
|
||||
'pos': (30, 18),
|
||||
'font': fonts.Small,
|
||||
'max': 18
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
logging.info("initializing oledhat display")
|
||||
from pwnagotchi.ui.hw.libs.waveshare.oledhat.epd import EPD
|
||||
self._display = EPD()
|
||||
self._display.init()
|
||||
self._display.Clear()
|
||||
|
||||
def render(self, canvas):
|
||||
self._display.display(canvas)
|
||||
|
||||
def clear(self):
|
||||
self._display.clear()
|
47
pwnagotchi/ui/hw/papirus.py
Normal file
47
pwnagotchi/ui/hw/papirus.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class Papirus(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(Papirus, self).__init__(config, 'papirus')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
fonts.setup(10, 8, 10, 23)
|
||||
self._layout['width'] = 200
|
||||
self._layout['height'] = 96
|
||||
self._layout['face'] = (0, 24)
|
||||
self._layout['name'] = (5, 14)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (25, 0)
|
||||
self._layout['uptime'] = (135, 0)
|
||||
self._layout['line1'] = [0, 11, 200, 11]
|
||||
self._layout['line2'] = [0, 85, 200, 85]
|
||||
self._layout['friend_face'] = (0, 69)
|
||||
self._layout['friend_name'] = (40, 71)
|
||||
self._layout['shakes'] = (0, 86)
|
||||
self._layout['mode'] = (175, 86)
|
||||
self._layout['status'] = {
|
||||
'pos': (85, 14),
|
||||
'font': fonts.Medium,
|
||||
'max': 16
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
logging.info("initializing papirus display")
|
||||
from pwnagotchi.ui.hw.libs.papirus.epd import EPD
|
||||
os.environ['EPD_SIZE'] = '2.0'
|
||||
self._display = EPD()
|
||||
self._display.clear()
|
||||
|
||||
def render(self, canvas):
|
||||
self._display.display(canvas)
|
||||
self._display.partial_update()
|
||||
|
||||
def clear(self):
|
||||
self._display.clear()
|
86
pwnagotchi/ui/hw/waveshare1.py
Normal file
86
pwnagotchi/ui/hw/waveshare1.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class WaveshareV1(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(WaveshareV1, self).__init__(config, 'waveshare_1')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
if self.config['color'] == 'black':
|
||||
fonts.setup(10, 9, 10, 35)
|
||||
self._layout['width'] = 250
|
||||
self._layout['height'] = 122
|
||||
self._layout['face'] = (0, 40)
|
||||
self._layout['name'] = (5, 20)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (28, 0)
|
||||
self._layout['uptime'] = (185, 0)
|
||||
self._layout['line1'] = [0, 14, 250, 14]
|
||||
self._layout['line2'] = [0, 108, 250, 108]
|
||||
self._layout['friend_face'] = (0, 92)
|
||||
self._layout['friend_name'] = (40, 94)
|
||||
self._layout['shakes'] = (0, 109)
|
||||
self._layout['mode'] = (225, 109)
|
||||
self._layout['status'] = {
|
||||
'pos': (125, 20),
|
||||
'font': fonts.Medium,
|
||||
'max': 20
|
||||
}
|
||||
else:
|
||||
fonts.setup(10, 8, 10, 25)
|
||||
self._layout['width'] = 212
|
||||
self._layout['height'] = 104
|
||||
self._layout['face'] = (0, 26)
|
||||
self._layout['name'] = (5, 15)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (28, 0)
|
||||
self._layout['status'] = (91, 15)
|
||||
self._layout['uptime'] = (147, 0)
|
||||
self._layout['line1'] = [0, 12, 212, 12]
|
||||
self._layout['line2'] = [0, 92, 212, 92]
|
||||
self._layout['friend_face'] = (0, 76)
|
||||
self._layout['friend_name'] = (40, 78)
|
||||
self._layout['shakes'] = (0, 93)
|
||||
self._layout['mode'] = (187, 93)
|
||||
self._layout['status'] = {
|
||||
'pos': (125, 20),
|
||||
'font': fonts.Medium,
|
||||
'max': 14
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
if self.config['color'] == 'black':
|
||||
logging.info("initializing waveshare v1 display in monochromatic mode")
|
||||
from pwnagotchi.ui.hw.libs.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)
|
||||
|
||||
else:
|
||||
logging.info("initializing waveshare v1 display 3-color mode")
|
||||
from pwnagotchi.ui.hw.libs.waveshare.v1.epd2in13bc import EPD
|
||||
self._display = EPD()
|
||||
self._display.init()
|
||||
self._display.Clear()
|
||||
|
||||
def render(self, canvas):
|
||||
if self.config['color'] == 'black':
|
||||
buf = self._display.getbuffer(canvas)
|
||||
self._display.display(buf)
|
||||
else:
|
||||
buf_black = self._display.getbuffer(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 clear(self):
|
||||
self._display.Clear(0xff)
|
69
pwnagotchi/ui/hw/waveshare2.py
Normal file
69
pwnagotchi/ui/hw/waveshare2.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
|
||||
import pwnagotchi.ui.fonts as fonts
|
||||
from pwnagotchi.ui.hw.base import DisplayImpl
|
||||
|
||||
|
||||
class WaveshareV2(DisplayImpl):
|
||||
def __init__(self, config):
|
||||
super(WaveshareV2, self).__init__(config, 'waveshare_2')
|
||||
self._display = None
|
||||
|
||||
def layout(self):
|
||||
if self.config['color'] == 'black':
|
||||
fonts.setup(10, 9, 10, 35)
|
||||
self._layout['width'] = 250
|
||||
self._layout['height'] = 122
|
||||
self._layout['face'] = (0, 40)
|
||||
self._layout['name'] = (5, 20)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (28, 0)
|
||||
self._layout['uptime'] = (185, 0)
|
||||
self._layout['line1'] = [0, 14, 250, 14]
|
||||
self._layout['line2'] = [0, 108, 250, 108]
|
||||
self._layout['friend_face'] = (0, 92)
|
||||
self._layout['friend_name'] = (40, 94)
|
||||
self._layout['shakes'] = (0, 109)
|
||||
self._layout['mode'] = (225, 109)
|
||||
self._layout['status'] = {
|
||||
'pos': (125, 20),
|
||||
'font': fonts.Medium,
|
||||
'max': 20
|
||||
}
|
||||
else:
|
||||
fonts.setup(10, 8, 10, 25)
|
||||
self._layout['width'] = 212
|
||||
self._layout['height'] = 104
|
||||
self._layout['face'] = (0, 26)
|
||||
self._layout['name'] = (5, 15)
|
||||
self._layout['channel'] = (0, 0)
|
||||
self._layout['aps'] = (28, 0)
|
||||
self._layout['status'] = (91, 15)
|
||||
self._layout['uptime'] = (147, 0)
|
||||
self._layout['line1'] = [0, 12, 212, 12]
|
||||
self._layout['line2'] = [0, 92, 212, 92]
|
||||
self._layout['friend_face'] = (0, 76)
|
||||
self._layout['friend_name'] = (40, 78)
|
||||
self._layout['shakes'] = (0, 93)
|
||||
self._layout['mode'] = (187, 93)
|
||||
self._layout['status'] = {
|
||||
'pos': (125, 20),
|
||||
'font': fonts.Medium,
|
||||
'max': 14
|
||||
}
|
||||
return self._layout
|
||||
|
||||
def initialize(self):
|
||||
logging.info("initializing waveshare v2 display")
|
||||
from pwnagotchi.ui.hw.libs.waveshare.v2.waveshare import EPD
|
||||
self._display = EPD()
|
||||
self._display.init(self._display.FULL_UPDATE)
|
||||
self._display.Clear(0xff)
|
||||
self._display.init(self._display.PART_UPDATE)
|
||||
|
||||
def render(self, canvas):
|
||||
buf = self._display.getbuffer(canvas)
|
||||
self._display.displayPartial(buf)
|
||||
|
||||
def clear(self):
|
||||
self._display.Clear(0xff)
|
@@ -18,64 +18,8 @@ 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)
|
||||
status_font = fonts.Medium
|
||||
status_max_length = None
|
||||
|
||||
if config['ui']['display']['type'] in ('inky', 'inkyphat'):
|
||||
fonts.setup(10, 8, 10, 28)
|
||||
|
||||
width = 212
|
||||
height = 104
|
||||
face_pos = (0, 37)
|
||||
name_pos = (5, 18)
|
||||
status_pos = (102, 18)
|
||||
status_font = fonts.Small
|
||||
status_max_length = 20
|
||||
|
||||
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))
|
||||
status_font = fonts.Medium
|
||||
status_max_length = (width - status_pos[0]) // 6
|
||||
|
||||
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)
|
||||
status_font = fonts.Medium
|
||||
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))
|
||||
status_font = fonts.Medium
|
||||
status_max_length = (width - status_pos[0]) // 6
|
||||
|
||||
return width, height, face_pos, name_pos, status_pos, status_font, status_max_length
|
||||
|
||||
|
||||
class View(object):
|
||||
def __init__(self, config, state={}):
|
||||
def __init__(self, config, impl, state=None):
|
||||
global ROOT
|
||||
|
||||
self._render_cbs = []
|
||||
@@ -84,53 +28,51 @@ class View(object):
|
||||
self._frozen = False
|
||||
self._lock = Lock()
|
||||
self._voice = Voice(lang=config['main']['lang'])
|
||||
|
||||
self._width, self._height, \
|
||||
face_pos, name_pos, status_pos, status_font, status_max_length = setup_display_specifics(config)
|
||||
|
||||
self._implementation = impl
|
||||
self._layout = impl.layout()
|
||||
self._width = self._layout['width']
|
||||
self._height = self._layout['height']
|
||||
self._state = State(state={
|
||||
'channel': LabeledValue(color=BLACK, label='CH', value='00', position=(0, 0), label_font=fonts.Bold,
|
||||
'channel': LabeledValue(color=BLACK, label='CH', value='00', position=self._layout['channel'],
|
||||
label_font=fonts.Bold,
|
||||
text_font=fonts.Medium),
|
||||
'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=(30, 0), label_font=fonts.Bold,
|
||||
'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=self._layout['aps'],
|
||||
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),
|
||||
'uptime': LabeledValue(color=BLACK, label='UP', value='00:00:00', position=self._layout['uptime'],
|
||||
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),
|
||||
'line1': Line(self._layout['line1'], color=BLACK),
|
||||
'line2': Line(self._layout['line2'], color=BLACK),
|
||||
|
||||
'face': Text(value=faces.SLEEP, position=face_pos, color=BLACK, font=fonts.Huge),
|
||||
'face': Text(value=faces.SLEEP, position=self._layout['face'], 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,
|
||||
'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=BLACK),
|
||||
'friend_name': Text(value=None, position=self._layout['friend_name'], font=fonts.BoldSmall,
|
||||
color=BLACK),
|
||||
|
||||
'name': Text(value='%s>' % 'pwnagotchi', position=name_pos, color=BLACK, font=fonts.Bold),
|
||||
'name': Text(value='%s>' % 'pwnagotchi', position=self._layout['name'], color=BLACK, font=fonts.Bold),
|
||||
|
||||
'status': Text(value=self._voice.default(),
|
||||
position=status_pos,
|
||||
position=self._layout['status']['pos'],
|
||||
color=BLACK,
|
||||
font=status_font,
|
||||
font=self._layout['status']['font'],
|
||||
wrap=True,
|
||||
# the current maximum number of characters per line, assuming each character is 6 pixels wide
|
||||
max_length=status_max_length),
|
||||
max_length=self._layout['status']['max']),
|
||||
|
||||
'shakes': LabeledValue(label='PWND ', value='0 (00)', color=BLACK,
|
||||
position=(0, self._height - int(self._height * .12) + 1), label_font=fonts.Bold,
|
||||
position=self._layout['shakes'], label_font=fonts.Bold,
|
||||
text_font=fonts.Medium),
|
||||
'mode': Text(value='AUTO', position=(self._width - 25, self._height - int(self._height * .12) + 1),
|
||||
'mode': Text(value='AUTO', position=self._layout['mode'],
|
||||
font=fonts.Bold, color=BLACK),
|
||||
})
|
||||
|
||||
for key, value in state.items():
|
||||
self._state.set(key, value)
|
||||
if state:
|
||||
for key, value in state.items():
|
||||
self._state.set(key, value)
|
||||
|
||||
plugins.on('ui_setup', self)
|
||||
|
||||
|
@@ -56,6 +56,28 @@ def load_config(args):
|
||||
if user_config:
|
||||
config = merge_config(user_config, config)
|
||||
|
||||
# the very first step is to normalize the display name so we don't need dozens of if/elif around
|
||||
if config['ui']['display']['type'] in ('inky', 'inkyphat'):
|
||||
config['ui']['display']['type'] = 'inky'
|
||||
|
||||
elif config['ui']['display']['type'] in ('papirus', 'papi'):
|
||||
config['ui']['display']['type'] = 'papirus'
|
||||
|
||||
elif config['ui']['display']['type'] in ('oledhat'):
|
||||
config['ui']['display']['type'] = 'oledhat'
|
||||
|
||||
elif config['ui']['display']['type'] in ('ws_1', 'ws1', 'waveshare_1', 'waveshare1'):
|
||||
config['ui']['display']['type'] = 'waveshare_1'
|
||||
|
||||
elif config['ui']['display']['type'] in ('ws_2', 'ws2', 'waveshare_2', 'waveshare2'):
|
||||
config['ui']['display']['type'] = 'waveshare_2'
|
||||
|
||||
else:
|
||||
print("unsupported display type %s" % config['ui']['display']['type'])
|
||||
exit(1)
|
||||
|
||||
print("Effective Configuration:")
|
||||
print(yaml.dump(config, default_flow_style=False))
|
||||
return config
|
||||
|
||||
|
||||
|
@@ -14,6 +14,9 @@ class Voice:
|
||||
translation.install()
|
||||
self._ = translation.gettext
|
||||
|
||||
def custom(self, s):
|
||||
return s
|
||||
|
||||
def default(self):
|
||||
return self._('ZzzzZZzzzzZzzz')
|
||||
|
||||
@@ -68,8 +71,8 @@ class Voice:
|
||||
|
||||
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())])
|
||||
self._('Hello {name}! Nice to meet you.').format(name=peer.name()),
|
||||
self._('Unit {name} is nearby!').format(name=peer.name())])
|
||||
|
||||
def on_lost_peer(self, peer):
|
||||
return random.choice([
|
||||
|
@@ -10,6 +10,7 @@ TEMP_BACKUP_FOLDER=/tmp/pwnagotchi_backup
|
||||
FILES_TO_BACKUP=(
|
||||
/root/brain.nn
|
||||
/root/brain.json
|
||||
/root/.api-report.json
|
||||
/root/handshakes
|
||||
/etc/pwnagotchi/
|
||||
/etc/hostname
|
||||
|
@@ -4,6 +4,9 @@
|
||||
|
||||
set -eu
|
||||
|
||||
echo "THIS SCRIPT IS DEPRECATED, PLEASE REFER TO THE OFFICIAL DOCUMENTATION AT https://pwnagotchi.ai/contributing/#creating-an-image"
|
||||
exit 1
|
||||
|
||||
REQUIREMENTS=( wget gunzip git dd e2fsck resize2fs parted losetup qemu-system-x86_64 )
|
||||
DEBREQUIREMENTS=( wget gzip git parted qemu-system-x86 qemu-user-static )
|
||||
REPO_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
|
||||
|
Reference in New Issue
Block a user