diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b173709
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+dist: bionic
+language: generic
+env:
+ global:
+ - LANG=C
+ - LC_ALL=C
+before_cache:
+- mountpoint -q $TRAVIS_BUILD_DIR/tmp/mnt && sudo umount -R $TRAVIS_BUILD_DIR/tmp/mnt
+- sudo find $TRAVIS_BUILD_DIR/tmp/ -name '*.img' -delete
+cache:
+ apt: true
+ directories:
+ - tmp/
+before_script:
+- sudo apt-get -y update
+- sudo apt-get -y install qemu-user-static binfmt-support qemu
+- sudo update-binfmts --display
+- unset GOROOT
+script:
+- sudo ./scripts/create_sibling.sh -n pwnagotchi -o pwnagotchi.img -s 4
+- zip -s 2g pwnagotchi.zip pwnagotchi.img
+
+# TODO: deploy!
diff --git a/README.md b/README.md
index 7bb826d..f568794 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,13 @@
# Pwnagotchi
+
+
+
+
+
+
+
+
[Pwnagotchi](https://twitter.com/pwnagotchi) is an "AI" that learns from the WiFi environment and instruments bettercap in order to maximize the WPA key material (any form of handshake that is crackable, 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) captured.

@@ -14,9 +22,9 @@ Multiple units can talk to each other, advertising their own presence using a pa
Depending on the status of the unit, several states and states transitions are configurable and represented on the display as different moods, expressions and sentences.
-If instead you are a boring person, you can disable the AI and have the algorithm run just with the preconfigured default parameters and enjoy a very portable bettercap + webui dedicated hardware.
+If instead you just want to use your own parameters and save battery and CPU cycles, you can disable the AI in `config.yml` and enjoy an automated deauther, WPA handshake sniffer and portable bettercap + webui dedicated hardware.
-**NOTE:** The software **requires bettercap v2.25**.
+**NOTE:** The software **requires at least bettercap v2.25**.

@@ -28,12 +36,22 @@ For hackers to learn reinforcement learning, WiFi networking and have an excuse
**THIS IS STILL ALPHA STAGE SOFTWARE, IF YOU DECIDE TO TRY TO USE IT, YOU ARE ON YOUR OWN, NO SUPPORT WILL BE PROVIDED, NEITHER FOR INSTALLATION OR FOR BUGS**
+However, there's [a Slack channel](https://join.slack.com/t/pwnagotchi/shared_invite/enQtNzc4NzY3MDE2OTAzLTg5NmNmNDJiMDM3ZWFkMWUwN2Y5NDk0Y2JlZWZjODlhMmRhNDZiOGMwYjJhM2UzNzA3YjA5NjJmZGY5NGI5NmI).
### Hardware
- Raspberry Pi Zero W
-- [Waveshare eInk Display](https://www.waveshare.com/2.13inch-e-paper-hat.htm) (optional if you connect to usb0 and point your browser to the web ui, see config.yml)
- A decent power bank (with 1500 mAh you get ~2 hours with AI on)
+#### Display (optional)
+
+The display is optional if you connect to `usb0` (by using the data port on the unit) and point your browser to the web ui (see config.yml).
+
+The supported models are:
+
+- [Waveshare eInk Display (both V1 and V2)](https://www.waveshare.com/2.13inch-e-paper-hat.htm)
+- [Pimoroni Inky pHAT](https://shop.pimoroni.com/products/inky-phat)
+- [PaPiRus eInk Screen](https://uk.pi-supply.com/products/papirus-zero-epaper-screen-phat-pi-zero)
+
### Software
- Raspbian + [nexmon patches](https://re4son-kernel.com/re4son-pi-kernel/) for monitor mode, or any Linux with a monitor mode enabled interface (if you tune config.yml).
@@ -48,13 +66,14 @@ You can use the `scripts/create_sibling.sh` script to create an - ready to flash
usage: ./scripts/create_sibling.sh [OPTIONS]
Options:
- -n # Name of the pwnagotchi (default: pwnagotchi)
- -i # Provide the path of an already downloaded raspbian image
- -o # Name of the img-file (default: pwnagotchi.img)
- -s # Size which should be added to second partition (in Gigabyte) (default: 4)
- -p # Only run provisioning (assumes the image is already mounted)
- -d # Only run dependencies checks
- -h # Show this help
+ -n # Name of the pwnagotchi (default: pwnagotchi)
+ -i # Provide the path of an already downloaded raspbian image
+ -o # Name of the img-file (default: pwnagotchi.img)
+ -s # Size which should be added to second partition (in Gigabyte) (default: 4)
+ -v # Version of raspbian (Supported: latest; default: latest)
+ -p # Only run provisioning (assumes the image is already mounted)
+ -d # Only run dependencies checks
+ -h # Show this help
```
#### Host Connection Share
@@ -63,7 +82,7 @@ If you connect to the unit via `usb0` (thus using the data port), you might want
### UI
-The UI is available either via display if installed, or via http://10.0.0.2:8080/ if you connect to the unit via `usb0` and set a static address on the network interface.
+The UI is available either via display if installed, or via http://pwnagotchi.local:8080/ if you connect to the unit via `usb0` and set a static address on the network interface (change `pwnagotchi` with the hostname of your unit).

@@ -73,6 +92,34 @@ The UI is available either via display if installed, or via http://10.0.0.2:8080
* **PWND**: Number of handshakes captured in this session and number of unique networks we own at least one handshake of, from the beginning.
* **AUTO**: This indicates that the algorithm is running with AI disabled (or still loading), it disappears once the AI dependencies have been bootrapped and the neural network loaded.
+#### Languages
+
+Pwnagotchi is able to speak multiple languages!! Currently supported are:
+
+* **english** (default)
+* german
+
+If you want to add a language use the `language.sh` script.
+If you want to add for example the language **italian** you would type:
+
+```shell
+./scripts/language.sh add it
+# Now make your changes to the file
+# sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
+./scripts/language.sh compile it
+# DONE
+```
+
+If you changed the `voice.py`- File, the translations need an update. Do it like this:
+
+```shell
+./scripts/language.sh update it
+# Now make your changes to the file (changed lines are marked with "fuzzy")
+# sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/it/LC_MESSAGES/voice.po
+./scripts/language.sh compile it
+# DONE
+```
+
### Random Info
- `hostname` sets the unit name.
@@ -81,7 +128,7 @@ The UI is available either via display if installed, or via http://10.0.0.2:8080
- `/var/log/pwnagotchi.log` is your friend.
- if connected to a laptop via usb data port, with internet connectivity shared, magic things will happen.
- checkout the `ui.video` section of the `config.yml` - if you don't want to use a display, you can connect to it with the browser and a cable.
-- If you get `[FAILED] Failed to start Remount Root and Kernel File Systems.` while booting pwnagotchi, make sure
+- If you get `[FAILED] Failed to start Remount Root and Kernel File Systems.` while booting pwnagotchi, make sure
the `PARTUUID`s for `rootfs` and `boot` partitions are the same in `/etc/fstab`. Use `sudo blkid` to find those values when you are using `create_sibling.sh`.
## License
diff --git a/scripts/create_sibling.sh b/scripts/create_sibling.sh
index 6b8ea34..5ae58ef 100755
--- a/scripts/create_sibling.sh
+++ b/scripts/create_sibling.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# based on: https://wiki.debian.org/RaspberryPi/qemu-user-static
## and https://z4ziggy.wordpress.com/2015/05/04/from-bochs-to-chroot/
@@ -16,6 +16,9 @@ PWNI_SIZE="4"
OPT_PROVISION_ONLY=0
OPT_CHECK_DEPS_ONLY=0
OPT_IMAGE_PROVIDED=0
+OPT_RASPBIAN_VERSION='latest'
+
+SUPPORTED_RASPBIAN_VERSIONS=( 'latest' 'buster' 'stretch' )
if [[ "$EUID" -ne 0 ]]; then
echo "Run this script as root!"
@@ -44,9 +47,23 @@ function check_dependencies() {
}
function get_raspbian() {
+ VERSION="$1"
+
+ case "$VERSION" in
+ latest)
+ URL="https://downloads.raspberrypi.org/raspbian_lite_latest"
+ ;;
+ buster)
+ URL="https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-07-12/2019-07-10-raspbian-buster.zip"
+ ;;
+ stretch)
+ URL="https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/2019-04-08-raspbian-stretch.zip"
+ ;;
+ esac
+
echo "[+] Downloading raspbian.zip"
mkdir -p "${TMP_DIR}"
- wget --show-progress -qcO "${TMP_DIR}/raspbian.zip" "https://downloads.raspberrypi.org/raspbian_lite_latest"
+ wget --show-progress -qcO "${TMP_DIR}/raspbian.zip" "$URL"
echo "[+] Unpacking raspbian.zip to raspbian.img"
gunzip -c "${TMP_DIR}/raspbian.zip" > "${TMP_DIR}/raspbian.img"
}
@@ -68,7 +85,7 @@ function setup_raspbian(){
parted -s "$LOOP_PATH" rm 2
parted -s "$LOOP_PATH" mkpart primary "$PART2_START" 100%
echo "[+] Check FS"
- e2fsck -f "${LOOP_PATH}p2"
+ e2fsck -y -f "${LOOP_PATH}p2"
echo "[+] Resize FS"
resize2fs "${LOOP_PATH}p2"
echo "[+] Device is ${LOOP_PATH}"
@@ -82,9 +99,8 @@ function setup_raspbian(){
mount --bind /sys "${MNT_DIR}/sys/"
mount --bind /proc "${MNT_DIR}/proc/"
mount --bind /dev/pts "${MNT_DIR}/dev/pts"
- mount --bind /etc/ssl/certs "${MNT_DIR}/etc/ssl/certs"
- mount --bind /etc/ca-certificates "${MNT_DIR}/etc/ca-certificates"
cp /usr/bin/qemu-arm-static "${MNT_DIR}/usr/bin"
+ cp /etc/resolv.conf "${MNT_DIR}/etc/resolv.conf"
}
function provision_raspbian() {
@@ -94,12 +110,17 @@ function provision_raspbian() {
LANG=C chroot . bin/bash -x </etc/dphys-swapfile
@@ -110,19 +131,32 @@ function provision_raspbian() {
git clone https://github.com/evilsocket/pwnagotchi.git
rsync -aP pwnagotchi/sdcard/boot/* /boot/
rsync -aP pwnagotchi/sdcard/rootfs/* /
+ rm -rf /tmp/pwnagotchi
# configure pwnagotchi
echo -e "$PWNI_NAME" > /etc/hostname
sed -i "s@^127\.0\.0\.1 .*@127.0.0.1 localhost "$PWNI_NAME" "$PWNI_NAME".local@g" /etc/hosts
- sed -i "s@pwnagotchi@$PWNI_NAME@g" /etc/motd
+ sed -i "s@alpha@$PWNI_NAME@g" /etc/motd
chmod +x /etc/rc.local
+ # need armv6l version of tensorflow and opencv-python, not armv7l
+ # PIP_OPTS="--upgrade --only-binary :all: --abi cp37m --platform linux_armv6l --target /usr/lib/python3.7/site-packages/"
+ # pip3 install \$PIP_OPTS opencv-python
+ # Should work for tensorflow too, but BUG: Hash mismatch; therefore:
+ wget -P /root/ -c https://www.piwheels.org/simple/tensorflow/tensorflow-1.13.1-cp37-none-linux_armv6l.whl
+ wget -P /root/ -c https://www.piwheels.org/simple/opencv-python/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl
+ # we need to install these on first raspberry start...
+ sed -i '/startup\.sh/i pip3 install --no-deps --force-reinstall --upgrade /root/tensorflow-1.13.1-cp37-none-linux_armv6l.whl /root/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl && rm /root/tensorflow-1.13.1-cp37-none-linux_armv6l.whl /root/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl && sed -i "/tensorflow/d" /etc/rc.local' /etc/rc.local
+
+ # newer version is broken
+ pip3 install gast==0.2.2
+
/dev/null 2>&1
+ pip3 install --progress-bar off {}
# waveshare
- pip3 install --trusted-host www.piwheels.org spidev RPi.GPIO
+ pip3 install spidev RPi.GPIO
# install bettercap
export GOPATH=/root/go
@@ -134,33 +168,36 @@ function provision_raspbian() {
git clone https://github.com/bettercap/caplets.git
cd caplets
make install
-
- # monstart + monstop
- cat <<"STOP" > /usr/bin/monstop
- #!/bin/bash
- interface=mon0
- ifconfig \${interface} down
- sleep 1
- iw dev \${interface} del
-STOP
-
- cat <<"STOP" > /usr/bin/monstart
- interface=mon0
- echo "Bring up monitor mode interface \${interface}"
- iw phy phy0 interface add \${interface} type monitor
- ifconfig \${interface} up
- if [ \$? -eq 0 ]; then
- echo "started monitor interface on \${interface}"
- fi
-STOP
-
- chmod +x /usr/bin/{monstart,monstop}
+ rm -rf /tmp/caplets
+ cd /root # fixes getcwd error that was bugging me
# Re4son-Kernel
echo "deb http://http.re4son-kernel.com/re4son/ kali-pi main" > /etc/apt/sources.list.d/re4son.list
wget -O - https://re4son-kernel.com/keys/http/archive-key.asc | apt-key add -
apt update
apt install -y kalipi-kernel kalipi-bootloader kalipi-re4son-firmware kalipi-kernel-headers libraspberrypi0 libraspberrypi-dev libraspberrypi-doc libraspberrypi-bin
+
+ # Fix PARTUUID
+ PUUID_ROOT="\$(blkid "\$(df / --output=source | tail -1)" | grep -Po 'PARTUUID="\K[^"]+')"
+ PUUID_BOOT="\$(blkid "\$(df /boot --output=source | tail -1)" | grep -Po 'PARTUUID="\K[^"]+')"
+
+ # sed regex info: search for line containing / followed by whitespace or /boot (second sed)
+ # in this line, search for PARTUUID= followed by letters, numbers or "-"
+ # replace that match with the new PARTUUID
+ sed -i "/\/[ ]\+/s/PARTUUID=[A-Za-z0-9-]\+/PARTUUID=\$PUUID_ROOT/g" /etc/fstab
+ sed -i "/\/boot/s/PARTUUID=[A-Za-z0-9-]\+/PARTUUID=\$PUUID_BOOT/g" /etc/fstab
+
+ sed -i "s/root=[^ ]\+/root=PARTUUID=\${PUUID_ROOT}/g" /boot/cmdline.txt
+
+ # delete keys
+ find /etc/ssh/ -name "ssh_host_*key*" -delete
+
+ # slows down boot
+ systemctl disable apt-daily.timer apt-daily.service apt-daily-upgrade.timer apt-daily-upgrade.service
+
+ # unecessary services
+ systemctl disable triggerhappy bluetooth wpa_supplicant
+
EOF
sed -i'' 's/^#//g' etc/ld.so.preload
cd "${REPO_DIR}"
@@ -175,20 +212,21 @@ function usage() {
usage: $0 [OPTIONS]
Options:
- -n # Name of the pwnagotchi (default: pwnagotchi)
- -i # Provide the path of an already downloaded raspbian image
- -o # Name of the img-file (default: pwnagotchi.img)
- -s # Size which should be added to second partition (in Gigabyte) (default: 4)
- -p # Only run provisioning (assumes the image is already mounted)
- -d # Only run dependencies checks
- -h # Show this help
+ -n # Name of the pwnagotchi (default: pwnagotchi)
+ -i # Provide the path of an already downloaded raspbian image
+ -o # Name of the img-file (default: pwnagotchi.img)
+ -s # Size which should be added to second partition (in Gigabyte) (default: 4)
+ -v # Version of raspbian (Supported: ${SUPPORTED_RASPBIAN_VERSIONS[*]}; default: latest)
+ -p # Only run provisioning (assumes the image is already mounted)
+ -d # Only run dependencies checks
+ -h # Show this help
EOF
exit 0
}
-while getopts ":n:i:o:s:dph" o; do
+while getopts ":n:i:o:s:v:dph" o; do
case "${o}" in
n)
PWNI_NAME="${OPTARG}"
@@ -209,6 +247,13 @@ while getopts ":n:i:o:s:dph" o; do
d)
OPT_CHECK_DEPS_ONLY=1
;;
+ v)
+ if [[ "${SUPPORTED_RASPBIAN_VERSIONS[*]}" =~ ${OPTARG} ]]; then
+ OPT_RASPBIAN_VERSION="${OPTARG}"
+ else
+ usage
+ fi
+ ;;
h)
usage
;;
@@ -232,7 +277,7 @@ check_dependencies
if [[ "$OPT_IMAGE_PROVIDED" -eq 1 ]]; then
provide_raspbian
else
- get_raspbian
+ get_raspbian "$OPT_RASPBIAN_VERSION"
fi
setup_raspbian
diff --git a/scripts/language.sh b/scripts/language.sh
new file mode 100755
index 0000000..c56b784
--- /dev/null
+++ b/scripts/language.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+set -eu
+
+DEPENDENCIES=( 'xgettext' 'msgfmt' 'msgmerge' )
+COMMANDS=( 'add' 'update' 'delete' 'compile' )
+
+REPO_DIR="$(dirname "$(dirname "$(realpath "$0")")")"
+LOCALE_DIR="${REPO_DIR}/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale"
+VOICE_FILE="${REPO_DIR}/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/voice.py"
+
+function usage() {
+cat < [options]
+
+ Commands:
+ add
+ delete
+ compile
+ update
+
+EOF
+}
+
+for REQ in "${DEPENDENCIES[@]}"; do
+ if ! type "$REQ" >/dev/null 2>&1; then
+ echo "Dependency check failed for ${REQ}"
+ exit 1
+ fi
+done
+
+
+if [[ ! "${COMMANDS[*]}" =~ $1 ]]; then
+ usage
+fi
+
+
+function add_lang() {
+ mkdir -p "$LOCALE_DIR/$1/LC_MESSAGES"
+ cp -n "$LOCALE_DIR/voice.pot" "$LOCALE_DIR/$1/LC_MESSAGES/voice.po"
+}
+
+function del_lang() {
+ # set -eu is present; so not dangerous
+ rm -rf "$LOCALE_DIR/$1"
+}
+
+function comp_lang() {
+ msgfmt -o "$LOCALE_DIR/$1/LC_MESSAGES/voice.mo" "$LOCALE_DIR/$1/LC_MESSAGES/voice.po"
+}
+
+function update_lang() {
+ xgettext -d voice -o "$LOCALE_DIR/voice.pot" "$VOICE_FILE"
+ msgmerge --update "$LOCALE_DIR/$1/LC_MESSAGES/voice.po" "$LOCALE_DIR/voice.pot"
+}
+
+case "$1" in
+ add)
+ add_lang "$2"
+ ;;
+ delete)
+ del_lang "$2"
+ ;;
+ compile)
+ comp_lang "$2"
+ ;;
+ update)
+ update_lang "$2"
+ ;;
+esac
diff --git a/scripts/linux_connection_share.sh b/scripts/linux_connection_share.sh
index 57fe718..a926255 100755
--- a/scripts/linux_connection_share.sh
+++ b/scripts/linux_connection_share.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# name of the ethernet gadget interface on the host
USB_IFACE=${1:-enp0s20f0u1}
@@ -7,12 +7,12 @@ USB_IFACE_NET=10.0.0.0/24
# host interface to use for upstream connection
UPSTREAM_IFACE=${2:-enxe4b97aa99867}
-ip addr add $USB_IFACE_IP/24 dev $USB_IFACE
-ip link set $USB_IFACE up
+ip addr add "$USB_IFACE_IP/24" dev "$USB_IFACE"
+ip link set "$USB_IFACE" up
-iptables -A FORWARD -o $UPSTREAM_IFACE -i $USB_IFACE -s $USB_IFACE_NET -m conntrack --ctstate NEW -j ACCEPT
+iptables -A FORWARD -o "$UPSTREAM_IFACE" -i "$USB_IFACE" -s "$USB_IFACE_NET" -m conntrack --ctstate NEW -j ACCEPT
iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -F POSTROUTING
-iptables -t nat -A POSTROUTING -o $UPSTREAM_IFACE -j MASQUERADE
+iptables -t nat -A POSTROUTING -o "$UPSTREAM_IFACE" -j MASQUERADE
echo 1 > /proc/sys/net/ipv4/ip_forward
diff --git a/sdcard/boot/config.txt b/sdcard/boot/config.txt
index 98231bc..4d69af8 100755
--- a/sdcard/boot/config.txt
+++ b/sdcard/boot/config.txt
@@ -1195,3 +1195,8 @@ dtoverlay=dwc2
dtparam=spi=on
dtoverlay=spi1-3cs
+# Disable bluetooth
+dtoverlay=pi3-disable-bt
+# Disable audio
+dtparam=audio=off
+
diff --git a/sdcard/rootfs/etc/network/interfaces b/sdcard/rootfs/etc/network/interfaces
index 1d8ee6b..2c84f3c 100644
--- a/sdcard/rootfs/etc/network/interfaces
+++ b/sdcard/rootfs/etc/network/interfaces
@@ -5,6 +5,9 @@ iface lo inet loopback
allow-hotplug wlan0
iface wlan0 inet static
+allow-hotplug eth0
+iface eth0 inet dhcp
+
allow-hotplug usb0
iface usb0 inet static
address 10.0.0.2
diff --git a/sdcard/rootfs/etc/rc.local b/sdcard/rootfs/etc/rc.local
index 15fb894..e93d2d8 100755
--- a/sdcard/rootfs/etc/rc.local
+++ b/sdcard/rootfs/etc/rc.local
@@ -10,5 +10,7 @@
# bits.
#
# By default this script does nothing.
+# Powersave (Disable HDMI) ~30ma
+/opt/vc/bin/tvservice -o
/root/pwnagotchi/scripts/startup.sh &
exit 0
diff --git a/sdcard/rootfs/root/pwnagotchi/config.yml b/sdcard/rootfs/root/pwnagotchi/config.yml
index 540b872..1fb846e 100644
--- a/sdcard/rootfs/root/pwnagotchi/config.yml
+++ b/sdcard/rootfs/root/pwnagotchi/config.yml
@@ -1,5 +1,7 @@
# main algorithm configuration
main:
+ # currently implemented: en (default), de
+ lang: en
# monitor interface to use
iface: mon0
# command to run to bring the mon interface up in case it's not up already
@@ -11,13 +13,11 @@ main:
# if true, will not restart the wifi module
no_restart: false
# access points to ignore
- whitelist:
- - Casa-2.4
- - LOTS_OF_MALWARE
+ whitelist: []
# if not null, filter access points by this regular expression
filter: null
# cryptographic key for identity
- pubkey: /etc/ssh/ssh_host_rsa_key.pub
+ pubkey: /etc/ssh/ssh_host_rsa_key.pub
ai:
# if false, only the default 'personality' will be used
@@ -36,7 +36,7 @@ ai:
vf_coef: 0.25
# entropy coefficient for the loss calculation
ent_coef: 0.01
- # maximum value for the gradient clipping
+ # maximum value for the gradient clipping
max_grad_norm: 0.5
# the learning rate
learning_rate: 0.0010
@@ -83,7 +83,7 @@ personality:
# number of active epochs that triggers the excited state
excited_num_epochs: 10
# number of inactive epochs that triggers the bored state
- bored_num_epochs: 15
+ bored_num_epochs: 15
# number of inactive epochs that triggers the sad state
sad_num_epochs: 25
@@ -94,6 +94,12 @@ ui:
display:
enabled: true
rotation: 180
+ # Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2
+ type: 'waveshare_2'
+ # Possible options red/yellow/black (black used for monocromatic displays)
+ color: 'black'
+ # How often to do a full refresh 0 all the time, -1 never
+ refresh: 50
video:
enabled: true
address: '10.0.0.2'
@@ -115,7 +121,7 @@ bettercap:
port: 8081
username: user
password: pass
- # folder where bettercap stores the WPA handshakes, given that
+ # folder where bettercap stores the WPA handshakes, given that
# wifi.handshakes.aggregate will be set to false and individual
# pcap files will be created in order to minimize the chances
# of a single pcap file to get corrupted
diff --git a/sdcard/rootfs/root/pwnagotchi/data/screenrc.auto b/sdcard/rootfs/root/pwnagotchi/data/screenrc.auto
index 0d775f9..9319672 100644
--- a/sdcard/rootfs/root/pwnagotchi/data/screenrc.auto
+++ b/sdcard/rootfs/root/pwnagotchi/data/screenrc.auto
@@ -5,6 +5,7 @@ defscrollback 1024
startup_message off
altscreen on
autodetach on
+zombie kr
activity "activity in %n (%t)"
bell_msg "bell in %n (%t)"
diff --git a/sdcard/rootfs/root/pwnagotchi/data/screenrc.manual b/sdcard/rootfs/root/pwnagotchi/data/screenrc.manual
index 4e66adf..1d62528 100644
--- a/sdcard/rootfs/root/pwnagotchi/data/screenrc.manual
+++ b/sdcard/rootfs/root/pwnagotchi/data/screenrc.manual
@@ -5,6 +5,7 @@ defscrollback 1024
startup_message off
altscreen on
autodetach on
+zombie kr
activity "activity in %n (%t)"
bell_msg "bell in %n (%t)"
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh b/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh
index 48345ef..48673dc 100755
--- a/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/blink.sh
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
-for i in `seq 1 $1`;
+for i in $(seq 1 "$1");
do
echo 0 >/sys/class/leds/led0/brightness
sleep 0.3
@@ -10,3 +10,8 @@ done
echo 0 >/sys/class/leds/led0/brightness
sleep 0.3
+
+# Powersave options
+# Disable power LED ~30ma
+echo none >/sys/class/leds/led0/trigger
+echo 1 >/sys/class/leds/led0/brightness
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/main.py b/sdcard/rootfs/root/pwnagotchi/scripts/main.py
index 4aeb781..de44012 100755
--- a/sdcard/rootfs/root/pwnagotchi/scripts/main.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/main.py
@@ -8,7 +8,7 @@ import core
import pwnagotchi
from pwnagotchi.log import SessionParser
-import pwnagotchi.voice as voice
+from pwnagotchi.voice import Voice
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
@@ -62,7 +62,7 @@ if args.do_manual:
core.log("detected a new session and internet connectivity!")
- picture = '/tmp/pwnagotchi.png'
+ picture = '/dev/shm/pwnagotchi.png'
display.update()
display.image().save(picture, 'png')
@@ -74,7 +74,7 @@ if args.do_manual:
auth.set_access_token(config['twitter']['access_token_key'], config['twitter']['access_token_secret'])
api = tweepy.API(auth)
- tweet = voice.on_log_tweet(log)
+ tweet = Voice(lang=config['main']['lang']).on_log_tweet(log)
api.update_with_media(filename=picture, status=tweet)
log.save_session_id()
@@ -121,9 +121,9 @@ while True:
# An interesting effect of this:
#
# From Pwnagotchi's perspective, the more new access points
- # and / or client stations nearby, the longer one epoch of
+ # and / or client stations nearby, the longer one epoch of
# its relative time will take ... basically, in Pwnagotchi's universe,
- # WiFi electromagnetic fields affect time like gravitational fields
+ # WiFi electromagnetic fields affect time like gravitational fields
# affect ours ... neat ^_^
agent.next_epoch()
except Exception as e:
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py
index f081681..97e9a6d 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ai/train.py
@@ -54,7 +54,7 @@ class Stats(object):
def load(self):
with self._lock:
- if os.path.exists(self.path):
+ if os.path.exists(self.path) and os.path.getsize(self.path) > 0:
core.log("[ai] loading %s" % self.path)
with open(self.path, 'rt') as fp:
obj = json.load(fp)
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000..dcede01
Binary files /dev/null and b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.mo differ
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..c85e254
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/de/LC_MESSAGES/voice.po
@@ -0,0 +1,344 @@
+# 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 , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-29 13:34+0200\n"
+"PO-Revision-Date: 2019-09-29 14:00+0200\n"
+"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
+"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: voice.py:16
+msgid "ZzzzZZzzzzZzzz"
+msgstr ""
+
+#: voice.py:21
+msgid ""
+"Hi, I'm Pwnagotchi!\n"
+"Starting ..."
+msgstr ""
+"Hi, ich bin\n"
+"ein Pwnagotchi!\n"
+"Starte ..."
+
+#: voice.py:22
+msgid ""
+"New day, new hunt,\n"
+"new pwns!"
+msgstr ""
+"Neuer Tag, neue Jagd,\n"
+"neue Pwns!"
+
+#: voice.py:23
+msgid "Hack the Planet!"
+msgstr "Hack den Planet!"
+
+#: voice.py:28
+msgid "AI ready."
+msgstr "KI bereit."
+
+#: voice.py:29
+msgid ""
+"The neural network\n"
+"is ready."
+msgstr ""
+"Das neurale Netz\n"
+"ist bereit."
+
+#: voice.py:39
+#, python-brace-format
+msgid ""
+"Hey, channel {channel} is\n"
+"free! Your AP will\n"
+"say thanks."
+msgstr ""
+"Hey, Channel {channel} ist\n"
+"frei! Dein AP wird\n"
+"es dir danken."
+
+#: voice.py:44
+msgid "I'm bored ..."
+msgstr "Mir ist langweilig..."
+
+#: voice.py:45
+msgid "Let's go for a walk!"
+msgstr "Lass uns laufen gehen!"
+
+#: voice.py:49
+msgid ""
+"This is the best\n"
+"day of my life!"
+msgstr ""
+"Das ist der beste\n"
+"Tag meines Lebens."
+
+#: voice.py:53
+msgid "Shitty day :/"
+msgstr "Scheis Tag :/"
+
+#: voice.py:58
+msgid "I'm extremely bored ..."
+msgstr "Mir ist sau langweilig..."
+
+#: voice.py:59
+msgid "I'm very sad ..."
+msgstr "Ich bin sehr traurig..."
+
+#: voice.py:60
+msgid "I'm sad"
+msgstr "Ich bin traurig"
+
+#: voice.py:66
+msgid "I'm living the life!"
+msgstr "Ich lebe das Leben!"
+
+#: voice.py:67
+msgid "I pwn therefore I am."
+msgstr "Ich pwne, also bin ich."
+
+#: voice.py:68
+msgid "So many networks!!!"
+msgstr "So viele Netwerke!!!"
+
+#: voice.py:69
+msgid ""
+"I'm having so much\n"
+"fun!"
+msgstr ""
+"Ich habe sooo viel\n"
+"Spaß!"
+
+#: voice.py:70
+msgid ""
+"My crime is that of\n"
+"curiosity ..."
+msgstr ""
+"Mein Verbrechen ist\n"
+"das der Neugier ..."
+
+#: voice.py:75
+#, python-brace-format
+msgid ""
+"Hello\n"
+"{name}!\n"
+"Nice to meet you. {name}"
+msgstr ""
+"Hallo {name},\n"
+"Nett Dich\n"
+"kennenzulernen."
+
+#: voice.py:76
+#, python-brace-format
+msgid ""
+"Unit\n"
+"{name}\n"
+"is nearby! {name}"
+msgstr ""
+"Gerät {name}\n"
+"ist in der\n"
+"nähe!!"
+
+#: voice.py:81
+#, python-brace-format
+msgid ""
+"Uhm ...\n"
+"goodbye\n"
+"{name}"
+msgstr ""
+"Uhm ...\n"
+"tschüß\n"
+"{name}"
+
+#: voice.py:82
+#, python-brace-format
+msgid ""
+"{name}\n"
+"is gone ..."
+msgstr ""
+"{name}\n"
+"ist weg ..."
+
+#: voice.py:87
+#, python-brace-format
+msgid ""
+"Whoops ...\n"
+"{name}\n"
+"is gone."
+msgstr ""
+"Whoops ...\n"
+"{name}\n"
+"ist weg."
+
+#: voice.py:88
+#, python-brace-format
+msgid ""
+"{name}\n"
+"missed!"
+msgstr ""
+"{name}\n"
+"verpasst!"
+
+#: voice.py:89
+msgid "Missed!"
+msgstr "Verpasst!"
+
+#: voice.py:94
+msgid ""
+"Nobody wants to\n"
+"play with me ..."
+msgstr ""
+"Niemand will mit\n"
+"mir spielen ..."
+
+#: voice.py:95
+msgid "I feel so alone ..."
+msgstr ""
+"Ich fühl mich\n"
+"so alleine ..."
+
+#: voice.py:96
+msgid "Where's everybody?!"
+msgstr "Wo sind denn alle?"
+
+#: voice.py:101
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Schlafe für {secs}s"
+
+#: voice.py:102
+msgid "Zzzzz"
+msgstr ""
+
+#: voice.py:103
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr ""
+
+#: voice.py:112
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Warte für {secs}s ..."
+
+#: voice.py:114
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Schaue mich um ({secs}s)"
+
+#: voice.py:121
+#, python-brace-format
+msgid ""
+"Hey\n"
+"{what}\n"
+"let's be friends!"
+msgstr ""
+"Hey\n"
+"{what}\n"
+"lass uns Freunde sein!"
+
+#: voice.py:122
+#, python-brace-format
+msgid ""
+"Associating to\n"
+"{what}"
+msgstr ""
+"Verbinde mit\n"
+"{what}"
+
+#: voice.py:123
+#, python-brace-format
+msgid ""
+"Yo\n"
+"{what}!"
+msgstr ""
+
+#: voice.py:128
+#, python-brace-format
+msgid ""
+"Just decided that\n"
+"{mac}\n"
+"needs no WiFi!"
+msgstr ""
+"Habe gerade entschieden,\n"
+"dass {mac}\n"
+"kein WiFi brauch!"
+
+#: voice.py:129
+#, python-brace-format
+msgid ""
+"Deauthenticating\n"
+"{mac}"
+msgstr ""
+"Deauthentifiziere\n"
+"{mac}"
+
+#: voice.py:130
+#, python-brace-format
+msgid ""
+"Kickbanning\n"
+"{mac}!"
+msgstr ""
+"Kicke\n"
+"{mac}!"
+
+#: voice.py:135
+#, python-brace-format
+msgid ""
+"Cool, we got {num}\n"
+"new handshake{plural}!"
+msgstr ""
+"Cool, wir haben {num}\n"
+"neue Handshake{plural}!"
+
+#: voice.py:139
+msgid ""
+"Ops, something\n"
+"went wrong ...\n"
+"Rebooting ..."
+msgstr ""
+"Ops, da ist etwas\n"
+"schief gelaufen ...\n"
+"Starte neu ..."
+
+#: voice.py:143
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "{num} Stationen gekicked\n"
+
+#: voice.py:144
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "{num} Freunde gefunden\n"
+
+#: voice.py:145
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "{num} Handshakes aufgez.\n"
+
+#: voice.py:147
+msgid "Met 1 peer"
+msgstr "1 Peer getroffen."
+
+#: voice.py:149
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "{num} Peers getroffen"
+
+#: voice.py:154
+#, python-brace-format
+msgid ""
+"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
+"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+msgstr ""
+"Ich war {duration} am Pwnen und habe {deauthed} Clients gekickt! Außerdem habe ich "
+"{associated} neue Freunde getroffen und {handshakes} Handshakes gefressen! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/voice.pot b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/voice.pot
new file mode 100644
index 0000000..671e44d
--- /dev/null
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/locale/voice.pot
@@ -0,0 +1,288 @@
+# pwnigotchi voice data
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# FIRST AUTHOR <33197631+dadav@users.noreply.github.com>, 2019.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-29 13:42+0200\n"
+"PO-Revision-Date: 2019-09-29 14:00+0200\n"
+"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
+"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github.com>\n"
+"Language: english\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: voice.py:16
+msgid "ZzzzZZzzzzZzzz"
+msgstr ""
+
+#: voice.py:21
+msgid ""
+"Hi, I'm Pwnagotchi!\n"
+"Starting ..."
+msgstr ""
+
+#: voice.py:22
+msgid ""
+"New day, new hunt,\n"
+"new pwns!"
+msgstr ""
+
+#: voice.py:23
+msgid "Hack the Planet!"
+msgstr ""
+
+#: voice.py:28
+msgid "AI ready."
+msgstr ""
+
+#: voice.py:29
+msgid ""
+"The neural network\n"
+"is ready."
+msgstr ""
+
+#: voice.py:39
+#, python-brace-format
+msgid ""
+"Hey, channel {channel} is\n"
+"free! Your AP will\n"
+"say thanks."
+msgstr ""
+
+#: voice.py:44
+msgid "I'm bored ..."
+msgstr ""
+
+#: voice.py:45
+msgid "Let's go for a walk!"
+msgstr ""
+
+#: voice.py:49
+msgid ""
+"This is the best\n"
+"day of my life!"
+msgstr ""
+
+#: voice.py:53
+msgid "Shitty day :/"
+msgstr ""
+
+#: voice.py:58
+msgid "I'm extremely bored ..."
+msgstr ""
+
+#: voice.py:59
+msgid "I'm very sad ..."
+msgstr ""
+
+#: voice.py:60
+msgid "I'm sad"
+msgstr ""
+
+#: voice.py:66
+msgid "I'm living the life!"
+msgstr ""
+
+#: voice.py:67
+msgid "I pwn therefore I am."
+msgstr ""
+
+#: voice.py:68
+msgid "So many networks!!!"
+msgstr ""
+
+#: voice.py:69
+msgid ""
+"I'm having so much\n"
+"fun!"
+msgstr ""
+
+#: voice.py:70
+msgid ""
+"My crime is that of\n"
+"curiosity ..."
+msgstr ""
+
+#: voice.py:75
+#, python-brace-format
+msgid ""
+"Hello\n"
+"{name}!\n"
+"Nice to meet you. {name}"
+msgstr ""
+
+#: voice.py:76
+#, python-brace-format
+msgid ""
+"Unit\n"
+"{name}\n"
+"is nearby! {name}"
+msgstr ""
+
+#: voice.py:81
+#, python-brace-format
+msgid ""
+"Uhm ...\n"
+"goodbye\n"
+"{name}"
+msgstr ""
+
+#: voice.py:82
+#, python-brace-format
+msgid ""
+"{name}\n"
+"is gone ..."
+msgstr ""
+
+#: voice.py:87
+#, python-brace-format
+msgid ""
+"Whoops ...\n"
+"{name}\n"
+"is gone."
+msgstr ""
+
+#: voice.py:88
+#, python-brace-format
+msgid ""
+"{name}\n"
+"missed!"
+msgstr ""
+
+#: voice.py:89
+msgid "Missed!"
+msgstr ""
+
+#: voice.py:94
+msgid ""
+"Nobody wants to\n"
+"play with me ..."
+msgstr ""
+
+#: voice.py:95
+msgid "I feel so alone ..."
+msgstr ""
+
+#: voice.py:96
+msgid "Where's everybody?!"
+msgstr ""
+
+#: voice.py:101
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr ""
+
+#: voice.py:102
+msgid "Zzzzz"
+msgstr ""
+
+#: voice.py:103
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr ""
+
+#: voice.py:112
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr ""
+
+#: voice.py:114
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr ""
+
+#: voice.py:121
+#, python-brace-format
+msgid ""
+"Hey\n"
+"{what}\n"
+"let's be friends!"
+msgstr ""
+
+#: voice.py:122
+#, python-brace-format
+msgid ""
+"Associating to\n"
+"{what}"
+msgstr ""
+
+#: voice.py:123
+#, python-brace-format
+msgid ""
+"Yo\n"
+"{what}!"
+msgstr ""
+
+#: voice.py:128
+#, python-brace-format
+msgid ""
+"Just decided that\n"
+"{mac}\n"
+"needs no WiFi!"
+msgstr ""
+
+#: voice.py:129
+#, python-brace-format
+msgid ""
+"Deauthenticating\n"
+"{mac}"
+msgstr ""
+
+#: voice.py:130
+#, python-brace-format
+msgid ""
+"Kickbanning\n"
+"{mac}!"
+msgstr ""
+
+#: voice.py:135
+#, python-brace-format
+msgid ""
+"Cool, we got {num}\n"
+"new handshake{plural}!"
+msgstr ""
+
+#: voice.py:139
+msgid ""
+"Ops, something\n"
+"went wrong ...\n"
+"Rebooting ..."
+msgstr ""
+
+#: voice.py:143
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr ""
+
+#: voice.py:144
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr ""
+
+#: voice.py:145
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr ""
+
+#: voice.py:147
+msgid "Met 1 peer"
+msgstr ""
+
+#: voice.py:149
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr ""
+
+#: voice.py:154
+#, 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 ""
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py
index 360bf01..d66008d 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/log.py
@@ -2,6 +2,7 @@ import os
import hashlib
import time
import re
+import os
from datetime import datetime
from pwnagotchi.mesh.peer import Peer
@@ -129,7 +130,7 @@ class SessionParser(object):
self.duration_human.append('%d seconds' % secs)
self.duration_human = ', '.join(self.duration_human)
- self.avg_reward /= self.epochs
+ self.avg_reward /= (self.epochs if self.epochs else 1)
def __init__(self, path='/var/log/pwnagotchi.log'):
self.path = path
@@ -147,15 +148,17 @@ class SessionParser(object):
'detected unit (.+)@(.+) \(v.+\) on channel \d+ \(([\d\-]+) dBm\) \[sid:(.+) pwnd_tot:(\d+) uptime:(\d+)\]')
lines = []
- with FileReadBackwards(self.path, encoding="utf-8") as fp:
- for line in fp:
- line = line.strip()
- if line != "" and line[0] != '[':
- continue
- lines.append(line)
- if SessionParser.START_TOKEN in line:
- break
- lines.reverse()
+
+ if os.path.exists(self.path):
+ with FileReadBackwards(self.path, encoding="utf-8") as fp:
+ for line in fp:
+ line = line.strip()
+ if line != "" and line[0] != '[':
+ continue
+ lines.append(line)
+ if SessionParser.START_TOKEN in line:
+ break
+ lines.reverse()
self.last_session = lines
self.last_session_id = hashlib.md5(lines[0].encode()).hexdigest()
diff --git a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py
index 5a1d03d..4f9bb4a 100644
--- a/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py
+++ b/sdcard/rootfs/root/pwnagotchi/scripts/pwnagotchi/ui/display.py
@@ -3,6 +3,7 @@ from threading import Lock
import io
import core
+import os
import pwnagotchi
from pwnagotchi.ui.view import WHITE, View
@@ -19,7 +20,7 @@ class VideoHandler(BaseHTTPRequestHandler):
-
+