Repository: hannadiamond/pwnagotchi-plugins Branch: main Commit: 80483470eee6 Files: 14 Total size: 27.4 KB Directory structure: gitextract_cqjvcuup/ ├── .idea/ │ ├── .gitignore │ ├── inspectionProfiles/ │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── markdown.xml │ ├── misc.xml │ ├── modules.xml │ ├── pwnagotchi-plugins.iml │ └── vcs.xml ├── LICENSE ├── README.md ├── plugins/ │ ├── age.py │ ├── exp.py │ └── ups_hat_c.py └── waveshare_37inch/ └── waveshare3in7.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml # Editor-based HTTP Client requests /httpRequests/ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml ================================================ FILE: .idea/inspectionProfiles/Project_Default.xml ================================================ ================================================ FILE: .idea/inspectionProfiles/profiles_settings.xml ================================================ ================================================ FILE: .idea/markdown.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/pwnagotchi-plugins.iml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Hanna.Diamond Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # pwnagotchi-plugins # Custom Plugin Setup If you have not set up a directory for custom plugins, create the directory and add its path to your config.toml. `main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/"` # Age Plugin A plugin that adds age and int stats based on the the time training and the number of epochs trained. Whenever your pwnagotchi has lived through another 100 epochs or epochs trained, a new status will appear! ![Age](images/age.jpg) ## Setup 1. Copy over `age.py` into your custom plugins directory 2. In your `config.toml` file add: ```toml main.plugins.age.enabled = true main.plugins.age.age_x_coord = 0 main.plugins.age.age_y_coord = 32 main.plugins.age.int_x_coord = 67 main.plugins.age.int_y_coord = 32 ``` 3. Restart your device to see your new stats! # UPS HAT (C) Plugin A plugin that will add a battery capacity and charging indicator for the Waveshare UPS HAT (C) ![UPS Hat C](images/ups_hat_c.jpg) ## Requirements - "i2c" in raspi-config enabled - smbus installed `sudo apt-get install -y python-smbus` ## Setup 1. Copy over `ups_hat_c.py` into your custom plugins directory 2. In your `config.toml` file, add: ```toml main.plugins.ups_hat_c.enabled = true main.plugins.ups_hat_c.label_on = true # show BAT label or just percentage main.plugins.ups_hat_c.shutdown = 5 # battery percent at which the device will turn off main.plugins.ups_hat_c.bat_x_coord = 140 main.plugins.ups_hat_c.bat_y_coord = 0 ``` 3. Restart your device to see your new indicator! # Waveshare 3.7 Inch Display ## Setup 3. Copy `waveshare3in7.py` into `/usr/local/lib/python3.11/dist-packages/pwnagotchi/ui/hw` 4. In the `config.toml` set `ui.display.type = "waveshare3in7"` 5. In `/usr/local/lib/python3.11/dist-packages/pwnagotchi/ui/components.py` in the `class LabeledValue`, replace `def draw` with ```python def draw(self, canvas, drawer): if self.label is None: drawer.text(self.xy, self.value, font=self.label_font, fill=self.color) else: pos = self.xy drawer.text(pos, self.label, font=self.label_font, fill=self.color) drawer.text((pos[0] + self.label_spacing + self.label_font.getsize(self.label)[0], pos[1]), self.value, font=self.text_font, fill=self.color) ``` ================================================ FILE: plugins/age.py ================================================ import os import json import logging from datetime import datetime import pwnagotchi import pwnagotchi.plugins as plugins import pwnagotchi.ui.faces as faces import pwnagotchi.ui.fonts as fonts from pwnagotchi.ui.components import LabeledValue from pwnagotchi.ui.view import BLACK DATA_PATH = '/root/brain.json' class Age(plugins.Plugin): __author__ = 'HannaDiamond' __version__ = '2.0.1' __license__ = 'MIT' __description__ = 'A plugin that will add age and strength stats based on epochs and trained epochs' def __init__(self): self.train_epochs = 0 self.device_start_time = datetime.now() def on_loaded(self): self.load_data() def on_ui_setup(self, ui): ui.add_element('Age', LabeledValue(color=BLACK, label='♥ Age', value=0, position=(int(self.options["age_x_coord"]), int(self.options["age_y_coord"])), label_font=fonts.Bold, text_font=fonts.Medium)) ui.add_element('Int', LabeledValue(color=BLACK, label='Int', value=0, position=(int(self.options["int_x_coord"]), int(self.options["int_y_coord"])), label_font=fonts.Bold, text_font=fonts.Medium)) def on_unload(self, ui): with ui._lock: ui.remove_element('Age') ui.remove_element('Int') def on_ui_update(self, ui): ui.set('Age', "%s" % self.calculate_device_age()) ui.set('Int', "%s" % self.abrev_number(self.train_epochs)) def on_ai_training_step(self, agent, _locals, _globals): self.train_epochs += 1 if self.train_epochs % 100 == 0: self.intelligence_checkpoint(agent) self.age_checkpoint(agent) def abrev_number(self, num): if num < 100000: return str(num) else: magnitude = 0 while abs(num) >= 1000: magnitude += 1 num /= 1000.0 abbr = ['', 'K', 'M', 'B', 'T', 'P'][magnitude] return '{}{}'.format('{:.2f}'.format(num).rstrip('0').rstrip('.'), abbr) def age_checkpoint(self, agent): view = agent.view() view.set('face', faces.HAPPY) view.set('status', "Wow, I've lived for " + self.calculate_device_age()) view.update(force=True) def intelligence_checkpoint(self, agent): view = agent.view() view.set('face', faces.MOTIVATED) view.set('status', "Look at my intelligence go up! \n" \ "I've trained for " + self.abrev_number(self.train_epochs) + " epochs") view.update(force=True) def calculate_device_age(self): current_time = datetime.now() age_delta = current_time - self.device_start_time years = age_delta.days // 365 remaining_days = age_delta.days % 365 months = remaining_days // 30 days = remaining_days % 30 age_str = "" if years != 0: age_str = f'{years}y' if months != 0: age_str = f'{age_str} {months}m' if len(age_str) !=0 : age_str = f'{age_str} {days}d' else: age_str = f'{days}d' return age_str def load_data(self): if os.path.exists(DATA_PATH): with open(DATA_PATH) as f: data = json.load(f) self.train_epochs = data['epochs_trained'] ================================================ FILE: plugins/exp.py ================================================ import logging import os import random import json import pwnagotchi import pwnagotchi.agent import pwnagotchi.plugins as plugins import pwnagotchi.ui.fonts as fonts from pwnagotchi.ui.components import LabeledValue from pwnagotchi.ui.view import BLACK # Static Variables MULTIPLIER_ASSOCIATION = 1 MULTIPLIER_DEAUTH = 2 MULTIPLIER_HANDSHAKE = 3 MULTIPLIER_AI_BEST_REWARD = 5 TAG = "[EXP Plugin]" FACE_LEVELUP = '(≧◡◡≦)' BAR_ERROR = "| error |" FILE_SAVE = "exp_stats" FILE_SAVE_LEGACY = "exp" JSON_KEY_LEVEL = "level" JSON_KEY_EXP ="exp" JSON_KEY_EXP_TOT ="exp_tot" JSON_KEY_STRENGTH = "strength" class EXP(plugins.Plugin): __author__ = 'GaelicThunder, HannaDiamond, Kaska89, Rai, JayofElony, & Terminator' __version__ = '2.0.1' __license__ = 'GPL3' __description__ = 'Get exp every time a handshake get captured.' # Attention number masking def LogInfo(self, text): logging.info(TAG + " " +text) # Attention number masking def LogDebug(self, text): logging.debug(TAG + " " +text) def __init__(self): self.percent=0 self.strength=1 self.calculateInitialXP = False self.exp=0 self.lv=1 self.exp_tot=0 # Sets the file type I recommend json self.save_file_mode = self.save_file_modes("json") self.save_file = self.getSaveFileName(self.save_file_mode) # Migrate from old save system self.migrateLegacySave() # Create save file if not os.path.exists(self.save_file): self.Save(self.save_file, self.save_file_mode) else: try: # Try loading self.Load(self.save_file, self.save_file_mode) except: # Likely throws an exception if json file is corrupted, so we need to calculate from scratch self.calculateInitialXP = True # No previous data, try get it if self.lv == 1 and self.exp == 0: self.calculateInitialXP = True if self.exp_tot == 0: self.LogInfo("Need to calculate Total Exp") self.exp_tot = self.calcActualSum(self.lv, self.exp) self.Save(self.save_file, self.save_file_mode) self.expneeded = self.calcExpNeeded(self.lv) def on_loaded(self): # logging.info("Exp plugin loaded for %s" % self.options['device']) self.LogInfo("Plugin Loaded") def save_file_modes(self,argument): switcher = { "txt": 0, "json": 1, } return switcher.get(argument, 0) def Save(self, file, save_file_mode): self.LogDebug('Saving Exp') if save_file_mode == 0: self.saveToTxtFile(file) if save_file_mode == 1: self.saveToJsonFile(file) def saveToTxtFile(self, file): outfile=open(file, 'w') print(self.exp,file=outfile) print(self.lv,file=outfile) print(self.exp_tot,file=outfile) print(self.strength,file=outfile) outfile.close() def loadFromTxtFile(self, file): if os.path.exists(file): outfile= open(file, 'r+') lines = outfile.readlines() linecounter = 1 for line in lines: if linecounter == 1: self.exp = int(line) elif linecounter == 2: self.lv == int(line) elif linecounter == 3: self.exp_tot == int(line) elif linecounter == 4: self.strength == int(line) linecounter += 1 outfile.close() def saveToJsonFile(self,file): data = { JSON_KEY_LEVEL : self.lv, JSON_KEY_EXP : self.exp, JSON_KEY_EXP_TOT : self.exp_tot, JSON_KEY_STRENGTH : self.strength } with open(file, 'w') as f: f.write(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))) def loadFromJsonFile(self, file): # Tot exp is introduced with json, no check needed data = {} with open(file, 'r') as f: data = json.loads(f.read()) if bool(data): self.lv = data[JSON_KEY_LEVEL] self.exp = data[JSON_KEY_EXP] self.exp_tot = data[JSON_KEY_EXP_TOT] self.strength = data[JSON_KEY_STRENGTH] else: self.LogInfo("Empty json") # TODO: one day change save file mode to file date def Load(self, file, save_file_mode): self.LogDebug('Loading Exp') if save_file_mode == 0: self.loadFromTxtFile(file) if save_file_mode == 1: self.loadFromJsonFile(file) def getSaveFileName(self, save_file_mode): file = os.path.dirname(os.path.realpath(__file__)) file = file + "/" +FILE_SAVE if save_file_mode == 0: file = file + ".txt" elif save_file_mode == 1: file = file + ".json" else: # See switcher file = file + ".txt" return file def migrateLegacySave(self): legacyFile = os.path.dirname(os.path.realpath(__file__)) legacyFile = legacyFile + "/" + FILE_SAVE_LEGACY +".txt" if os.path.exists(legacyFile): self.loadFromTxtFile(legacyFile) self.LogInfo("Migrating Legacy Save...") self.Save(self.save_file, self.save_file_mode) os.remove(legacyFile) def barString(self, symbols_count, p): if p > 100: return BAR_ERROR length = symbols_count-2 bar_char = '▥' blank_char = ' ' bar_length = int(round((length / 100)*p)) blank_length = length - bar_length res = '|' + bar_char * bar_length + blank_char * blank_length + '|' return res def on_ui_setup(self, ui): ui.add_element('Lv', LabeledValue(color=BLACK, label='Lv', value=0, position=(int(self.options["lvl_x_coord"]), int(self.options["lvl_y_coord"])), label_font=fonts.Bold, text_font=fonts.Medium)) ui.add_element('Exp', LabeledValue(color=BLACK, label='Exp', value=0, position=(int(self.options["exp_x_coord"]), int(self.options["exp_y_coord"])), label_font=fonts.Bold, text_font=fonts.Medium)) ui.add_element('Str', LabeledValue(color=BLACK, label='Str', value=0, position=(int(self.options["str_x_coord"]), int(self.options["str_y_coord"])), label_font=fonts.Bold, text_font=fonts.Medium)) def on_ui_update(self, ui): self.expneeded=self.calcExpNeeded(self.lv) self.percent=int((self.exp/self.expneeded)*100) symbols_count=int(self.options["bar_symbols_count"]) bar=self.barString(symbols_count, self.percent) ui.set('Lv', "%d" % self.lv) ui.set('Exp', "%s" % bar) self.calcStrength() ui.set('Str', "%d" % self.strength) def calcExpNeeded(self, level): # If the pwnagotchi is lvl <1 it causes the keys to be deleted if level == 1: return 5 return int((level**3)/2) def exp_check(self, agent): self.LogDebug("EXP CHECK") if self.exp>=self.expneeded: self.exp=1 self.lv=self.lv+1 self.expneeded=self.calcExpNeeded(self.lv) self.displayLevelUp(agent) def parseSessionStats(self): sum = 0 dir = pwnagotchi.config['main']['plugins']['session-stats']['save_directory'] # TODO: remove self.LogInfo("Session-Stats dir: " + dir) for filename in os.listdir(dir): self.LogInfo("Parsing " + filename + "...") if filename.endswith(".json") & filename.startswith("stats"): try: sum += self.parseSessionStatsFile(os.path.join(dir,filename)) except: self.LogInfo("ERROR parsing File: "+ filename) return sum def parseSessionStatsFile(self, path): sum = 0 deauths = 0 handshakes = 0 associations = 0 with open(path) as json_file: data = json.load(json_file) for entry in data["data"]: deauths += data["data"][entry]["num_deauths"] handshakes += data["data"][entry]["num_handshakes"] associations += data["data"][entry]["num_associations"] sum += deauths * MULTIPLIER_DEAUTH sum += handshakes * MULTIPLIER_HANDSHAKE sum += associations * MULTIPLIER_ASSOCIATION return sum # If initial sum is 0, we try to parse it def calculateInitialSum(self, agent): sessionStatsActive = False sum = 0 # Check if session stats is loaded for plugin in pwnagotchi.plugins.loaded: if plugin == "session-stats": sessionStatsActive = True break if sessionStatsActive: try: self.LogInfo("parsing session-stats") sum = self.parseSessionStats() except: self.LogInfo("Error parsing session-stats") else: self.LogInfo("parsing last session") sum = self.lastSessionPoints(agent) self.LogInfo(str(sum) + " Points calculated") return sum # Get Last Sessions Points def lastSessionPoints(self, agent): summary = 0 summary += agent.LastSession.handshakes * MULTIPLIER_HANDSHAKE summary += agent.LastSession.associated * MULTIPLIER_ASSOCIATION summary += agent.LastSession.deauthed * MULTIPLIER_DEAUTH return summary # Helper function to calculate multiple Levels from a sum of EXPs def calcLevelFromSum(self, sum, agent): sum1 = sum level = 1 while sum1 > self.calcExpNeeded(level): sum1 -= self.calcExpNeeded(level) level += 1 self.lv = level self.exp = sum1 self.expneeded = self.calcExpNeeded(level) - sum1 if level > 1: #get Excited ;-) self.displayLevelUp(agent) def calcActualSum(self, level, exp): lvlCounter = 1 sum = exp # I know it wouldn't work if you change the lvl algorithm while lvlCounter < level: sum += self.calcExpNeeded(lvlCounter) lvlCounter += 1 return sum def displayLevelUp(self, agent): view = agent.view() view.set('face', FACE_LEVELUP) view.set('status', "Level Up!") view.update(force=True) def calcStrength(self): # Custom formula for strength calculation self.strength = self.exp * self.lv * 0.05 # Event Handling def on_association(self, agent, access_point): self.exp += MULTIPLIER_ASSOCIATION self.exp_tot += MULTIPLIER_ASSOCIATION self.exp_check(agent) self.Save(self.save_file, self.save_file_mode) def on_deauthentication(self, agent, access_point, client_station): self.exp += MULTIPLIER_DEAUTH self.exp_tot += MULTIPLIER_DEAUTH self.exp_check(agent) self.Save(self.save_file, self.save_file_mode) def on_handshake(self, agent, filename, access_point, client_station): self.exp += MULTIPLIER_HANDSHAKE self.exp_tot += MULTIPLIER_HANDSHAKE self.exp_check(agent) self.Save(self.save_file, self.save_file_mode) def on_ai_best_reward(self, agent, reward): self.exp += MULTIPLIER_AI_BEST_REWARD self.exp_tot += MULTIPLIER_AI_BEST_REWARD self.exp_check(agent) self.Save(self.save_file, self.save_file_mode) def on_ready(self, agent): if self.calculateInitialXP: self.LogInfo("Initial point calculation") sum = self.calculateInitialSum(agent) self.exp_tot = sum self.calcLevelFromSum(sum, agent) self.Save(self.save_file, self.save_file_mode) ================================================ FILE: plugins/ups_hat_c.py ================================================ # functions for get UPS status - needs enable "i2c" in raspi-config, smbus installed (sudo apt-get install -y python-smbus) import logging import time import pwnagotchi import pwnagotchi.plugins as plugins import pwnagotchi.ui.fonts as fonts from pwnagotchi.ui.components import LabeledValue from pwnagotchi.ui.view import BLACK # Config Register (R/W) _REG_CONFIG = 0x00 # SHUNT VOLTAGE REGISTER (R) _REG_SHUNTVOLTAGE = 0x01 # BUS VOLTAGE REGISTER (R) _REG_BUSVOLTAGE = 0x02 # POWER REGISTER (R) _REG_POWER = 0x03 # CURRENT REGISTER (R) _REG_CURRENT = 0x04 # CALIBRATION REGISTER (R/W) _REG_CALIBRATION = 0x05 class UPS: def __init__(self): # only import when the module is loaded and enabled import smbus self._bus = smbus.SMBus(1) self._addr = 0x43 # Set chip to known config values to start self._cal_value = 0 self._current_lsb = 0 self._power_lsb = 0 self.set_calibration_32V_2A() def read(self, address): data = self._bus.read_i2c_block_data(self._addr, address, 2) return ((data[0] * 256) + data[1]) def write(self, address, data): temp = [0, 0] temp[1] = data & 0xFF temp[0] = (data & 0xFF00) >> 8 self._bus.write_i2c_block_data(self._addr, address, temp) def set_calibration_32V_2A(self): """Configures to INA219 to be able to measure up to 32V and 2A of current. Counter overflow occurs at 3.2A. ..note :: These calculations assume a 0.1 shunt ohm resistor is present """ self._cal_value = 0 self._current_lsb = 1 # Current LSB = 100uA per bit self._cal_value = 4096 self._power_lsb = .002 # Power LSB = 2mW per bit # Set Calibration register to 'Cal' calculated above self.write(_REG_CALIBRATION, self._cal_value) # Set Config register to take into account the settings above self.bus_voltage_range = 0x01 self.gain = 0x03 self.bus_adc_resolution = 0x0D self.shunt_adc_resolution = 0x0D self.mode = 0x07 self.config = self.bus_voltage_range << 13 | \ self.gain << 11 | \ self.bus_adc_resolution << 7 | \ self.shunt_adc_resolution << 3 | \ self.mode self.write(_REG_CONFIG, self.config) def getBusVoltage_V(self): self.write(_REG_CALIBRATION, self._cal_value) self.read(_REG_BUSVOLTAGE) return (self.read(_REG_BUSVOLTAGE) >> 3) * 0.004 def getCurrent_mA(self): value = self.read(_REG_CURRENT) if value > 32767: value -= 65535 if (value * self._current_lsb) < 0: return "" else: return "+" class UPSC(plugins.Plugin): __author__ = 'HannaDiamond' __version__ = '1.0.1' __license__ = 'MIT' __description__ = 'A plugin that will add a battery capacity and charging indicator for the UPS HAT C' def __init__(self): self.ups = None def on_loaded(self): self.ups = UPS() def on_ui_setup(self, ui): if self.options["label_on"]: ui.add_element('ups', LabeledValue(color=BLACK, label='BAT', value="--%", position=(int(self.options["bat_x_coord"]), int(self.options["bat_y_coord"])), label_font=fonts.Bold, text_font=fonts.Medium)) else: ui.add_element('ups', LabeledValue(color=BLACK, label='', value="--%", position=(int(self.options["bat_x_coord"]), int(self.options["bat_y_coord"])), 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): bus_voltage = self.ups.getBusVoltage_V() capacity = int((bus_voltage - 3) / 1.2 * 100) if (capacity > 100): capacity = 100 if (capacity < 0): capacity = 0 charging = self.ups.getCurrent_mA() ui.set('ups', str(capacity) + "%" + charging) if capacity <= self.options['shutdown']: logging.info('[ups_hat_c] Empty battery (<= %s%%): shutting down' % self.options['shutdown']) ui.update(force=True, new_data={'status': 'Battery exhausted, bye ...'}) time.sleep(3) pwnagotchi.shutdown() ================================================ FILE: waveshare_37inch/waveshare3in7.py ================================================ import logging import pwnagotchi.ui.fonts as fonts from pwnagotchi.ui.hw.base import DisplayImpl class Waveshare3in7(DisplayImpl): def __init__(self, config): super(Waveshare3in7, self).__init__(config, 'waveshare3in7') def layout(self): fonts.setup(20, 19, 20, 45, 35, 19) self._layout['width'] = 480 self._layout['height'] = 280 self._layout['face'] = (0, 75) self._layout['name'] = (5, 35) self._layout['channel'] = (0, 0) self._layout['aps'] = (65, 0) self._layout['uptime'] = (355, 0) self._layout['line1'] = [0, 25, 480, 25] self._layout['line2'] = [0, 255, 480, 255] self._layout['friend_face'] = (0, 146) self._layout['friend_name'] = (40, 146) self._layout['shakes'] = (0, 258) self._layout['mode'] = (430, 258) self._layout['status'] = { 'pos': (225, 35), 'font': fonts.status_font(fonts.Medium), 'max': 21 } return self._layout def initialize(self): logging.info("initializing waveshare 3.7 inch lcd display") from pwnagotchi.ui.hw.libs.waveshare.epaper.v3in7.epd3in7 import EPD self._display = EPD() self._display.init(0) self._display.Clear(0) self._display.init(1) # 1Gray mode def render(self, canvas): buf = self._display.getbuffer(canvas) self._display.display_1Gray(buf) def clear(self): self._display.Clear(0)