├── .idea ├── .gitignore ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── markdown.xml ├── misc.xml ├── modules.xml ├── pwnagotchi-plugins.iml └── vcs.xml ├── LICENSE ├── README.md ├── images ├── age.jpg └── ups_hat_c.jpg ├── plugins ├── age.py ├── exp.py └── ups_hat_c.py └── waveshare_37inch └── waveshare3in7.py /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/markdown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/pwnagotchi-plugins.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hanna.Diamond 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pwnagotchi-plugins 2 | 3 | # Custom Plugin Setup 4 | If you have not set up a directory for custom plugins, create the directory and add its path to your config.toml. 5 | `main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins/"` 6 | 7 | # Age Plugin 8 | A plugin that adds age and int stats based on the the time training and the number of epochs trained. 9 | Whenever your pwnagotchi has lived through another 100 epochs or epochs trained, a new status will appear! 10 | ![Age](images/age.jpg) 11 | 12 | ## Setup 13 | 1. Copy over `age.py` into your custom plugins directory 14 | 2. In your `config.toml` file add: 15 | ```toml 16 | main.plugins.age.enabled = true 17 | main.plugins.age.age_x_coord = 0 18 | main.plugins.age.age_y_coord = 32 19 | main.plugins.age.int_x_coord = 67 20 | main.plugins.age.int_y_coord = 32 21 | ``` 22 | 3. Restart your device to see your new stats! 23 | 24 | # UPS HAT (C) Plugin 25 | A plugin that will add a battery capacity and charging indicator for the Waveshare UPS HAT (C) 26 | ![UPS Hat C](images/ups_hat_c.jpg) 27 | 28 | ## Requirements 29 | - "i2c" in raspi-config enabled 30 | - smbus installed `sudo apt-get install -y python-smbus` 31 | ## Setup 32 | 1. Copy over `ups_hat_c.py` into your custom plugins directory 33 | 2. In your `config.toml` file, add: 34 | ```toml 35 | main.plugins.ups_hat_c.enabled = true 36 | main.plugins.ups_hat_c.label_on = true # show BAT label or just percentage 37 | main.plugins.ups_hat_c.shutdown = 5 # battery percent at which the device will turn off 38 | main.plugins.ups_hat_c.bat_x_coord = 140 39 | main.plugins.ups_hat_c.bat_y_coord = 0 40 | ``` 41 | 3. Restart your device to see your new indicator! 42 | 43 | # Waveshare 3.7 Inch Display 44 | 45 | ## Setup 46 | 3. Copy `waveshare3in7.py` into `/usr/local/lib/python3.11/dist-packages/pwnagotchi/ui/hw` 47 | 4. In the `config.toml` set `ui.display.type = "waveshare3in7"` 48 | 5. In `/usr/local/lib/python3.11/dist-packages/pwnagotchi/ui/components.py` in the `class LabeledValue`, replace `def draw` with 49 | ```python 50 | def draw(self, canvas, drawer): 51 | if self.label is None: 52 | drawer.text(self.xy, self.value, font=self.label_font, fill=self.color) 53 | else: 54 | pos = self.xy 55 | drawer.text(pos, self.label, font=self.label_font, fill=self.color) 56 | 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) 57 | ``` 58 | -------------------------------------------------------------------------------- /images/age.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hannadiamond/pwnagotchi-plugins/80483470eee670bf0e11b0f1a731d2c3171e0ac9/images/age.jpg -------------------------------------------------------------------------------- /images/ups_hat_c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hannadiamond/pwnagotchi-plugins/80483470eee670bf0e11b0f1a731d2c3171e0ac9/images/ups_hat_c.jpg -------------------------------------------------------------------------------- /plugins/age.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | from datetime import datetime 5 | 6 | import pwnagotchi 7 | import pwnagotchi.plugins as plugins 8 | import pwnagotchi.ui.faces as faces 9 | import pwnagotchi.ui.fonts as fonts 10 | from pwnagotchi.ui.components import LabeledValue 11 | from pwnagotchi.ui.view import BLACK 12 | 13 | DATA_PATH = '/root/brain.json' 14 | 15 | class Age(plugins.Plugin): 16 | __author__ = 'HannaDiamond' 17 | __version__ = '2.0.1' 18 | __license__ = 'MIT' 19 | __description__ = 'A plugin that will add age and strength stats based on epochs and trained epochs' 20 | 21 | def __init__(self): 22 | self.train_epochs = 0 23 | self.device_start_time = datetime.now() 24 | 25 | def on_loaded(self): 26 | self.load_data() 27 | 28 | 29 | def on_ui_setup(self, ui): 30 | ui.add_element('Age', LabeledValue(color=BLACK, label='♥ Age', value=0, 31 | position=(int(self.options["age_x_coord"]), 32 | int(self.options["age_y_coord"])), 33 | label_font=fonts.Bold, text_font=fonts.Medium)) 34 | ui.add_element('Int', LabeledValue(color=BLACK, label='Int', value=0, 35 | position=(int(self.options["int_x_coord"]), 36 | int(self.options["int_y_coord"])), 37 | label_font=fonts.Bold, text_font=fonts.Medium)) 38 | 39 | def on_unload(self, ui): 40 | with ui._lock: 41 | ui.remove_element('Age') 42 | ui.remove_element('Int') 43 | 44 | def on_ui_update(self, ui): 45 | ui.set('Age', "%s" % self.calculate_device_age()) 46 | ui.set('Int', "%s" % self.abrev_number(self.train_epochs)) 47 | 48 | 49 | def on_ai_training_step(self, agent, _locals, _globals): 50 | self.train_epochs += 1 51 | if self.train_epochs % 100 == 0: 52 | self.intelligence_checkpoint(agent) 53 | self.age_checkpoint(agent) 54 | 55 | def abrev_number(self, num): 56 | if num < 100000: 57 | return str(num) 58 | else: 59 | magnitude = 0 60 | while abs(num) >= 1000: 61 | magnitude += 1 62 | num /= 1000.0 63 | abbr = ['', 'K', 'M', 'B', 'T', 'P'][magnitude] 64 | return '{}{}'.format('{:.2f}'.format(num).rstrip('0').rstrip('.'), abbr) 65 | 66 | def age_checkpoint(self, agent): 67 | view = agent.view() 68 | view.set('face', faces.HAPPY) 69 | view.set('status', "Wow, I've lived for " + self.calculate_device_age()) 70 | view.update(force=True) 71 | 72 | def intelligence_checkpoint(self, agent): 73 | view = agent.view() 74 | view.set('face', faces.MOTIVATED) 75 | view.set('status', "Look at my intelligence go up! \n" \ 76 | "I've trained for " + self.abrev_number(self.train_epochs) + " epochs") 77 | view.update(force=True) 78 | 79 | def calculate_device_age(self): 80 | current_time = datetime.now() 81 | age_delta = current_time - self.device_start_time 82 | 83 | years = age_delta.days // 365 84 | remaining_days = age_delta.days % 365 85 | months = remaining_days // 30 86 | days = remaining_days % 30 87 | 88 | age_str = "" 89 | if years != 0: 90 | age_str = f'{years}y' 91 | if months != 0: 92 | age_str = f'{age_str} {months}m' 93 | if len(age_str) !=0 : 94 | age_str = f'{age_str} {days}d' 95 | else: 96 | age_str = f'{days}d' 97 | 98 | return age_str 99 | 100 | def load_data(self): 101 | if os.path.exists(DATA_PATH): 102 | with open(DATA_PATH) as f: 103 | data = json.load(f) 104 | self.train_epochs = data['epochs_trained'] 105 | -------------------------------------------------------------------------------- /plugins/exp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import random 4 | import json 5 | 6 | import pwnagotchi 7 | import pwnagotchi.agent 8 | import pwnagotchi.plugins as plugins 9 | import pwnagotchi.ui.fonts as fonts 10 | from pwnagotchi.ui.components import LabeledValue 11 | from pwnagotchi.ui.view import BLACK 12 | 13 | # Static Variables 14 | MULTIPLIER_ASSOCIATION = 1 15 | MULTIPLIER_DEAUTH = 2 16 | MULTIPLIER_HANDSHAKE = 3 17 | MULTIPLIER_AI_BEST_REWARD = 5 18 | TAG = "[EXP Plugin]" 19 | FACE_LEVELUP = '(≧◡◡≦)' 20 | BAR_ERROR = "| error |" 21 | FILE_SAVE = "exp_stats" 22 | FILE_SAVE_LEGACY = "exp" 23 | JSON_KEY_LEVEL = "level" 24 | JSON_KEY_EXP ="exp" 25 | JSON_KEY_EXP_TOT ="exp_tot" 26 | JSON_KEY_STRENGTH = "strength" 27 | 28 | class EXP(plugins.Plugin): 29 | __author__ = 'GaelicThunder, HannaDiamond, Kaska89, Rai, JayofElony, & Terminator' 30 | __version__ = '2.0.1' 31 | __license__ = 'GPL3' 32 | __description__ = 'Get exp every time a handshake get captured.' 33 | 34 | # Attention number masking 35 | def LogInfo(self, text): 36 | logging.info(TAG + " " +text) 37 | 38 | # Attention number masking 39 | def LogDebug(self, text): 40 | logging.debug(TAG + " " +text) 41 | 42 | 43 | def __init__(self): 44 | self.percent=0 45 | self.strength=1 46 | self.calculateInitialXP = False 47 | self.exp=0 48 | self.lv=1 49 | self.exp_tot=0 50 | # Sets the file type I recommend json 51 | self.save_file_mode = self.save_file_modes("json") 52 | self.save_file = self.getSaveFileName(self.save_file_mode) 53 | # Migrate from old save system 54 | self.migrateLegacySave() 55 | 56 | # Create save file 57 | if not os.path.exists(self.save_file): 58 | self.Save(self.save_file, self.save_file_mode) 59 | else: 60 | try: 61 | # Try loading 62 | self.Load(self.save_file, self.save_file_mode) 63 | except: 64 | # Likely throws an exception if json file is corrupted, so we need to calculate from scratch 65 | self.calculateInitialXP = True 66 | 67 | # No previous data, try get it 68 | if self.lv == 1 and self.exp == 0: 69 | self.calculateInitialXP = True 70 | if self.exp_tot == 0: 71 | self.LogInfo("Need to calculate Total Exp") 72 | self.exp_tot = self.calcActualSum(self.lv, self.exp) 73 | self.Save(self.save_file, self.save_file_mode) 74 | 75 | self.expneeded = self.calcExpNeeded(self.lv) 76 | 77 | def on_loaded(self): 78 | # logging.info("Exp plugin loaded for %s" % self.options['device']) 79 | self.LogInfo("Plugin Loaded") 80 | 81 | def save_file_modes(self,argument): 82 | switcher = { 83 | "txt": 0, 84 | "json": 1, 85 | } 86 | return switcher.get(argument, 0) 87 | 88 | def Save(self, file, save_file_mode): 89 | self.LogDebug('Saving Exp') 90 | if save_file_mode == 0: 91 | self.saveToTxtFile(file) 92 | if save_file_mode == 1: 93 | self.saveToJsonFile(file) 94 | 95 | def saveToTxtFile(self, file): 96 | outfile=open(file, 'w') 97 | print(self.exp,file=outfile) 98 | print(self.lv,file=outfile) 99 | print(self.exp_tot,file=outfile) 100 | print(self.strength,file=outfile) 101 | outfile.close() 102 | 103 | def loadFromTxtFile(self, file): 104 | if os.path.exists(file): 105 | outfile= open(file, 'r+') 106 | lines = outfile.readlines() 107 | linecounter = 1 108 | for line in lines: 109 | if linecounter == 1: 110 | self.exp = int(line) 111 | elif linecounter == 2: 112 | self.lv == int(line) 113 | elif linecounter == 3: 114 | self.exp_tot == int(line) 115 | elif linecounter == 4: 116 | self.strength == int(line) 117 | linecounter += 1 118 | outfile.close() 119 | 120 | def saveToJsonFile(self,file): 121 | data = { 122 | JSON_KEY_LEVEL : self.lv, 123 | JSON_KEY_EXP : self.exp, 124 | JSON_KEY_EXP_TOT : self.exp_tot, 125 | JSON_KEY_STRENGTH : self.strength 126 | } 127 | 128 | with open(file, 'w') as f: 129 | f.write(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))) 130 | 131 | def loadFromJsonFile(self, file): 132 | # Tot exp is introduced with json, no check needed 133 | data = {} 134 | with open(file, 'r') as f: 135 | data = json.loads(f.read()) 136 | 137 | if bool(data): 138 | self.lv = data[JSON_KEY_LEVEL] 139 | self.exp = data[JSON_KEY_EXP] 140 | self.exp_tot = data[JSON_KEY_EXP_TOT] 141 | self.strength = data[JSON_KEY_STRENGTH] 142 | else: 143 | self.LogInfo("Empty json") 144 | 145 | # TODO: one day change save file mode to file date 146 | def Load(self, file, save_file_mode): 147 | self.LogDebug('Loading Exp') 148 | if save_file_mode == 0: 149 | self.loadFromTxtFile(file) 150 | if save_file_mode == 1: 151 | self.loadFromJsonFile(file) 152 | 153 | def getSaveFileName(self, save_file_mode): 154 | file = os.path.dirname(os.path.realpath(__file__)) 155 | file = file + "/" +FILE_SAVE 156 | if save_file_mode == 0: 157 | file = file + ".txt" 158 | elif save_file_mode == 1: 159 | file = file + ".json" 160 | else: 161 | # See switcher 162 | file = file + ".txt" 163 | return file 164 | 165 | def migrateLegacySave(self): 166 | legacyFile = os.path.dirname(os.path.realpath(__file__)) 167 | legacyFile = legacyFile + "/" + FILE_SAVE_LEGACY +".txt" 168 | if os.path.exists(legacyFile): 169 | self.loadFromTxtFile(legacyFile) 170 | self.LogInfo("Migrating Legacy Save...") 171 | self.Save(self.save_file, self.save_file_mode) 172 | os.remove(legacyFile) 173 | 174 | def barString(self, symbols_count, p): 175 | if p > 100: 176 | return BAR_ERROR 177 | length = symbols_count-2 178 | bar_char = '▥' 179 | blank_char = ' ' 180 | bar_length = int(round((length / 100)*p)) 181 | blank_length = length - bar_length 182 | res = '|' + bar_char * bar_length + blank_char * blank_length + '|' 183 | return res 184 | 185 | def on_ui_setup(self, ui): 186 | ui.add_element('Lv', LabeledValue(color=BLACK, label='Lv', value=0, 187 | position=(int(self.options["lvl_x_coord"]), 188 | int(self.options["lvl_y_coord"])), 189 | label_font=fonts.Bold, text_font=fonts.Medium)) 190 | ui.add_element('Exp', LabeledValue(color=BLACK, label='Exp', value=0, 191 | position=(int(self.options["exp_x_coord"]), 192 | int(self.options["exp_y_coord"])), 193 | label_font=fonts.Bold, text_font=fonts.Medium)) 194 | ui.add_element('Str', LabeledValue(color=BLACK, label='Str', value=0, 195 | position=(int(self.options["str_x_coord"]), 196 | int(self.options["str_y_coord"])), 197 | label_font=fonts.Bold, text_font=fonts.Medium)) 198 | 199 | def on_ui_update(self, ui): 200 | self.expneeded=self.calcExpNeeded(self.lv) 201 | self.percent=int((self.exp/self.expneeded)*100) 202 | symbols_count=int(self.options["bar_symbols_count"]) 203 | bar=self.barString(symbols_count, self.percent) 204 | ui.set('Lv', "%d" % self.lv) 205 | ui.set('Exp', "%s" % bar) 206 | self.calcStrength() 207 | ui.set('Str', "%d" % self.strength) 208 | 209 | def calcExpNeeded(self, level): 210 | # If the pwnagotchi is lvl <1 it causes the keys to be deleted 211 | if level == 1: 212 | return 5 213 | return int((level**3)/2) 214 | 215 | def exp_check(self, agent): 216 | self.LogDebug("EXP CHECK") 217 | if self.exp>=self.expneeded: 218 | self.exp=1 219 | self.lv=self.lv+1 220 | self.expneeded=self.calcExpNeeded(self.lv) 221 | self.displayLevelUp(agent) 222 | 223 | def parseSessionStats(self): 224 | sum = 0 225 | dir = pwnagotchi.config['main']['plugins']['session-stats']['save_directory'] 226 | # TODO: remove 227 | self.LogInfo("Session-Stats dir: " + dir) 228 | for filename in os.listdir(dir): 229 | self.LogInfo("Parsing " + filename + "...") 230 | if filename.endswith(".json") & filename.startswith("stats"): 231 | try: 232 | sum += self.parseSessionStatsFile(os.path.join(dir,filename)) 233 | except: 234 | self.LogInfo("ERROR parsing File: "+ filename) 235 | 236 | return sum 237 | 238 | def parseSessionStatsFile(self, path): 239 | sum = 0 240 | deauths = 0 241 | handshakes = 0 242 | associations = 0 243 | with open(path) as json_file: 244 | data = json.load(json_file) 245 | for entry in data["data"]: 246 | deauths += data["data"][entry]["num_deauths"] 247 | handshakes += data["data"][entry]["num_handshakes"] 248 | associations += data["data"][entry]["num_associations"] 249 | 250 | 251 | sum += deauths * MULTIPLIER_DEAUTH 252 | sum += handshakes * MULTIPLIER_HANDSHAKE 253 | sum += associations * MULTIPLIER_ASSOCIATION 254 | 255 | return sum 256 | 257 | 258 | # If initial sum is 0, we try to parse it 259 | def calculateInitialSum(self, agent): 260 | sessionStatsActive = False 261 | sum = 0 262 | # Check if session stats is loaded 263 | for plugin in pwnagotchi.plugins.loaded: 264 | if plugin == "session-stats": 265 | sessionStatsActive = True 266 | break 267 | 268 | if sessionStatsActive: 269 | try: 270 | self.LogInfo("parsing session-stats") 271 | sum = self.parseSessionStats() 272 | except: 273 | self.LogInfo("Error parsing session-stats") 274 | 275 | 276 | else: 277 | self.LogInfo("parsing last session") 278 | sum = self.lastSessionPoints(agent) 279 | 280 | self.LogInfo(str(sum) + " Points calculated") 281 | return sum 282 | 283 | 284 | 285 | # Get Last Sessions Points 286 | def lastSessionPoints(self, agent): 287 | summary = 0 288 | summary += agent.LastSession.handshakes * MULTIPLIER_HANDSHAKE 289 | summary += agent.LastSession.associated * MULTIPLIER_ASSOCIATION 290 | summary += agent.LastSession.deauthed * MULTIPLIER_DEAUTH 291 | return summary 292 | 293 | 294 | # Helper function to calculate multiple Levels from a sum of EXPs 295 | def calcLevelFromSum(self, sum, agent): 296 | sum1 = sum 297 | level = 1 298 | while sum1 > self.calcExpNeeded(level): 299 | sum1 -= self.calcExpNeeded(level) 300 | level += 1 301 | self.lv = level 302 | self.exp = sum1 303 | self.expneeded = self.calcExpNeeded(level) - sum1 304 | if level > 1: 305 | #get Excited ;-) 306 | self.displayLevelUp(agent) 307 | 308 | def calcActualSum(self, level, exp): 309 | lvlCounter = 1 310 | sum = exp 311 | # I know it wouldn't work if you change the lvl algorithm 312 | while lvlCounter < level: 313 | sum += self.calcExpNeeded(lvlCounter) 314 | lvlCounter += 1 315 | return sum 316 | 317 | def displayLevelUp(self, agent): 318 | view = agent.view() 319 | view.set('face', FACE_LEVELUP) 320 | view.set('status', "Level Up!") 321 | view.update(force=True) 322 | 323 | def calcStrength(self): 324 | # Custom formula for strength calculation 325 | self.strength = self.exp * self.lv * 0.05 326 | 327 | # Event Handling 328 | def on_association(self, agent, access_point): 329 | self.exp += MULTIPLIER_ASSOCIATION 330 | self.exp_tot += MULTIPLIER_ASSOCIATION 331 | self.exp_check(agent) 332 | self.Save(self.save_file, self.save_file_mode) 333 | 334 | def on_deauthentication(self, agent, access_point, client_station): 335 | self.exp += MULTIPLIER_DEAUTH 336 | self.exp_tot += MULTIPLIER_DEAUTH 337 | self.exp_check(agent) 338 | self.Save(self.save_file, self.save_file_mode) 339 | 340 | def on_handshake(self, agent, filename, access_point, client_station): 341 | self.exp += MULTIPLIER_HANDSHAKE 342 | self.exp_tot += MULTIPLIER_HANDSHAKE 343 | self.exp_check(agent) 344 | self.Save(self.save_file, self.save_file_mode) 345 | 346 | def on_ai_best_reward(self, agent, reward): 347 | self.exp += MULTIPLIER_AI_BEST_REWARD 348 | self.exp_tot += MULTIPLIER_AI_BEST_REWARD 349 | self.exp_check(agent) 350 | self.Save(self.save_file, self.save_file_mode) 351 | 352 | def on_ready(self, agent): 353 | if self.calculateInitialXP: 354 | self.LogInfo("Initial point calculation") 355 | sum = self.calculateInitialSum(agent) 356 | self.exp_tot = sum 357 | self.calcLevelFromSum(sum, agent) 358 | self.Save(self.save_file, self.save_file_mode) 359 | -------------------------------------------------------------------------------- /plugins/ups_hat_c.py: -------------------------------------------------------------------------------- 1 | # functions for get UPS status - needs enable "i2c" in raspi-config, smbus installed (sudo apt-get install -y python-smbus) 2 | 3 | 4 | import logging 5 | import time 6 | 7 | import pwnagotchi 8 | import pwnagotchi.plugins as plugins 9 | import pwnagotchi.ui.fonts as fonts 10 | from pwnagotchi.ui.components import LabeledValue 11 | from pwnagotchi.ui.view import BLACK 12 | 13 | # Config Register (R/W) 14 | _REG_CONFIG = 0x00 15 | # SHUNT VOLTAGE REGISTER (R) 16 | _REG_SHUNTVOLTAGE = 0x01 17 | 18 | # BUS VOLTAGE REGISTER (R) 19 | _REG_BUSVOLTAGE = 0x02 20 | 21 | # POWER REGISTER (R) 22 | _REG_POWER = 0x03 23 | 24 | # CURRENT REGISTER (R) 25 | _REG_CURRENT = 0x04 26 | 27 | # CALIBRATION REGISTER (R/W) 28 | _REG_CALIBRATION = 0x05 29 | 30 | 31 | class UPS: 32 | def __init__(self): 33 | # only import when the module is loaded and enabled 34 | import smbus 35 | self._bus = smbus.SMBus(1) 36 | self._addr = 0x43 37 | 38 | # Set chip to known config values to start 39 | self._cal_value = 0 40 | self._current_lsb = 0 41 | self._power_lsb = 0 42 | self.set_calibration_32V_2A() 43 | 44 | def read(self, address): 45 | data = self._bus.read_i2c_block_data(self._addr, address, 2) 46 | return ((data[0] * 256) + data[1]) 47 | 48 | def write(self, address, data): 49 | temp = [0, 0] 50 | temp[1] = data & 0xFF 51 | temp[0] = (data & 0xFF00) >> 8 52 | self._bus.write_i2c_block_data(self._addr, address, temp) 53 | 54 | def set_calibration_32V_2A(self): 55 | """Configures to INA219 to be able to measure up to 32V and 2A of current. Counter 56 | overflow occurs at 3.2A. 57 | ..note :: These calculations assume a 0.1 shunt ohm resistor is present 58 | """ 59 | 60 | self._cal_value = 0 61 | self._current_lsb = 1 # Current LSB = 100uA per bit 62 | self._cal_value = 4096 63 | self._power_lsb = .002 # Power LSB = 2mW per bit 64 | 65 | # Set Calibration register to 'Cal' calculated above 66 | self.write(_REG_CALIBRATION, self._cal_value) 67 | 68 | # Set Config register to take into account the settings above 69 | self.bus_voltage_range = 0x01 70 | self.gain = 0x03 71 | self.bus_adc_resolution = 0x0D 72 | self.shunt_adc_resolution = 0x0D 73 | self.mode = 0x07 74 | self.config = self.bus_voltage_range << 13 | \ 75 | self.gain << 11 | \ 76 | self.bus_adc_resolution << 7 | \ 77 | self.shunt_adc_resolution << 3 | \ 78 | self.mode 79 | self.write(_REG_CONFIG, self.config) 80 | 81 | def getBusVoltage_V(self): 82 | self.write(_REG_CALIBRATION, self._cal_value) 83 | self.read(_REG_BUSVOLTAGE) 84 | return (self.read(_REG_BUSVOLTAGE) >> 3) * 0.004 85 | 86 | def getCurrent_mA(self): 87 | value = self.read(_REG_CURRENT) 88 | if value > 32767: 89 | value -= 65535 90 | if (value * self._current_lsb) < 0: 91 | return "" 92 | else: 93 | return "+" 94 | 95 | 96 | class UPSC(plugins.Plugin): 97 | __author__ = 'HannaDiamond' 98 | __version__ = '1.0.1' 99 | __license__ = 'MIT' 100 | __description__ = 'A plugin that will add a battery capacity and charging indicator for the UPS HAT C' 101 | 102 | def __init__(self): 103 | self.ups = None 104 | 105 | def on_loaded(self): 106 | self.ups = UPS() 107 | 108 | def on_ui_setup(self, ui): 109 | if self.options["label_on"]: 110 | ui.add_element('ups', LabeledValue(color=BLACK, label='BAT', value="--%", 111 | position=(int(self.options["bat_x_coord"]), 112 | int(self.options["bat_y_coord"])), 113 | label_font=fonts.Bold, text_font=fonts.Medium)) 114 | else: 115 | ui.add_element('ups', LabeledValue(color=BLACK, label='', value="--%", 116 | position=(int(self.options["bat_x_coord"]), 117 | int(self.options["bat_y_coord"])), 118 | label_font=fonts.Bold, text_font=fonts.Medium)) 119 | 120 | def on_unload(self, ui): 121 | with ui._lock: 122 | ui.remove_element('ups') 123 | 124 | def on_ui_update(self, ui): 125 | bus_voltage = self.ups.getBusVoltage_V() 126 | capacity = int((bus_voltage - 3) / 1.2 * 100) 127 | if (capacity > 100): capacity = 100 128 | if (capacity < 0): capacity = 0 129 | 130 | charging = self.ups.getCurrent_mA() 131 | ui.set('ups', str(capacity) + "%" + charging) 132 | 133 | if capacity <= self.options['shutdown']: 134 | logging.info('[ups_hat_c] Empty battery (<= %s%%): shutting down' % self.options['shutdown']) 135 | ui.update(force=True, new_data={'status': 'Battery exhausted, bye ...'}) 136 | time.sleep(3) 137 | pwnagotchi.shutdown() 138 | -------------------------------------------------------------------------------- /waveshare_37inch/waveshare3in7.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pwnagotchi.ui.fonts as fonts 4 | from pwnagotchi.ui.hw.base import DisplayImpl 5 | 6 | 7 | class Waveshare3in7(DisplayImpl): 8 | def __init__(self, config): 9 | super(Waveshare3in7, self).__init__(config, 'waveshare3in7') 10 | 11 | def layout(self): 12 | fonts.setup(20, 19, 20, 45, 35, 19) 13 | self._layout['width'] = 480 14 | self._layout['height'] = 280 15 | self._layout['face'] = (0, 75) 16 | self._layout['name'] = (5, 35) 17 | self._layout['channel'] = (0, 0) 18 | self._layout['aps'] = (65, 0) 19 | self._layout['uptime'] = (355, 0) 20 | self._layout['line1'] = [0, 25, 480, 25] 21 | self._layout['line2'] = [0, 255, 480, 255] 22 | self._layout['friend_face'] = (0, 146) 23 | self._layout['friend_name'] = (40, 146) 24 | self._layout['shakes'] = (0, 258) 25 | self._layout['mode'] = (430, 258) 26 | self._layout['status'] = { 27 | 'pos': (225, 35), 28 | 'font': fonts.status_font(fonts.Medium), 29 | 'max': 21 30 | } 31 | return self._layout 32 | 33 | def initialize(self): 34 | logging.info("initializing waveshare 3.7 inch lcd display") 35 | from pwnagotchi.ui.hw.libs.waveshare.epaper.v3in7.epd3in7 import EPD 36 | self._display = EPD() 37 | self._display.init(0) 38 | self._display.Clear(0) 39 | self._display.init(1) # 1Gray mode 40 | 41 | def render(self, canvas): 42 | buf = self._display.getbuffer(canvas) 43 | self._display.display_1Gray(buf) 44 | 45 | def clear(self): 46 | self._display.Clear(0) 47 | --------------------------------------------------------------------------------