This commit is contained in:
V0r-T3x
2024-12-01 20:37:33 -05:00
56 changed files with 705 additions and 1288 deletions

View File

@ -1,55 +0,0 @@
name: Publish
on:
workflow_dispatch:
jobs:
build:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: "Raspberry Pi 32-bit"
id: "32bit"
- name: "Raspberry Pi 64-bit"
id: "64bit"
steps:
- uses: actions/checkout@v4
with:
path: publish/build
- name: Extract version from file
id: get_version
run: |
VERSION=$(cut -d "'" -f2 < publish/build/pwnagotchi/_version.py)
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Install qemu dependencies
run: sudo apt update && sudo apt install qemu-user-static qemu-utils xz-utils -y
- name: Build ${{ matrix.name }} img file
run: cd publish/build; ls -la .; pwd; make packer; make ${{ matrix.id }}
- name: Change name of .img.xz to add version
run: |
sudo chown runner:docker "pwnagotchi-${{ matrix.id }}.img"
mv "pwnagotchi-${{ matrix.id }}.img" "pwnagotchi-${{ env.VERSION }}-${{ matrix.id }}.img"
- name: PiShrink
run: |
wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh
sudo mv pishrink.sh /usr/local/bin
sudo pishrink.sh -aZ "pwnagotchi-${{ env.VERSION }}-${{ matrix.id }}.img"
- name: Release
uses: softprops/action-gh-release@v2
with:
prerelease: false
make_latest: true
tag_name: v${{ env.VERSION }}
name: Pwnagotchi v${{ env.VERSION }}
files: pwnagotchi-${{ env.VERSION }}-${{ matrix.id }}.img.xz
generate_release_notes: true

3
.gitignore vendored
View File

@ -1,2 +1 @@
*.pyc
*.pyc

View File

View File

@ -1,36 +0,0 @@
_show_complete()
{
local cur opts node_names all_options opt_line
all_options="
pwnagotchi -h --help -C --config -U --user-config --manual --skip-session --clear --debug --version --print-config --wizard --check-update --donate {plugins,google}
pwnagotchi plugins -h --help {list,install,enable,disable,uninstall,update,upgrade}
pwnagotchi plugins list -i --installed -h --help
pwnagotchi plugins install -h --help
pwnagotchi plugins uninstall -h --help
pwnagotchi plugins enable -h --help
pwnagotchi plugins disable -h --help
pwnagotchi plugins update -h --help
pwnagotchi plugins upgrade -h --help
pwnagotchi google -h --help {login,refresh}
pwnagotchi google login -h --help
pwnagotchi google refresh -h --help
"
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
# shellcheck disable=SC2124
cmd="${COMP_WORDS[@]:0:${#COMP_WORDS[@]}-1}"
opt_line="$(grep -m1 "$cmd" <<<"$all_options")"
if [[ ${cur} == -* ]] ; then
opts="$(echo "$opt_line" | tr ' ' '\n' | awk '/^ *-/{gsub("[^a-zA-Z0-9-]","",$1);print $1}')"
# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
# shellcheck disable=SC2086
opts="$(echo $opt_line | grep -Po '{\K[^}]+' | tr ',' '\n')"
# shellcheck disable=SC2207
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
}
complete -F _show_complete pwnagotchi

View File

@ -1,26 +0,0 @@
# /etc/dphys-swapfile - user settings for dphys-swapfile package
# author Neil Franklin, last modification 2010.05.05
# copyright ETH Zuerich Physics Departement
# use under either modified/non-advertising BSD or GPL license
# this file is sourced with . so full normal sh syntax applies
# the default settings are added as commented out CONF_*=* lines
# where we want the swapfile to be, this is the default
#CONF_SWAPFILE=/var/swap
# set size to absolute value, leaving empty (default) then uses computed value
# you most likely don't want this, unless you have an special disk situation
CONF_SWAPSIZE=2048
# set size to computed value, this times RAM size, dynamically adapts,
# guarantees that there is enough swap without wasting disk space on excess
#CONF_SWAPFACTOR=2
# restrict size (computed and absolute!) to maximally this limit
# can be set to empty for no limit, but beware of filled partitions!
# this is/was a (outdated?) 32bit kernel limit (in MBytes), do not overrun it
# but is also sensible on 64bit to prevent filling /var or even / partition
#CONF_MAXSWAP=2048

View File

@ -1,6 +0,0 @@
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
# Parameters can be specified after the module name.
i2c-dev

View File

@ -1,2 +0,0 @@
allow-hotplug eth0
iface eth0 inet dhcp

View File

@ -1,2 +0,0 @@
auto lo usb0
iface lo inet loopback

View File

@ -1,8 +0,0 @@
allow-hotplug usb0
iface usb0 inet static
address 10.0.0.2
netmask 255.255.255.0
network 10.0.0.0
broadcast 10.0.0.255
gateway 10.0.0.1
metric 101

View File

@ -1,2 +0,0 @@
allow-hotplug wlan0
iface wlan0 inet static

View File

@ -1,13 +0,0 @@
[Unit]
Description=bettercap api.rest service.
Documentation=https://bettercap.org
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/bin/bettercap-launcher
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@ -1,20 +0,0 @@
[Unit]
Description=Bluetooth service
Documentation=man:bluetoothd(8)
ConditionPathIsDirectory=/sys/class/bluetooth
[Service]
Type=dbus
BusName=org.bluez
ExecStart=/usr/libexec/bluetooth/bluetoothd --noplugin=sap,a2dp
NotifyAccess=main
#WatchdogSec=10
#Restart=on-failure
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
LimitNPROC=1
ProtectHome=true
ProtectSystem=full
[Install]
WantedBy=bluetooth.target
Alias=dbus-org.bluez.service

View File

@ -1,19 +0,0 @@
[Unit]
Description=pwnagotchi Deep Reinforcement Learning instrumenting bettercap for WiFI pwning.
Documentation=https://pwnagotchi.org
Wants=network.target
After=pwngrid-peer.service
[Service]
Type=simple
WorkingDirectory=~
ExecStart=/usr/bin/pwnagotchi-launcher
Restart=always
RestartSec=30
TasksMax=infinity
LimitNPROC=infinity
StandardOutput=null
StandardError=null
[Install]
WantedBy=multi-user.target

View File

@ -1,16 +0,0 @@
[Unit]
Description=pwngrid peer service.
Documentation=https://pwnagotchi.org
Wants=network.target
After=bettercap.service
[Service]
Environment=LD_PRELOAD=/usr/local/lib/libpcap.so.1
Environment=LD_LIBRARY_PATH=/usr/local/lib
Type=simple
ExecStart=/usr/local/bin/pwngrid -keys /etc/pwnagotchi -peers /root/peers -address 127.0.0.1:8666 -client-token /root/.api-enrollment.json -wait -log /etc/pwnagotchi/log/pwngrid-peer.log -iface wlan0mon
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@ -1,34 +0,0 @@
#!/bin/sh
_hostname=$(hostname)
_version=$(cut -d"'" -f2 < /usr/local/lib/python3.11/dist-packages/pwnagotchi/_version.py)
echo
echo "(◕‿‿◕) $_hostname"
echo
echo " Hi! I'm a pwnagotchi $_version, please take good care of me!"
echo " Here are some basic things you need to know to raise me properly!"
echo
echo " If you want to change my configuration, use /etc/pwnagotchi/config.toml"
echo " All plugin config files are located in /etc/pwnagotchi/conf.d/"
echo " Read the readme if you want to use gdrivesync plugin!!"
echo
echo " All the configuration options can be found on /etc/pwnagotchi/default.toml,"
echo " but don't change this file because I will recreate it every time I'm restarted!"
echo
echo " I use oPwnGrid as my main API, you can check stats at https://opwngrid.xyz"
echo
echo " I'm managed by systemd. Here are some basic commands."
echo
echo " If you want to know what I'm doing, you can check my logs with the command"
echo " - pwnlog"
echo " - sudo pwnagotchi --wizard, to help set up a config.toml"
echo " - sudo pwnagotchi --version, to check the current version"
echo " - sudo pwnagotchi --donate, to see how you can donate to this project"
echo " - sudo pwnagotchi --check-update, to see if there is a new version available"
echo
echo " If you want to know if I'm running, you can use"
echo " sudo systemctl status pwnagotchi"
echo
echo " You can restart me using"
echo " pwnkill"
echo
echo " You can learn more about me at https://pwnagotchi.org/"

View File

@ -1,15 +0,0 @@
client_config_backend: file
client_config_file: /root/client_secrets.json
client_config:
client_id: <YOUR CLIENT ID>
client_secret: <YOUR CLIENT SECRET>
save_credentials: True
save_credentials_backend: file
save_credentials_file: /root/credentials.json
get_refresh_token: True
oauth_scope:
- https://www.googleapis.com/auth/drive
- https://www.googleapis.com/auth/drive.install

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
source /usr/bin/pwnlib
# we need to decrypt something
if is_crypted_mode; then
while ! is_decrypted; do
echo "Waiting for decryption..."
sleep 1
done
fi
reload_brcm
start_monitor_interface
if is_auto_mode_no_delete; then
/usr/local/bin/bettercap -no-colors -caplet pwnagotchi-auto -iface wlan0mon
else
/usr/local/bin/bettercap -no-colors -caplet pwnagotchi-manual -iface wlan0mon
fi

View File

@ -1,148 +0,0 @@
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import parse_qsl
_HTML_FORM_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Decryption</title>
<style>
body {{ text-align: center; padding: 150px; }}
h1 {{ font-size: 50px; }}
body {{ font: 20px Helvetica, sans-serif; color: #333; }}
article {{ display: block; text-align: center; width: 650px; margin: 0 auto;}}
input {{
padding: 12px 20px;
margin: 8px 0;
box-sizing: border-box;
border: 1px solid #ccc;
}}
input[type=password] {{
width: 75%;
font-size: 24px;
}}
input[type=submit] {{
cursor: pointer;
width: 75%;
}}
input[type=submit]:hover {{
background-color: #d9d9d9;
}}
</style>
</head>
<body>
<article>
<h1>Decryption</h1>
<p>Some of your files are encrypted.</p>
<p>Please provide the decryption password.</p>
<div>
<form action="/set-password" method="POST">
{password_fields}
<input type="submit" value="Submit">
</form>
</div>
</article>
</body>
</html>
"""
POST_RESPONSE = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* Center the loader */
#loader {
position: absolute;
left: 50%;
top: 50%;
z-index: 1;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#myDiv {
display: none;
text-align: center;
}
</style>
<script type="text/javascript">
function checkPwnagotchi() {
var target = 'http://' + document.location.hostname + ':8080/';
var xhr = new XMLHttpRequest();
xhr.open('GET', target);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status == 401) {
window.location.replace(target);
}else{
setTimeout(checkPwnagotchi, 1000);
}
}
};
xhr.send();
}
setTimeout(checkPwnagotchi, 1000);
</script>
</head>
<body style="margin:0;">
<div id="loader"></div>
</body>
</html>
"""
HTML_FORM = None
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(HTML_FORM.encode())
def do_POST(self):
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
for mapping, password in parse_qsl(body.decode('UTF-8')):
with open('/tmp/.pwnagotchi-secret-{}'.format(mapping), 'wt') as pwfile:
pwfile.write(password)
self.send_response(200)
self.end_headers()
self.wfile.write(POST_RESPONSE.encode())
with open('/root/.pwnagotchi-crypted') as crypted_file:
mappings = [line.split()[0] for line in crypted_file.readlines()]
fields = ''.join(['<label for="{m}">Passphrase for {m}:</label>\n<input type="password" id="{m}" name="{m}" value=""><br>'.format(m=m)
for m in mappings])
HTML_FORM = _HTML_FORM_TEMPLATE.format(password_fields=fields)
httpd = HTTPServer(('0.0.0.0', 80), SimpleHTTPRequestHandler)
httpd.serve_forever()

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
sudo /usr/bin/tvservice -o

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
sudo /usr/bin/tvservice -p

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
source /usr/bin/pwnlib
start_monitor_interface

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
source /usr/bin/pwnlib
stop_monitor_interface

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
source /usr/bin/pwnlib
# we need to decrypt something
if is_crypted_mode; then
while ! is_decrypted; do
echo "Waiting for decryption..."
sleep 1
done
fi
if is_auto_mode; then
/opt/.pwn/bin/pwnagotchi
systemctl restart bettercap
else
/opt/.pwn/bin/pwnagotchi --manual
fi

View File

@ -1,176 +0,0 @@
#!/usr/bin/env bash
# reload mod
reload_brcm() {
if ! modprobe -r brcmfmac; then
return 1
fi
sleep 1
if ! modprobe brcmfmac; then
return 1
fi
sleep 2
iw dev wlan0 set power_save off
return 0
}
# starts mon0
start_monitor_interface() {
ifconfig wlan0 up
sleep 3
iw phy "$(iw phy | head -1 | cut -d" " -f2)" interface add wlan0mon type monitor
sleep 2
rfkill unblock all
ifconfig wlan0 down
ifconfig wlan0mon up
iw dev wlan0mon set power_save off
}
# stops mon0
stop_monitor_interface() {
ifconfig wlan0mon down && iw dev wlan0mon del
reload_brcm
ifconfig wlan0 up
}
# returns 0 if the specified network interface is up
is_interface_up() {
if grep -qi 'up' /sys/class/net/"$1"/operstate; then
return 0
fi
return 1
}
# returns 0 if conditions for AUTO mode are met
is_auto_mode() {
# check override file first
if [ -f /root/.pwnagotchi-manual ]; then
# remove the override file if found
rm -rf /root/.pwnagotchi-manual
return 1
fi
# check override file first
if [ -f /root/.pwnagotchi-auto ]; then
# remove the override file if found
rm -rf /root/.pwnagotchi-auto
return 0
fi
# if usb0 is up, we're in MANU
if is_interface_up usb0; then
return 1
fi
# if eth0 is up (for other boards), we're in MANU
if is_interface_up eth0; then
return 0
fi
# no override, but none of the interfaces is up -> AUTO
return 0
}
# returns 0 if conditions for AUTO mode are met
is_auto_mode_no_delete() {
# check override file first
if [ -f /root/.pwnagotchi-manual ]; then
return 1
fi
# check override file first
if [ -f /root/.pwnagotchi-auto ]; then
return 0
fi
# if usb0 is up, we're in MANU
if is_interface_up usb0; then
return 1
fi
# if eth0 is up (for other boards), we're in MANU
if is_interface_up eth0; then
return 0
fi
# no override, but none of the interfaces is up -> AUTO
return 0
}
# check if we need to decrypt something
is_crypted_mode() {
if [ -f /root/.pwnagotchi-crypted ]; then
return 0
fi
return 1
}
# decryption loop
is_decrypted() {
while read -r mapping container mount; do
# mapping = name the device or file will be mapped to
# container = the luks encrypted device or file
# mount = the mountpoint
# fail if not mounted
if ! mountpoint -q "$mount" >/dev/null 2>&1; then
if [ -f /tmp/.pwnagotchi-secret-"$mapping" ]; then
</tmp/.pwnagotchi-secret-"$mapping" read -r SECRET
if ! test -b /dev/disk/by-id/dm-uuid-*"$(cryptsetup luksUUID "$container" | tr -d -)"*; then
if echo -n "$SECRET" | cryptsetup luksOpen -d- "$container" "$mapping" >/dev/null 2>&1; then
echo "Container decrypted!"
fi
fi
if mount /dev/mapper/"$mapping" "$mount" >/dev/null 2>&1; then
echo "Mounted /dev/mapper/$mapping to $mount"
continue
fi
fi
if ! ip -4 addr show wlan0 | grep inet >/dev/null 2>&1; then
>/dev/null 2>&1 ip addr add 192.168.0.10/24 dev wlan0
fi
if ! pgrep -f decryption-webserver >/dev/null 2>&1; then
>/dev/null 2>&1 decryption-webserver &
fi
if ! pgrep wpa_supplicant >/dev/null 2>&1; then
>/tmp/wpa_supplicant.conf cat <<EOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
ap_scan=2
network={
ssid="DECRYPT-ME"
mode=2
key_mgmt=WPA-PSK
psk="pwnagotchi"
frequency=2437
}
EOF
>/dev/null 2>&1 wpa_supplicant -u -s -O -D nl80211 -i wlan0 -c /tmp/wpa_supplicant.conf &
fi
if ! pgrep dnsmasq >/dev/null 2>&1; then
>/dev/null 2>&1 dnsmasq -k -p 53 -h -O "6,192.168.0.10" -A "/#/192.168.0.10" -i wlan0 -K -F 192.168.0.50,192.168.0.60,255.255.255.0,24h &
fi
return 1
fi
done </root/.pwnagotchi-crypted
# overwrite passwords
python3 -c 'print("A"*4096)' | tee /tmp/.pwnagotchi-secret-* >/dev/null
# delete
rm /tmp/.pwnagotchi-secret-*
sync # flush
pkill wpa_supplicant
pkill dnsmasq
pid="$(pgrep -f "decryption-webserver")"
[[ -n "$pid" ]] && kill "$pid"
return 0
}

View File

@ -9,7 +9,6 @@ _name = None
config = None config = None
_cpu_stats = {} _cpu_stats = {}
def set_name(new_name): def set_name(new_name):
if new_name is None: if new_name is None:
return return

View File

@ -2,6 +2,7 @@ import logging
import pwnagotchi.plugins as plugins import pwnagotchi.plugins as plugins
from pwnagotchi.ai.epoch import Epoch from pwnagotchi.ai.epoch import Epoch
import os
# basic mood system # basic mood system
@ -136,7 +137,12 @@ class Automata(object):
self.set_grateful() self.set_grateful()
plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data()) plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
if self._epoch.blind_for % 10 == 2:
logging.info("two blind epochs -> restarting wifi.recon...", self._epoch.blind_for)
self.run('wifi.recon on')
if self._epoch.blind_for and self._epoch.blind_for % 5 == 0:
logging.info("%d epochs without visible access points -> restarting bettercap...", self._epoch.blind_for)
os.system("systemctl restart bettercap")
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']: if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
logging.critical("%d epochs without visible access points -> restarting ...", self._epoch.blind_for) logging.critical("%d epochs without visible access points -> restarting ...", self._epoch.blind_for)
self._restart() self._restart()

2
bin/pwnagotchi → pwnagotchi/cli.py Executable file → Normal file
View File

@ -1,4 +1,3 @@
#!/usr/bin/python3
import logging import logging
import argparse import argparse
import time import time
@ -14,7 +13,6 @@ from pwnagotchi import utils
from pwnagotchi.google import cmd as google_cmd from pwnagotchi.google import cmd as google_cmd
from pwnagotchi.plugins import cmd as plugins_cmd from pwnagotchi.plugins import cmd as plugins_cmd
from pwnagotchi import log from pwnagotchi import log
from pwnagotchi import restart
from pwnagotchi import fs from pwnagotchi import fs
from pwnagotchi.utils import DottedTomlEncoder, parse_version as version_to_tuple from pwnagotchi.utils import DottedTomlEncoder, parse_version as version_to_tuple

View File

@ -174,7 +174,7 @@ class BTNap:
""" """
Wait for device Wait for device
returns device if found None if not returns device if found, None if not
""" """
logging.debug("BT-TETHER: Waiting for device") logging.debug("BT-TETHER: Waiting for device")

View File

@ -0,0 +1,233 @@
import json
import logging
import os
import subprocess
import threading
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
"""
# Android
# Termux:API : https://f-droid.org/en/packages/com.termux.api/
# Termux : https://f-droid.org/en/packages/com.termux/
pkg install termux-api socat bc
-----
#!/data/data/com.termux/files/usr/bin/bash
# Server details
SERVER_IP="192.168.44.45" # IP of the socat receiver
SERVER_PORT="5000" # UDP port to send data to
# Function to calculate checksum
calculate_checksum() {
local sentence="$1"
local checksum=0
# Loop through each character in the sentence
for ((i = 0; i < ${#sentence}; i++)); do
checksum=$((checksum ^ $(printf '%d' "'${sentence:i:1}")))
done
# Return checksum in hexadecimal
printf "%02X" $checksum
}
# Infinite loop to send GPS data
while true; do
# Get location data
LOCATION=$(termux-location -p gps)
# Extract latitude, longitude, altitude, speed, and bearing
LATITUDE=$(echo "$LOCATION" | jq '.latitude')
LONGITUDE=$(echo "$LOCATION" | jq '.longitude')
ALTITUDE=$(echo "$LOCATION" | jq '.altitude')
SPEED=$(echo "$LOCATION" | jq '.speed') # Speed in meters per second
BEARING=$(echo "$LOCATION" | jq '.bearing')
# Convert speed from meters per second to knots and km/h
SPEED_KNOTS=$(echo "$SPEED" | awk '{printf "%.1f", $1 * 1.943844}')
SPEED_KMH=$(echo "$SPEED" | awk '{printf "%.1f", $1 * 3.6}')
# Format latitude and longitude for NMEA
LAT_DEGREES=$(printf "%.0f" "${LATITUDE%.*}")
LAT_MINUTES=$(echo "(${LATITUDE#${LAT_DEGREES}} * 60)" | bc -l)
LAT_DIRECTION=$(if (( $(echo "$LATITUDE >= 0" | bc -l) )); then echo "N"; else echo "S"; fi)
LON_DEGREES=$(printf "%.0f" "${LONGITUDE%.*}")
LON_MINUTES=$(echo "(${LONGITUDE#${LON_DEGREES}} * 60)" | bc -l)
LON_DIRECTION=$(if (( $(echo "$LONGITUDE >= 0" | bc -l) )); then echo "E"; else echo "W"; fi)
# Format the NMEA GGA sentence
RAW_NMEA_GGA="GPGGA,123519,$(printf "%02d%07.4f" ${LAT_DEGREES#-} $LAT_MINUTES),$LAT_DIRECTION,$(printf "%03d%07.4f" ${LON_DEGREES#-} $LON_MINUTES),$LON_DIRECTION,1,08,0.9,$(printf "%.1f" $ALTITUDE),M,46.9,M,,"
CHECKSUM=$(calculate_checksum "$RAW_NMEA_GGA")
NMEA_GGA="\$${RAW_NMEA_GGA}*${CHECKSUM}"
# Format the VTG sentence
RAW_NMEA_VTG="GPVTG,$(printf "%.1f" $BEARING),T,,M,$(printf "%.1f" $SPEED_KNOTS),N,$(printf "%.1f" $SPEED_KMH),K"
CHECKSUM_VTG=$(calculate_checksum "$RAW_NMEA_VTG")
NMEA_VTG="\$${RAW_NMEA_VTG}*${CHECKSUM_VTG}"
# Send data via UDP
echo "$NMEA_GGA"
echo "$NMEA_GGA" | socat - UDP:$SERVER_IP:$SERVER_PORT
#echo "$NMEA_VTG"
#echo "$NMEA_VTG" | socat - UDP:$SERVER_IP:$SERVER_PORT
sleep 1
done
-----
# Pwnagotchi
main.plugins.gps_listener.enabled = true
# packages
sudo apt-get install socat
"""
class GPS(plugins.Plugin):
__author__ = 'https://github.com/krishenriksen'
__version__ = "1.0.0"
__license__ = "GPL3"
__description__ = "Receive GPS coordinates via termux-location and save whenever an handshake is captured."
def __init__(self):
self.listen_ip = self.get_ip_address('bnep0')
self.listen_port = "5000"
self.write_virtual_serial = "/dev/ttyUSB1"
self.read_virtual_serial = "/dev/ttyUSB0"
self.baud_rate = "19200"
self.socat_process = None
self.stop_event = threading.Event()
self.status_lock = threading.Lock()
self.status = '-'
self.socat_thread = threading.Thread(target=self.run_socat)
def get_ip_address(self, interface):
try:
result = subprocess.run(
["ip", "addr", "show", interface],
capture_output=True,
text=True,
check=True
)
for line in result.stdout.split('\n'):
if 'inet ' in line:
ip_address = line.strip().split()[1].split('/')[0]
return ip_address
except subprocess.CalledProcessError:
logging.warning(f"Could not get IP address for interface {interface}")
return None
def set_status(self, status):
with self.status_lock:
self.status = status
def get_status(self):
with self.status_lock:
return self.status
def on_loaded(self):
logging.info("GPS Listener plugin loaded")
self.cleanup_virtual_serial_ports()
self.create_virtual_serial_ports()
self.socat_thread.start()
def cleanup_virtual_serial_ports(self):
if os.path.exists(self.write_virtual_serial):
self.log.info(f"Removing old {self.write_virtual_serial}")
os.remove(self.write_virtual_serial)
if os.path.exists(self.read_virtual_serial):
self.log.info(f"Removing old {self.read_virtual_serial}")
os.remove(self.read_virtual_serial)
def create_virtual_serial_ports(self):
self.socat_process = subprocess.Popen(
["socat", "-d", "-d", f"pty,link={self.write_virtual_serial},mode=777",
f"pty,link={self.read_virtual_serial},mode=777"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def run_socat(self):
while not self.stop_event.is_set():
self.socat_process = subprocess.Popen(
["socat", f"UDP-RECVFROM:{self.listen_port},reuseaddr,bind={self.listen_ip}", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.set_status('C')
with open(self.write_virtual_serial, 'w') as serial_port:
for line in self.socat_process.stdout:
if self.stop_event.is_set():
break
serial_port.write(line)
serial_port.flush() # Ensure the data is written immediately
self.status = 'C'
self.socat_process.wait()
if self.stop_event.is_set():
break
self.set_status('-')
def cleanup(self):
if self.socat_process:
self.socat_process.terminate()
self.socat_process.wait() # Ensure the process is reaped
self.stop_event.set()
self.socat_thread.join()
self.cleanup_virtual_serial_ports()
def on_ready(self, agent):
if os.path.exists(self.read_virtual_serial):
logging.info(
f"enabling bettercap's gps module for {self.read_virtual_serial}"
)
try:
agent.run("gps off")
except Exception:
logging.info(f"bettercap gps module was already off")
pass
agent.run(f"set gps.device {self.read_virtual_serial}")
agent.run(f"set gps.baudrate {self.baud_rate}")
agent.run("gps on")
logging.info(f"bettercap gps module enabled on {self.read_virtual_serial}")
else:
self.set_status('NF')
logging.warning("no GPS detected")
def on_handshake(self, agent, filename, access_point, client_station):
info = agent.session()
coordinates = info["gps"]
gps_filename = filename.replace(".pcap", ".gps.json")
if coordinates and all([
# avoid 0.000... measurements
coordinates["Latitude"], coordinates["Longitude"]
]):
self.set_status('S')
logging.info(f"saving GPS to {gps_filename} ({coordinates})")
with open(gps_filename, "w+t") as fp:
json.dump(coordinates, fp)
else:
logging.warning("not saving GPS. Couldn't find location.")
def on_ui_setup(self, ui):
with ui._lock:
ui.add_element('gps', LabeledValue(color=BLACK, label='GPS', value='-', position=(ui.width() / 2 - 40, 0), label_font=fonts.Bold, text_font=fonts.Medium))
def on_unload(self, ui):
self.cleanup()
with ui._lock:
ui.remove_element('gps')
def on_ui_update(self, ui):
ui.set('gps', self.get_status())

View File

@ -0,0 +1,75 @@
# Witty Pi 4 L3V7
#
import logging
import pwnagotchi.plugins as plugins
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
class UPS:
I2C_MC_ADDRESS = 0x08
I2C_VOLTAGE_IN_I = 1
I2C_VOLTAGE_IN_D = 2
I2C_CURRENT_OUT_I = 5
I2C_CURRENT_OUT_D = 6
I2C_POWER_MODE = 7
def __init__(self):
# only import when the module is loaded and enabled
import smbus
# 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1)
self._bus = smbus.SMBus(1)
def voltage(self):
try:
i = self._bus.read_byte_data(self.I2C_MC_ADDRESS, self.I2C_VOLTAGE_IN_I)
d = self._bus.read_byte_data(self.I2C_MC_ADDRESS, self.I2C_VOLTAGE_IN_D)
return (i + d / 100)
except Exception as e:
logging.info(f"register={i} failed (exception={e})")
return 0.0
def current(self):
try:
i = self._bus.read_byte_data(self.I2C_MC_ADDRESS, self.I2C_CURRENT_OUT_I)
d = self._bus.read_byte_data(self.I2C_MC_ADDRESS, self.I2C_CURRENT_OUT_D)
return (i + d / 100)
except Exception as e:
logging.info(f"register={i} failed (exception={e})")
return 0.0
def capacity(self):
voltage = max(3.1, min(self.voltage(), 4.2)) # Clamp voltage
return round((voltage - 3.1) / (4.2 - 3.1) * 100)
def charging(self):
try:
dc = self._bus.read_byte_data(self.I2C_MC_ADDRESS, self.I2C_POWER_MODE)
return '+' if dc == 0 else '-'
except:
return '-'
class WittyPi(plugins.Plugin):
__author__ = 'https://github.com/krishenriksen'
__version__ = '1.0.0'
__license__ = 'GPL3'
__description__ = 'A plugin that will display battery info from Witty Pi 4 L3V7'
def __init__(self):
self.ups = None
def on_loaded(self):
self.ups = UPS()
logging.info("wittypi plugin loaded.")
def on_ui_setup(self, ui):
ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%', position=(ui.width() / 2 + 15, 0), label_font=fonts.Bold, text_font=fonts.Medium))
def on_unload(self, ui):
with ui._lock:
ui.remove_element('ups')
def on_ui_update(self, ui):
capacity = self.ups.capacity()
charging = self.ups.charging()
ui.set('ups', "%2i%s" % (capacity, charging))

View File

@ -1,12 +1,12 @@
# board GPIO: # board GPIO:
# Key1: # Key1: GPIO 16
# Key2: # Key2: GPIO 20
# Key3: # Key3: GPIO 21
# Key4: # Key4: GPIO 26
# # IR: GPIO 23
# Touch chipset: # Touch chipset:
# HW info: https://argon40.com/products/pod-display-2-8inch # HW info: https://argon40.com/products/pod-display-2-8inch
# HW datasheet: # HW MANUAL: https://cdn.shopify.com/s/files/1/0556/1660/2177/files/ARGON_POD_MANUAL.pdf?v=1668390711%0A
import logging import logging
@ -44,6 +44,9 @@ class ArgonPod(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("Initializing Argon Pod display") logging.info("Initializing Argon Pod display")
logging.info("Available pins for GPIO Buttons: 16, 20, 21, 26")
logging.info("IR available on GPIO 23")
logging.info("Backlight pin available on GPIO 18")
from pwnagotchi.ui.hw.libs.argon.argonpod.ILI9341 import ILI9341 from pwnagotchi.ui.hw.libs.argon.argonpod.ILI9341 import ILI9341
self._display = ILI9341(0, 0, 22, 18) self._display = ILI9341(0, 0, 22, 18)

View File

@ -1,9 +1,7 @@
import logging import logging
import pwnagotchi.ui.fonts as fonts import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.hw.base import DisplayImpl from pwnagotchi.ui.hw.base import DisplayImpl
class DisplayHatMini(DisplayImpl): class DisplayHatMini(DisplayImpl):
def __init__(self, config): def __init__(self, config):
super(DisplayHatMini, self).__init__(config, 'displayhatmini') super(DisplayHatMini, self).__init__(config, 'displayhatmini')
@ -29,11 +27,14 @@ class DisplayHatMini(DisplayImpl):
'font': fonts.status_font(fonts.Medium), 'font': fonts.status_font(fonts.Medium),
'max': 20 'max': 20
} }
return self._layout return self._layout
def initialize(self): def initialize(self):
logging.info("initializing Display Hat Mini") logging.info("Initializing Display Hat Mini")
logging.info("Available pins for GPIO Buttons A/B/X/Y: 5, 6, 16, 24")
logging.info("Available pins for RGB Led: 17, 27, 22")
logging.info("Backlight pin available on GPIO 13")
logging.info("I2C bus available on stemma QT and Breakout Garden headers")
from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789 from pwnagotchi.ui.hw.libs.pimoroni.displayhatmini.ST7789 import ST7789
self._display = ST7789(0,1,9,13) self._display = ST7789(0,1,9,13)

View File

@ -8,7 +8,7 @@
# ui.display.blcolor = "olive" # ui.display.blcolor = "olive"
# #
# Contrast should be between 30-50, default is 40 # Contrast should be between 30-50, default is 40
# Backlight are predefined in the epd.py # Backlight are predefined in the lcd.py
# Available backlight colors: # Available backlight colors:
# white, grey, maroon, red, purple, fuchsia, green, # white, grey, maroon, red, purple, fuchsia, green,
# lime, olive, yellow, navy, blue, teal, aqua # lime, olive, yellow, navy, blue, teal, aqua
@ -48,10 +48,16 @@ class GfxHat(DisplayImpl):
def initialize(self): def initialize(self):
contrast = self._config['contrast'] if 'contrast' in self._config else 40 contrast = self._config['contrast'] if 'contrast' in self._config else 40
blcolor = self._config['blcolor'] if 'blcolor' in self._config else 'OLIVE' blcolor = self._config['blcolor'] if 'blcolor' in self._config else 'OLIVE'
logging.info("initializing Pimoroni GfxHat") logging.info("Initializing Pimoroni GfxHat - Contrast: %d Backlight color: %s" % (contrast, blcolor))
logging.info("initializing Pimoroni GfxHat - Contrast: %d Backlight color: %s" % (contrast, blcolor)) logging.info("Available config options: ui.display.contrast and ui.display.color")
from pwnagotchi.ui.hw.libs.pimoroni.gfxhat.epd import EPD logging.info("Contrast should be between 30-50, default is 40")
self._display = EPD(contrast=contrast) logging.info("Backlight are predefined in the lcd.py")
logging.info("Available backlight colors:")
logging.info("white, grey, maroon, red, purple, fuchsia, green,")
logging.info("lime, olive, yellow, navy, blue, teal, aqua")
logging.info("Touch control work in progress (6 touch buttons with short and long press and LED feedback)")
from pwnagotchi.ui.hw.libs.pimoroni.gfxhat.lcd import LCD
self._display = LCD(contrast=contrast)
self._display.Init(color_name=blcolor) self._display.Init(color_name=blcolor)
self._display.Clear() self._display.Clear()

View File

@ -52,11 +52,11 @@ class I2COled(DisplayImpl):
i2caddr = self._config['i2c_addr'] if 'i2c_addr' in self._config else 0x3C i2caddr = self._config['i2c_addr'] if 'i2c_addr' in self._config else 0x3C
width = self._config['width'] if 'width' in self._config else 128 width = self._config['width'] if 'width' in self._config else 128
height = self._config['height'] if 'height' in self._config else 64 height = self._config['height'] if 'height' in self._config else 64
logging.info("Initializing SSD1306 based %dx%d I2C Oled Display on address 0x%X" % (width, height, i2caddr))
logging.info("Available config options: ui.display.width, ui.display.height and ui.display.i2caddr")
logging.info("initializing %dx%d I2C Oled Display on address 0x%X" % (width, height, i2caddr)) from pwnagotchi.ui.hw.libs.i2coled.oled import OLED
self._display = OLED(address=i2caddr, width=width, height=height)
from pwnagotchi.ui.hw.libs.i2coled.epd import EPD
self._display = EPD(address=i2caddr, width=width, height=height)
self._display.Init() self._display.Init()
self._display.Clear() self._display.Clear()

View File

@ -9,7 +9,7 @@ EPD_HEIGHT = 64
# disp = SSD1306.SSD1306_96_16(96, 16, address=0x3C) # disp = SSD1306.SSD1306_96_16(96, 16, address=0x3C)
# If you change for different resolution, you have to modify the layout in pwnagotchi/ui/hw/i2coled.py # If you change for different resolution, you have to modify the layout in pwnagotchi/ui/hw/i2coled.py
class EPD(object): class OLED(object):
def __init__(self, address=0x3C, width=EPD_WIDTH, height=EPD_HEIGHT): def __init__(self, address=0x3C, width=EPD_WIDTH, height=EPD_HEIGHT):
self.width = width self.width = width

View File

@ -9,7 +9,7 @@ LED_MAP = [2, 1, 0, 5, 4, 3]
def setup(): def setup():
"""Set up the backlight on GFX HAT.""" """Set up the backlight on GFX HAT."""
global _sn3218 global _sn3218
import sn3218 as _sn3218 from . import sn3218 as _sn3218
_sn3218.enable() _sn3218.enable()
_sn3218.enable_leds(0b111111111111111111) _sn3218.enable_leds(0b111111111111111111)

View File

@ -1,55 +0,0 @@
from . import st7567
from . import backlight
CONTRAST = 40
# Define RGB colors
WHITE = (255, 255, 255)
GREY = (255, 255, 255)
MAROON = (128, 0, 0)
RED = (255, 0, 0)
PURPLE = (128, 0, 128)
FUCHSIA = (255, 0, 255)
GREEN = (0, 128, 0)
LIME = (0, 255, 0)
OLIVE = (128, 128, 0)
YELLOW = (255, 255, 0)
NAVY = (0, 0, 128)
BLUE = (0, 0, 255)
TEAL = (0, 128, 128)
AQUA = (0, 255, 255)
# Map color names to RGB values
color_map = {
'WHITE': WHITE,
'GREY' : GREY,
'MAROON': MAROON,
'RED': RED,
'PURPLE': PURPLE,
'FUCHSIA': FUCHSIA,
'GREEN' : GREEN,
'LIME' : LIME,
'OLIVE' : OLIVE,
'YELLOW' : YELLOW,
'NAVY' : NAVY,
'BLUE' : BLUE,
'TEAL' : TEAL,
'AQUA' : AQUA
}
class EPD(object):
def __init__(self, contrast=CONTRAST, blcolor=('OLIVE')):
self.disp = st7567.ST7567()
self.disp.contrast(contrast)
def Init(self, color_name):
self.disp.setup()
blcolor = color_map.get(color_name.upper(), OLIVE) # Default to olive if color not found
backlight.set_all(*blcolor)
backlight.show()
def Clear(self):
self.disp.clear()
def Display(self, image):
self.disp.show(image)

View File

@ -1,57 +1,55 @@
"""Library for the GFX HAT ST7567 SPI LCD.""" from . import st7567
from .st7567 import ST7567 from . import backlight
CONTRAST = 40
st7567 = ST7567() # Define RGB colors
WHITE = (255, 255, 255)
GREY = (255, 255, 255)
MAROON = (128, 0, 0)
RED = (255, 0, 0)
PURPLE = (128, 0, 128)
FUCHSIA = (255, 0, 255)
GREEN = (0, 128, 0)
LIME = (0, 255, 0)
OLIVE = (128, 128, 0)
YELLOW = (255, 255, 0)
NAVY = (0, 0, 128)
BLUE = (0, 0, 255)
TEAL = (0, 128, 128)
AQUA = (0, 255, 255)
dimensions = st7567.dimensions # Map color names to RGB values
color_map = {
'WHITE': WHITE,
'GREY' : GREY,
'MAROON': MAROON,
'RED': RED,
'PURPLE': PURPLE,
'FUCHSIA': FUCHSIA,
'GREEN' : GREEN,
'LIME' : LIME,
'OLIVE' : OLIVE,
'YELLOW' : YELLOW,
'NAVY' : NAVY,
'BLUE' : BLUE,
'TEAL' : TEAL,
'AQUA' : AQUA
}
class LCD(object):
def clear(): def __init__(self, contrast=CONTRAST, blcolor=('OLIVE')):
"""Clear GFX HAT's display buffer.""" self.disp = st7567.ST7567()
st7567.clear() self.disp.contrast(contrast)
def Init(self, color_name):
self.disp.setup()
blcolor = color_map.get(color_name.upper(), OLIVE) # Default to olive if color not found
backlight.set_all(*blcolor)
backlight.show()
def set_pixel(x, y, value): def Clear(self):
"""Set a single pixel in GTX HAT's display buffer. self.disp.clear()
:param x: X position (from 0 to 127) def Display(self, image):
:param y: Y position (from 0 to 63) self.disp.show(image)
:param value: pixel state 1 = On, 0 = Off
"""
st7567.set_pixel(x, y, value)
def show():
"""Update GFX HAT with the current buffer contents."""
st7567.show()
def contrast(value):
"""Change GFX HAT LCD contrast."""
st7567.contrast(value)
def rotation(r=0):
"""Set the display rotation.
:param r: Specify the rotation in degrees: 0, or 180
"""
if r == 0:
st7567.rotated = False
elif r == 180:
st7567.rotated = True
else:
raise ValueError('Rotation must be 0 or 180 degrees')
def get_rotation():
"""Get the display rotation value.
Returns an integer, either 0, or 180
"""
return 180 if st7567.rotated else 0

View File

@ -0,0 +1,214 @@
import sys
import warnings
__version__ = '1.2.7'
I2C_ADDRESS = 0x54
CMD_ENABLE_OUTPUT = 0x00
CMD_SET_PWM_VALUES = 0x01
CMD_ENABLE_LEDS = 0x13
CMD_UPDATE = 0x16
CMD_RESET = 0x17
if sys.version_info < (3, ):
SMBUS = "python-smbus"
else:
SMBUS = "python3-smbus"
# Helper function to avoid exception chaining output in python3.
# use exec to shield newer syntax from older python
if sys.version_info < (3, 3):
def _raise_from_none(exc):
raise exc
else:
exec("def _raise_from_none(exc): raise exc from None")
try:
from smbus import SMBus
except ImportError:
err_string = "This library requires {smbus}\nInstall with: sudo apt install {smbus}".format(smbus=SMBUS)
import_error = ImportError(err_string)
_raise_from_none(import_error)
def i2c_bus_id():
"""
Returns the i2c bus ID.
"""
with open('/proc/cpuinfo') as cpuinfo:
revision = [l[12:-1] for l in cpuinfo if l[:8] == "Revision"][0]
# https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md
return 1 if int(revision, 16) >= 4 else 0
def enable():
"""
Enables output.
"""
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_ENABLE_OUTPUT, [0x01])
def disable():
"""
Disables output.
"""
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_ENABLE_OUTPUT, [0x00])
def reset():
"""
Resets all internal registers.
"""
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_RESET, [0xFF])
def enable_leds(enable_mask):
"""
Enables or disables each LED channel. The first 18 bit values are
used to determine the state of each channel (1=on, 0=off) if fewer
than 18 bits are provided the remaining channels are turned off.
Args:
enable_mask (int): up to 18 bits of data
Raises:
TypeError: if enable_mask is not an integer.
"""
if not isinstance(enable_mask, int):
raise TypeError("enable_mask must be an integer")
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_ENABLE_LEDS,
[enable_mask & 0x3F, (enable_mask >> 6) & 0x3F, (enable_mask >> 12) & 0X3F])
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_UPDATE, [0xFF])
def channel_gamma(channel, gamma_table):
"""
Overrides the gamma table for a single channel.
Args:
channel (int): channel number
gamma_table (list): list of 256 gamma correction values
Raises:
TypeError: if channel is not an integer.
ValueError: if channel is not in the range 0..17.
TypeError: if gamma_table is not a list.
"""
global channel_gamma_table
if not isinstance(channel, int):
raise TypeError("channel must be an integer")
if channel not in range(18):
raise ValueError("channel be an integer in the range 0..17")
if not isinstance(gamma_table, list) or len(gamma_table) != 256:
raise TypeError("gamma_table must be a list of 256 integers")
channel_gamma_table[channel] = gamma_table
def output(values):
"""
Outputs a new set of values to the driver.
Args:
values (list): channel number
Raises:
TypeError: if values is not a list of 18 integers.
"""
if not isinstance(values, list) or len(values) != 18:
raise TypeError("values must be a list of 18 integers")
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_SET_PWM_VALUES, [channel_gamma_table[i][values[i]] for i in range(18)])
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_UPDATE, [0xFF])
def output_raw(values):
"""
Outputs a new set of values to the driver.
Similar to output(), but does not use channel_gamma_table.
Args:
values (list): channel number
Raises:
TypeError: if values is not a list of 18 integers.
"""
# SMBus.write_i2c_block_data does the type check, so we don't have to
if len(values) != 18:
raise TypeError("values must be a list of 18 integers")
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_SET_PWM_VALUES, values)
i2c.write_i2c_block_data(I2C_ADDRESS, CMD_UPDATE, [0xFF])
try:
i2c = SMBus(i2c_bus_id())
except IOError as e:
warntxt="""
###### ###### ###### ###### ###### ###### ###### ######
i2c initialization failed - is i2c enabled on this system?
See https://github.com/pimoroni/sn3218/wiki/missing-i2c
###### ###### ###### ###### ###### ###### ###### ######
"""
warnings.warn(warntxt)
raise(e)
# generate a good default gamma table
default_gamma_table = [int(pow(255, float(i - 1) / 255)) for i in range(256)]
channel_gamma_table = [default_gamma_table] * 18
enable_leds(0b111111111111111111)
def test_cycles():
print("sn3218 test cycles")
import time
import math
# enable output
enable()
enable_leds(0b111111111111111111)
print(">> test enable mask (on/off)")
enable_mask = 0b000000000000000000
output([0x10] * 18)
for i in range(10):
enable_mask = ~enable_mask
enable_leds(enable_mask)
time.sleep(0.15)
print(">> test enable mask (odd/even)")
enable_mask = 0b101010101010101010
output([0x10] * 18)
for i in range(10):
enable_mask = ~enable_mask
enable_leds(enable_mask)
time.sleep(0.15)
print(">> test enable mask (rotate)")
enable_mask = 0b100000100000100000
output([0x10] * 18)
for i in range(10):
enable_mask = ((enable_mask & 0x01) << 18) | enable_mask >> 1
enable_leds(enable_mask)
time.sleep(0.15)
print(">> test gamma gradient")
enable_mask = 0b111111111111111111
enable_leds(enable_mask)
for i in range(256):
output([((j * (256//18)) + (i * (256//18))) % 256 for j in range(18)])
time.sleep(0.01)
print(">> test gamma fade")
enable_mask = 0b111111111111111111
enable_leds(enable_mask)
for i in range(512):
output([int((math.sin(float(i)/64.0) + 1.0) * 128.0)]*18)
time.sleep(0.01)
# turn everything off and disable output
output([0 for i in range(18)])
disable()
if __name__ == "__main__":
test_cycles()

View File

@ -1,6 +1,6 @@
# board GPIO: # board GPIO:
# A: GPIO22 # A: GPIO23
# B: GPIO23 # B: GPIO24
# #
# HW datasheet: https://learn.adafruit.com/adafruit-1-3-color-tft-bonnet-for-raspberry-pi/overview # HW datasheet: https://learn.adafruit.com/adafruit-1-3-color-tft-bonnet-for-raspberry-pi/overview
@ -38,7 +38,10 @@ class MiniPitft(DisplayImpl):
return self._layout return self._layout
def initialize(self): def initialize(self):
logging.info("initializing Adafruit Mini Pi Tft 240x240") logging.info("Initializing Adafruit Mini Pi Tft 240x240")
logging.info("Available pins for GPIO Buttons: 23, 24")
logging.info("Backlight pin available on GPIO 22")
logging.info("I2C bus available on stemma QT header")
from pwnagotchi.ui.hw.libs.adafruit.minipitft.ST7789 import ST7789 from pwnagotchi.ui.hw.libs.adafruit.minipitft.ST7789 import ST7789
self._display = ST7789(0,0,25,22) self._display = ST7789(0,0,25,22)

View File

@ -1,6 +1,6 @@
# board GPIO: # board GPIO:
# A: GPIO22 # A: GPIO23
# B: GPIO23 # B: GPIO24
# #
# HW datasheet: https://learn.adafruit.com/adafruit-1-3-color-tft-bonnet-for-raspberry-pi/overview # HW datasheet: https://learn.adafruit.com/adafruit-1-3-color-tft-bonnet-for-raspberry-pi/overview
@ -39,6 +39,9 @@ class MiniPitft2(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("initializing Adafruit Mini Pi Tft 135x240") logging.info("initializing Adafruit Mini Pi Tft 135x240")
logging.info("Available pins for GPIO Buttons: 23, 24")
logging.info("Backlight pin available on GPIO 22")
logging.info("I2C bus available on stemma QT header")
from pwnagotchi.ui.hw.libs.adafruit.minipitft2.ST7789 import ST7789 from pwnagotchi.ui.hw.libs.adafruit.minipitft2.ST7789 import ST7789
self._display = ST7789(0,0,25,22) self._display = ST7789(0,0,25,22)

View File

@ -45,6 +45,10 @@ class PirateAudio(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("Initializing PirateAudio - display only") logging.info("Initializing PirateAudio - display only")
logging.info("Available pins for GPIO Buttons A/B/X/Y: 5, 6, 16, 20 or 24")
logging.info("refer to the pimoroni site or pinout.xyz")
logging.info("Backlight pin available on GPIO 13")
logging.info("I2S for the DAC available on pins: 18, 19 and 21")
from pwnagotchi.ui.hw.libs.pimoroni.pirateaudio.ST7789 import ST7789 from pwnagotchi.ui.hw.libs.pimoroni.pirateaudio.ST7789 import ST7789
self._display = ST7789(0,1,9,13) self._display = ST7789(0,1,9,13)

View File

@ -50,6 +50,9 @@ class Pitft(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("Initializing adafruit pitft 320x240 screen") logging.info("Initializing adafruit pitft 320x240 screen")
logging.info("Available pins for GPIO Buttons on the 3,2inch: 17, 22, 23, 27")
logging.info("Available pins for GPIO Buttons on the 2,8inch: 26, 13, 12, 6, 5")
logging.info("Backlight pin available on GPIO 18")
from pwnagotchi.ui.hw.libs.adafruit.pitft.ILI9341 import ILI9341 from pwnagotchi.ui.hw.libs.adafruit.pitft.ILI9341 import ILI9341
self._display = ILI9341(0, 0, 25, 18) self._display = ILI9341(0, 0, 25, 18)

View File

@ -44,6 +44,9 @@ class TftBonnet(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("initializing Adafruit Tft Bonnet") logging.info("initializing Adafruit Tft Bonnet")
logging.info("Available pins for GPIO Buttons Up/Down/Left/Right/Center/A/B: 17, 22, 27, 23, 4, 5, 6")
logging.info("Backlight pin available on GPIO 26")
logging.info("I2C bus available on stemma QT header")
from pwnagotchi.ui.hw.libs.adafruit.tftbonnet.ST7789 import ST7789 from pwnagotchi.ui.hw.libs.adafruit.tftbonnet.ST7789 import ST7789
self._display = ST7789(0,0,25,26) self._display = ST7789(0,0,25,26)

View File

@ -49,6 +49,7 @@ class Waveshareoledlcd(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("initializing Waveshare OLED/LCD hat") logging.info("initializing Waveshare OLED/LCD hat")
logging.info("Available pins for GPIO Buttons K1/K2/K3/K4: 4, 17, 23, 24")
from pwnagotchi.ui.hw.libs.waveshare.oled.oledlcd.ST7789 import ST7789 from pwnagotchi.ui.hw.libs.waveshare.oled.oledlcd.ST7789 import ST7789
self._display = ST7789(0,0,22,18) self._display = ST7789(0,0,22,18)

View File

@ -49,6 +49,7 @@ class Waveshareoledlcdvert(DisplayImpl):
def initialize(self): def initialize(self):
logging.info("initializing Waveshare OLED/LCD hat vertical mode") logging.info("initializing Waveshare OLED/LCD hat vertical mode")
logging.info("Available pins for GPIO Buttons K1/K2/K3/K4: 4, 17, 23, 24")
from pwnagotchi.ui.hw.libs.waveshare.oled.oledlcd.ST7789vert import ST7789 from pwnagotchi.ui.hw.libs.waveshare.oled.oledlcd.ST7789vert import ST7789
self._display = ST7789(0,0,22,18) self._display = ST7789(0,0,22,18)

View File

@ -1,4 +1,4 @@
#import _thread # import _thread
import threading import threading
import logging import logging
import random import random
@ -6,7 +6,6 @@ import time
from threading import Lock from threading import Lock
from PIL import ImageDraw from PIL import ImageDraw
from PIL import ImageColor as colors
import pwnagotchi import pwnagotchi
import pwnagotchi.plugins as plugins import pwnagotchi.plugins as plugins
@ -19,105 +18,28 @@ from pwnagotchi.ui.components import *
from pwnagotchi.ui.state import State from pwnagotchi.ui.state import State
from pwnagotchi.voice import Voice from pwnagotchi.voice import Voice
WHITE = 0x00 # white is actually black on jays image WHITE = 0x00 # white is actually black on jays image
BLACK = 0xFF # black is actually white on jays image BLACK = 0xFF # black is actually white on jays image
BACKGROUND_1 = 0
FOREGROUND_1 = 1
BACKGROUND_L = 0
FOREGROUND_L = 255
BACKGROUND_BGR_16 = (0,0,0)
FOREGROUND_BGR_16 = (31,63,31)
BACKGROUND_RGB = (0,0,0)
FOREGROUND_RGB = (255,255,255)
ROOT = None
#1 (1-bit pixels, black and white, stored with one pixel per byte)
#L (8-bit pixels, grayscale)
#P (8-bit pixels, mapped to any other mode using a color palette)
#BGR;16 (5,6,5 bits, for 65k color)
#RGB (3x8-bit pixels, true color)
#RGBA (4x8-bit pixels, true color with transparency mask)
#CMYK (4x8-bit pixels, color separation)
#YCbCr (3x8-bit pixels, color video format)
#self.FOREGROUND is the main color
#self.BACKGROUNDGROUND is the 2ndary color, used for background
class View(object): class View(object):
def __init__(self, config, impl, state=None): def __init__(self, config, impl, state=None):
global ROOT, BLACK, WHITE global ROOT, BLACK, WHITE
#values/code for display color mode
self.mode = '1' # 1 = (1-bit pixels, black and white, stored with one pixel per byte)
if hasattr(impl, 'mode'):
self.mode = impl.mode
match self.mode:
case '1':
self.BACKGROUND = BACKGROUND_1
self.FOREGROUND = FOREGROUND_1
# do stuff is color mode is 1 when View object is created.
case 'L':
self.BACKGROUND = BACKGROUND_L # black 0 to 255
self.FOREGROUND = FOREGROUND_L
# do stuff is color mode is L when View object is created.
case 'P':
pass
# do stuff is color mode is P when View object is created.
case 'BGR;16':
self.BACKGROUND = BACKGROUND_BGR_16 #black tuple
self.FOREGROUND = FOREGROUND_BGR_16 #white tuple
case 'RGB':
self.BACKGROUND = BACKGROUND_RGB #black tuple
self.FOREGROUND = FOREGROUND_RGB #white tuple
# do stuff is color mode is RGB when View object is created.
case 'RGBA':
# do stuff is color mode is RGBA when View object is created.
pass
case 'CMYK':
# do stuff is color mode is CMYK when View object is created.
pass
case 'YCbCr':
# do stuff is color mode is YCbCr when View object is created.
pass
case _:
# do stuff when color mode doesnt exist for display
self.BACKGROUND = BACKGROUND_1
self.FOREGROUND = FOREGROUND_1
self.invert = 0 self.invert = 0
self._black = 0xFF
self._white = 0x00
if 'invert' in config['ui'] and config['ui']['invert'] == True: if 'invert' in config['ui'] and config['ui']['invert'] == True:
logging.debug("INVERT:" + str(config['ui']['invert'])) logging.debug("INVERT BLACK/WHITES:" + str(config['ui']['invert']))
self.invert = 1 self.invert = 1
tmp = self.FOREGROUND BLACK = 0x00
self.FOREGROUND = self.FOREGROUND WHITE = 0xFF
self.FOREGROUND = tmp self._black = 0x00
self._white = 0xFF
# setup faces from the configuration in case the user customized them # setup faces from the configuration in case the user customized them
faces.load_from_config(config['ui']['faces']) faces.load_from_config(config['ui']['faces'])
self._agent = None self._agent = None
self._render_cbs = [] self._render_cbs = []
self._config = config self._config = config
@ -130,40 +52,42 @@ class View(object):
self._width = self._layout['width'] self._width = self._layout['width']
self._height = self._layout['height'] self._height = self._layout['height']
self._state = State(state={ self._state = State(state={
'channel': LabeledValue(color=self.FOREGROUND, label='CH', value='00', position=self._layout['channel'], 'channel': LabeledValue(color=BLACK, label='CH', value='00', position=self._layout['channel'],
label_font=fonts.Bold, label_font=fonts.Bold,
text_font=fonts.Medium), text_font=fonts.Medium),
'aps': LabeledValue(color=self.FOREGROUND, label='APS', value='0 (00)', position=self._layout['aps'], 'aps': LabeledValue(color=BLACK, label='APS', value='0 (00)', position=self._layout['aps'],
label_font=fonts.Bold, label_font=fonts.Bold,
text_font=fonts.Medium), text_font=fonts.Medium),
'uptime': LabeledValue(color=self.FOREGROUND, label='UP', value='00:00:00', position=self._layout['uptime'], 'uptime': LabeledValue(color=BLACK, label='UP', value='00:00:00', position=self._layout['uptime'],
label_font=fonts.Bold, label_font=fonts.Bold,
text_font=fonts.Medium), text_font=fonts.Medium),
'line1': Line(self._layout['line1'], color=self.FOREGROUND), 'line1': Line(self._layout['line1'], color=BLACK),
'line2': Line(self._layout['line2'], color=self.FOREGROUND), 'line2': Line(self._layout['line2'], color=BLACK),
'face': Text(value=faces.SLEEP, position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']), color=self.FOREGROUND, font=fonts.Huge, png=config['ui']['faces']['png']), 'face': Text(value=faces.SLEEP,
position=(config['ui']['faces']['position_x'], config['ui']['faces']['position_y']),
color=BLACK, font=fonts.Huge, png=config['ui']['faces']['png']),
# 'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=self.FOREGROUND), # 'friend_face': Text(value=None, position=self._layout['friend_face'], font=fonts.Bold, color=BLACK),
'friend_name': Text(value=None, position=self._layout['friend_face'], font=fonts.BoldSmall, color=self.FOREGROUND), 'friend_name': Text(value=None, position=self._layout['friend_face'], font=fonts.BoldSmall, color=BLACK),
'name': Text(value='%s>' % 'pwnagotchi', position=self._layout['name'], color=self.FOREGROUND, font=fonts.Bold), 'name': Text(value='%s>' % 'pwnagotchi', position=self._layout['name'], color=BLACK, font=fonts.Bold),
'status': Text(value=self._voice.default(), 'status': Text(value=self._voice.default(),
position=self._layout['status']['pos'], position=self._layout['status']['pos'],
color=self.FOREGROUND, color=BLACK,
font=self._layout['status']['font'], font=self._layout['status']['font'],
wrap=True, wrap=True,
# the current maximum number of characters per line, assuming each character is 6 pixels wide # the current maximum number of characters per line, assuming each character is 6 pixels wide
max_length=self._layout['status']['max']), max_length=self._layout['status']['max']),
'shakes': LabeledValue(label='PWND ', value='0 (00)', color=self.FOREGROUND, 'shakes': LabeledValue(label='PWND ', value='0 (00)', color=BLACK,
position=self._layout['shakes'], label_font=fonts.Bold, position=self._layout['shakes'], label_font=fonts.Bold,
text_font=fonts.Medium), text_font=fonts.Medium),
'mode': Text(value='AUTO', position=self._layout['mode'], 'mode': Text(value='AUTO', position=self._layout['mode'],
font=fonts.Bold, color=self.FOREGROUND), font=fonts.Bold, color=BLACK),
}) })
if state: if state:
@ -173,8 +97,8 @@ class View(object):
plugins.on('ui_setup', self) plugins.on('ui_setup', self)
if config['ui']['fps'] > 0.0: if config['ui']['fps'] > 0.0:
threading.Thread(target=self._refresh_handler, args=(), name="UI Handler", daemon = True).start() threading.Thread(target=self._refresh_handler, args=(), name="UI Handler", daemon=True).start()
self._ignore_changes = () self._ignore_changes = ()
else: else:
logging.warning("ui.fps is 0, the display will only update for major changes") logging.warning("ui.fps is 0, the display will only update for major changes")
@ -189,7 +113,7 @@ class View(object):
self._state.has_element(key) self._state.has_element(key)
def add_element(self, key, elem): def add_element(self, key, elem):
if self.invert is 1 and hasattr(elem, 'color'): if self.invert is 1 and elem.color:
if elem.color == 0xff: if elem.color == 0xff:
elem.color = 0x00 elem.color = 0x00
elif elem.color == 0x00: elif elem.color == 0x00:
@ -250,7 +174,8 @@ class View(object):
self.set('uptime', last_session.duration) self.set('uptime', last_session.duration)
self.set('channel', '-') self.set('channel', '-')
self.set('aps', "%d" % last_session.associated) self.set('aps', "%d" % last_session.associated)
self.set('shakes', '%d (%s)' % (last_session.handshakes, utils.total_unique_handshakes(self._config['bettercap']['handshakes']))) self.set('shakes', '%d (%s)' % (
last_session.handshakes, utils.total_unique_handshakes(self._config['bettercap']['handshakes'])))
self.set_closest_peer(last_session.last_peer, last_session.peers) self.set_closest_peer(last_session.last_peer, last_session.peers)
self.update() self.update()
@ -340,7 +265,7 @@ class View(object):
def wait(self, secs, sleeping=True): def wait(self, secs, sleeping=True):
was_normal = self.is_normal() was_normal = self.is_normal()
part = secs/10.0 part = secs / 10.0
for step in range(0, 10): for step in range(0, 10):
# if we weren't in a normal state before going # if we weren't in a normal state before going
@ -468,13 +393,13 @@ class View(object):
state = self._state state = self._state
changes = state.changes(ignore=self._ignore_changes) changes = state.changes(ignore=self._ignore_changes)
if force or len(changes): if force or len(changes):
self._canvas = Image.new(self.mode, (self._width, self._height), self.BACKGROUND) self._canvas = Image.new('1', (self._width, self._height), self._white)
drawer = ImageDraw.Draw(self._canvas, self.mode) drawer = ImageDraw.Draw(self._canvas)
plugins.on('ui_update', self) plugins.on('ui_update', self)
for key, lv in state.items(): for key, lv in state.items():
#lv is a ui element # lv is a ui element
lv.draw(self._canvas, drawer) lv.draw(self._canvas, drawer)
web.update_frame(self._canvas) web.update_frame(self._canvas)

View File

@ -1,38 +1,18 @@
[build-system] [build-system]
requires = ["setuptools", "wheel"] requires = ["setuptools"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "pwnagotchi" name = "pwnagotchi"
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"PyYAML", "PyYAML", "dbus-python", "file-read-backwards", "flask", "flask-cors",
"dbus-python", "flask-wtf", "gast", "gpiozero", "inky", "numpy", "pycryptodome", "pydrive2", "python-dateutil",
"file-read-backwards", "requests", "rpi-lgpio", "rpi_hardware_pwm", "scapy", "setuptools", "shimmy", "smbus", "smbus2",
"flask", "spidev", "toml", "tweepy", "websockets",
"flask-cors",
"flask-wtf",
"gast",
"gpiozero",
"inky",
"numpy",
"pycryptodome",
"pydrive2",
"python-dateutil",
"requests",
"rpi-lgpio",
"rpi_hardware_pwm",
"scapy",
"setuptools",
"shimmy",
"smbus",
"smbus2",
"spidev",
"toml",
"tweepy",
"websockets",
] ]
requires-python = ">=3.9"
requires-python = ">=3.11"
authors = [ authors = [
{name = "Evilsocket", email = "evilsocket@gmail.com"}, {name = "Evilsocket", email = "evilsocket@gmail.com"},
{name = "Jayofelony", email = "oudshoorn.jeroen@gmail.com"} {name = "Jayofelony", email = "oudshoorn.jeroen@gmail.com"}
@ -44,12 +24,15 @@ description = "(⌐■_■) - Deep Reinforcement Learning instrumenting betterca
readme = "README.md" readme = "README.md"
license = {file = "LICENSE.md"} license = {file = "LICENSE.md"}
classifiers = [ classifiers = [
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.11',
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License (GPL)',
'Environment :: Console', 'Environment :: Console',
] ]
[tool.setuptools.dynamic]
version = {attr = "pwnagotchi.__version__"}
[project.urls] [project.urls]
Homepage = "https://pwnagotchi.org/" Homepage = "https://pwnagotchi.org/"
Documentation = "https://github.com/jayofelony/pwnagotchi/wiki" Documentation = "https://github.com/jayofelony/pwnagotchi/wiki"
@ -59,4 +42,4 @@ Issues = "https://github.com/jayofelony/pwnagotchi/issues"
Download = "https://github.com/jayofelony/pwnagotchi/releases/latest" Download = "https://github.com/jayofelony/pwnagotchi/releases/latest"
[project.scripts] [project.scripts]
pwnagotchi_cli = "bin.pwnagotchi:pwnagotchi_cli" pwnagotchi = "pwnagotchi.cli:pwnagotchi_cli"

View File

@ -1,7 +0,0 @@
#!/usr/bin/env stork -f
version:parser "__version__\\s*=\\s*['\"]([\\d\\.ab]+)[\"']"
version:file "pwnagotchi/_version.py"
version:from_user
git:create_tag $VERSION

View File

@ -1,25 +0,0 @@
PyYAML
dbus-python
file-read-backwards
flask
flask-cors
flask-wtf
gast
gpiozero
inky
numpy
pycryptodome
pydrive2
python-dateutil
requests
rpi-lgpio
rpi_hardware_pwm
scapy
setuptools
shimmy
smbus
smbus2
spidev
toml
tweepy
websockets

View File

@ -32,28 +32,31 @@ OUTPUT=${OUTPUT:-${UNIT_HOSTNAME}-backup-$(date +%s).tgz}
# username to use for ssh # username to use for ssh
UNIT_USERNAME=${UNIT_USERNAME:-pi} UNIT_USERNAME=${UNIT_USERNAME:-pi}
# what to backup # what to backup
FILES_TO_BACKUP="/root/brain.nn \ FILES_TO_BACKUP="
/root/brain.json \ /boot/firmware/cmdline.txt \
/root/.api-report.json \ /boot/firmware/config.txt \
/root/.ssh \ /root/settings.yaml \
/root/.bashrc \ /root/client_secrets.json \
/root/.profile \ /root/.api-report.json \
/root/handshakes \ /root/.ssh \
/root/peers \ /root/.bashrc \
/etc/pwnagotchi/ \ /root/.profile \
/etc/ssh/ \ /root/handshakes \
/var/log/pwnagotchi.log \ /root/peers \
/var/log/pwnagotchi*.gz \ /etc/modprobe.d/g_ether.conf \
/home/pi/.ssh \ /etc/pwnagotchi/ \
/home/pi/.bashrc \ /etc/ssh/ \
/home/pi/.profile \ /etc/pwnagotchi/log/pwnagotchi.log \
/root/.api-report.json \ /etc/pwnagotchi/log/pwnagotchi*.gz \
/root/.auto-update \ /home/pi/.ssh \
/root/.bt-tether* \ /home/pi/.bashrc \
/root/.net_pos_saved \ /home/pi/.profile \
/root/.ohc_uploads \ /root/.api-report.json \
/root/.wigle_uploads \ /root/.auto-update \
/root/.wpa_sec_uploads" /root/.bt-tether* \
/root/.ohc_uploads \
/root/.wigle_uploads \
/root/.wpa_sec_uploads"
ping -c 1 "${UNIT_HOSTNAME}" > /dev/null 2>&1 || { ping -c 1 "${UNIT_HOSTNAME}" > /dev/null 2>&1 || {
echo "@ unit ${UNIT_HOSTNAME} can't be reached, make sure it's connected and a static IP assigned to the USB interface." echo "@ unit ${UNIT_HOSTNAME} can't be reached, make sure it's connected and a static IP assigned to the USB interface."

View File

@ -1,217 +0,0 @@
#!/usr/bin/env python3
import sys
import os
import argparse
import yaml
import toml
sys.path.insert(0,
os.path.join(os.path.dirname(os.path.realpath(__file__)),
'../'))
import pwnagotchi.ui.faces as faces
from pwnagotchi.ui.display import Display
from PIL import Image
class CustomDisplay(Display):
def __init__(self, config, state):
self.last_image = None
super(CustomDisplay, self).__init__(config, state)
def _http_serve(self):
# do nothing
pass
def _on_view_rendered(self, img):
self.last_image = img
def get_image(self):
"""
Return the saved image
"""
return self.last_image
class DummyPeer:
def __init__(self):
self.rssi = -50
@staticmethod
def name():
return "beta"
@staticmethod
def pwnd_run():
return 50
@staticmethod
def pwnd_total():
return 100
@staticmethod
def first_encounter():
return 1
@staticmethod
def face():
return faces.FRIEND
def append_images(images, horizontal=True, xmargin=0, ymargin=0):
w, h = zip(*(i.size for i in images))
if horizontal:
t_w = sum(w)
t_h = max(h)
else:
t_w = max(w)
t_h = sum(h)
result = Image.new('RGB', (t_w, t_h))
x_offset = 0
y_offset = 0
for im in images:
result.paste(im, (x_offset, y_offset))
if horizontal:
x_offset += im.size[0] + xmargin
else:
y_offset += im.size[1] + ymargin
return result
def main():
parser = argparse.ArgumentParser(description="This program emulates\
the pwnagotchi display")
parser.add_argument('--displays', help="Which displays to use.", nargs="+", default=["waveshare_2"])
parser.add_argument('--lang', help="Language to use", default="en")
parser.add_argument('--output', help="Path to output image (PNG)", default="preview.png")
parser.add_argument('--show-peer', dest="showpeer", help="This options will show a dummy peer", action="store_true")
parser.add_argument('--xmargin', help="Add X-Margin", type=int, default=5)
parser.add_argument('--ymargin', help="Add Y-Margin", type=int, default=5)
args = parser.parse_args()
config_template = '''
main:
lang: {lang}
ui:
font:
name: 'DejaVuSansMono'
size_offset: 0
size: 0
fps: 0.3
display:
enabled: false
rotation: 180
color: black
refresh: 30
type: {display}
web:
enabled: true
address: '::'
port: 8080
faces:
look_r: '( ⚆_⚆)'
look_l: '(☉_☉ )'
look_r_happy: '( ◕‿◕)'
look_l_happy: '(◕‿◕ )'
sleep: '(⇀‿‿↼)'
sleep2: '(≖‿‿≖)'
awake: '(◕‿‿◕)'
bored: '(-__-)'
intense: '(°▃▃°)'
cool: '(⌐■_■)'
happy: '(•‿‿•)'
excited: '(ᵔ◡◡ᵔ)'
grateful: '(^‿‿^)'
motivated: '(☼‿‿☼)'
demotivated: '(≖__≖)'
smart: '(✜‿‿✜)'
lonely: '(ب__ب)'
sad: '(╥☁╥ )'
friend: '(♥‿‿♥)'
broken: '(☓‿‿☓)'
debug: '(#__#)'
'''
list_of_displays = list()
for display_type in args.displays:
config = yaml.safe_load(config_template.format(display=display_type,
lang=args.lang))
display = CustomDisplay(config=config, state={'name': f"{display_type}>"})
list_of_displays.append(display)
columns = list()
for display in list_of_displays:
emotions = list()
if args.showpeer:
display.set_closest_peer(DummyPeer(), 10)
display.on_starting()
display.update()
emotions.append(display.get_image())
display.on_ai_ready()
display.update()
emotions.append(display.get_image())
display.on_normal()
display.update()
emotions.append(display.get_image())
display.on_new_peer(DummyPeer())
display.update()
emotions.append(display.get_image())
display.on_lost_peer(DummyPeer())
display.update()
emotions.append(display.get_image())
display.on_free_channel('6')
display.update()
emotions.append(display.get_image())
display.wait(2)
display.update()
emotions.append(display.get_image())
display.on_bored()
display.update()
emotions.append(display.get_image())
display.on_sad()
display.update()
emotions.append(display.get_image())
display.on_motivated(1)
display.update()
emotions.append(display.get_image())
display.on_demotivated(-1)
display.update()
emotions.append(display.get_image())
display.on_excited()
display.update()
emotions.append(display.get_image())
display.on_deauth({'mac': 'DE:AD:BE:EF:CA:FE'})
display.update()
emotions.append(display.get_image())
display.on_miss('test')
display.update()
emotions.append(display.get_image())
display.on_lonely()
display.update()
emotions.append(display.get_image())
display.on_handshakes(1)
display.update()
emotions.append(display.get_image())
display.on_rebooting()
display.update()
emotions.append(display.get_image())
# append them all together (vertical)
columns.append(append_images(emotions, horizontal=False, xmargin=args.xmargin, ymargin=args.ymargin))
# append columns side by side
final_image = append_images(columns, horizontal=True, xmargin=args.xmargin, ymargin=args.ymargin)
final_image.save(args.output, 'PNG')
if __name__ == '__main__':
SystemExit(main())

View File

@ -1,6 +0,0 @@
#!/bin/bash
rm -rf build dist pwnagotchi.egg-info &&
python3 setup.py sdist bdist_wheel &&
clear &&
twine upload dist/*

110
setup.py
View File

@ -1,110 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
from setuptools.command.install import install
import glob
import logging
import os
import re
import shutil
import warnings
import platform
log = logging.getLogger(__name__)
def install_file(source_filename, dest_filename):
# do not overwrite network configuration if it exists already
# https://github.com/evilsocket/pwnagotchi/issues/483
if dest_filename.startswith('/etc/network/interfaces.d/') and os.path.exists(dest_filename):
log.info(f"{dest_filename} exists, skipping ...")
return
elif dest_filename.startswith('/root/') and os.path.exists(dest_filename):
log.info(f"{dest_filename} exists, skipping ...")
return
log.info(f"installing {source_filename} to {dest_filename} ...")
dest_folder = os.path.dirname(dest_filename)
if not os.path.isdir(dest_folder):
os.makedirs(dest_folder)
shutil.copy2(source_filename, dest_filename)
if dest_filename.startswith("/usr/bin/"):
os.chmod(dest_filename, 0o755)
def install_system_files():
data_path = None
if os.stat("apt_packages.txt").st_size != 0:
f = open("apt_packages.txt", "r")
for x in f:
os.system(f"apt-get install -y {x}")
f.close()
setup_path = os.path.dirname(__file__)
data_path = os.path.join(setup_path, "builder/data")
for source_filename in glob.glob("%s/**" % data_path, recursive=True):
if os.path.isfile(source_filename):
dest_filename = source_filename.replace(data_path, '')
install_file(source_filename, dest_filename)
def restart_services():
# reload systemd units
os.system("systemctl daemon-reload")
# for people updating https://github.com/evilsocket/pwnagotchi/pull/551/files
os.system("systemctl enable fstrim.timer")
class CustomInstall(install):
def run(self):
super().run()
if os.geteuid() != 0:
warnings.warn("Not running as root, can't install pwnagotchi system files!")
return
install_system_files()
restart_services()
def version(version_file):
with open(version_file, 'rt') as vf:
version_file_content = vf.read()
version_match = re.search(r"__version__\s*=\s*[\"\']([^\"\']+)", version_file_content)
if version_match:
return version_match.groups()[0]
return None
with open('requirements.txt') as fp:
required = [
line.strip()
for line in fp
if line.strip() and not line.startswith("--")
]
VERSION_FILE = 'pwnagotchi/_version.py'
pwnagotchi_version = version(VERSION_FILE)
setup(name='pwnagotchi',
version=pwnagotchi_version,
description='(⌐■_■) - Deep Reinforcement Learning instrumenting bettercap for WiFI pwning.',
author='evilsocket && the dev team',
author_email='evilsocket@gmail.com',
url='https://pwnagotchi.ai/',
license='GPL',
cmdclass={
"install": CustomInstall,
},
scripts=['bin/pwnagotchi'],
package_data={'pwnagotchi': ['defaults.toml', 'pwnagotchi/defaults.toml', 'locale/*/LC_MESSAGES/*.mo']},
include_package_data=True,
packages=find_packages(),
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Environment :: Console',
])