├── README.md ├── darkmode.py ├── gps_error.py ├── gps_fix.py ├── gps_fix.yml ├── gps_grid.py ├── gps_live.py ├── gps_sat.py ├── gps_sat.yml ├── pwnagotchi1.jpg ├── rtc_grid.py ├── rtc_grid.yml ├── tracker.py ├── ups_lite_1_2.py ├── ups_lite_1_2.yml ├── ups_lite_1_3.py ├── ups_lite_1_3.yml ├── waveshare_v3_touch.py ├── xp.py ├── xp.yml ├── xp_grid.py └── xp_grid.yml /README.md: -------------------------------------------------------------------------------- 1 | # Custom pwnagotchi plugins 2 | 3 | These plugins have been tested in pwnagotchi v1.5.5 4 | 5 | - [darkmode](#darkmode) 6 | - [gps_error](#gps_error) 7 | - [gps_fix](#gps_fix) 8 | - [gps_grid](#gps_grid) 9 | - [gps_live](#gps_live) 10 | - [gps_sat](#gps_sat) 11 | - [rtc_grid](#rtc_grid) 12 | - [tracker](#tracker) 13 | - [ups_lite_1_2](#ups_lite_1_2) 14 | - [ups_lite_1_3](#ups_lite_1_3) 15 | - [waveshare_v3_touch](#waveshare_v3_touch) 16 | - [xp](#xp) 17 | - [xp_grid](#xp_grid) 18 | 19 | ![demo](/pwnagotchi1.jpg) 20 | 21 | ## darkmode 22 | 23 | Plugin to enable/disable darkmode by modifying `pwnagotchi.ui.view` constants and update current ui elements. 24 | 25 | ### Configuration 26 | 27 | ``` 28 | main.plugins.darkmode.enabled = true 29 | ``` 30 | 31 | ## gps_error 32 | 33 | Plugin to display GPS errors in case the default `gps` plugin: 34 | - is not loaded 35 | - is not running 36 | - receive no data 37 | - is not fixed (FixQuality=0) 38 | 39 | Errors are displayed in the default `latitude` ui element. 40 | 41 | This plugin requires the default `gps` plugin enabled. 42 | 43 | ### Configuration 44 | 45 | ``` 46 | main.plugins.gps_error.enabled = true 47 | ``` 48 | 49 | ## gps_fix 50 | 51 | Plugin to diplay current GPS fix quality. 52 | 53 | This plugin requires the default `gps` plugin enabled. 54 | 55 | ### Configuration 56 | 57 | ``` 58 | main.plugins.gps_fix.enabled = true 59 | main.plugins.gps_fix.position = "0,83" 60 | ``` 61 | 62 | ## gps_grid 63 | 64 | Plugin to share current GPS data to other units with grid. 65 | Maybe you don't want to enable this as it send current position in advertissement packets! 66 | 67 | This plugin requires the default `gps` plugin enabled. 68 | 69 | ### Configuration 70 | 71 | ``` 72 | main.plugins.gps_grid.enabled = true 73 | ``` 74 | 75 | ## gps_live 76 | 77 | Plugin to update GPS coordinates on each epoch. 78 | 79 | This plugin requires the default `gps` plugin enabled. 80 | 81 | ### Configuration 82 | 83 | ``` 84 | main.plugins.gps_live.enabled = true 85 | ``` 86 | 87 | ## gps_sat 88 | 89 | Plugin to display current number of sattelites. 90 | 91 | This plugin requires the default `gps` plugin enabled. 92 | 93 | ### Configuration 94 | 95 | ``` 96 | main.plugins.gps_sat.enabled = true 97 | main.plugins.gps_sat.position = "90,83" 98 | ``` 99 | 100 | ## rtc_grid 101 | 102 | Plugin to share timestamp from RTC module with peers. 103 | 104 | For units with RTC module: a clock will be displayed and the current timestamp will be shared with grid. 105 | For units without RTC module: they will see if some peers (good friends) share their timestamp and set time from it. 106 | 107 | ### Configuration 108 | 109 | ``` 110 | main.plugins.rtc_grid.enabled = true 111 | main.plugins.rtc_grid.position = "100,-1" 112 | main.plugins.rtc_grid.peer_position = "242,94" 113 | ``` 114 | 115 | ## tracker 116 | 117 | Plugin to track every APs and clients seen. 118 | 119 | ### Configuration 120 | 121 | ``` 122 | main.plugins.tracker.enabled = true 123 | ``` 124 | 125 | This plugin create many files depending of your environment, files are located in `/var/local/pwnagotchi/tracker` directory. It can be useful to mount this directory in memory with this configuration: 126 | 127 | ``` 128 | fs.memory.mounts.data.enabled = true 129 | fs.memory.mounts.data.mount = "/var/local" 130 | fs.memory.mounts.data.size = "50M" 131 | fs.memory.mounts.data.sync = 60 132 | fs.memory.mounts.data.zram = true 133 | fs.memory.mounts.data.rsync = true 134 | ``` 135 | 136 | See https://pwnagotchi.ai/configuration/#sdcard-protection 137 | 138 | ## ups_lite_1_2 139 | 140 | This is a copy of the default `ups_lite` plugins with some additions: 141 | - Don't shutdown if battery is charging 142 | 143 | ### Configuration 144 | 145 | ``` 146 | main.plugins.ups_lite_1_2.enabled = true 147 | main.plugins.ups_lite_1_2.shutdown = 5 148 | ``` 149 | 150 | ## ups_lite_1_3 151 | 152 | This is a copy of the default `ups_lite` plugins with some additions: 153 | - Support UPSLite v1.3 154 | - Don't shutdown if battery is charging 155 | 156 | ### Configuration 157 | 158 | ``` 159 | main.plugins.ups_lite_1_3.enabled = true 160 | main.plugins.ups_lite_1_3.shutdown = 5 161 | ``` 162 | 163 | ## waveshare_v3_touch 164 | 165 | Experimental plugin to play with Waveshare v3 touch screen. 166 | This plugin is not currently working.. 167 | 168 | ### Configuration 169 | 170 | ``` 171 | main.plugins.waveshare_v3_touch.enabled = false 172 | ``` 173 | 174 | ## xp 175 | 176 | Plugin implementing xp/level/ranks for your unit. 177 | 178 | - New rank every 5 levels 179 | - Custom heads for each rank 180 | - Provide web ui to follow progression 181 | 182 | ### Configuration 183 | 184 | ``` 185 | main.plugins.xp.enabled = true 186 | main.plugins.xp.load_initial_xp = false 187 | main.plugins.xp.level_position = "10,30" 188 | main.plugins.xp.rank_position = "50,30" 189 | main.plugins.xp.progressbar_position = "20,40,103,44" 190 | ``` 191 | 192 | ## xp_grid 193 | 194 | Plugin to share `xp` plugin data (level, rank) with peers. 195 | 196 | ### Configuration 197 | 198 | ``` 199 | main.plugins.xp_grid.enabled = true 200 | main.plugins.xp_grid.position = "36,95" 201 | main.plugins.xp_grid.name_position = "63,95" 202 | ``` 203 | 204 | This plugin requires the `xp` plugin enabled. 205 | -------------------------------------------------------------------------------- /darkmode.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi 3 | import pwnagotchi.plugins as plugins 4 | 5 | 6 | class Darkmode(plugins.Plugin): 7 | __author__ = "sliim@mailoo.org" 8 | __version__ = "1.0.0" 9 | __license__ = "GPL3" 10 | __description__ = "Darkmode plugin." 11 | 12 | def _update_ui_colors(self, ui): 13 | for key, element in ui._state.items(): 14 | if element.color != pwnagotchi.ui.view.BLACK: 15 | logging.warning("[darkmode] Update element color: %s" % key) 16 | element.color = pwnagotchi.ui.view.BLACK 17 | ui.remove_element(key) 18 | ui.add_element(key, element) 19 | 20 | def on_loaded(self): 21 | try: 22 | pwnagotchi.ui.view.BLACK = 0xff 23 | pwnagotchi.ui.view.WHITE = 0x00 24 | logging.info("[darkmode] plugin loaded") 25 | except Exception as e: 26 | logging.error("darkmode.on_loaded: %s" % e) 27 | 28 | def on_unload(self, ui): 29 | try: 30 | pwnagotchi.ui.view.BLACK = 0x00 31 | pwnagotchi.ui.view.WHITE = 0xff 32 | self._update_ui_colors(ui) 33 | logging.info("[darkmode] plugin unloaded") 34 | except Exception as e: 35 | logging.error("darkmode.on_unload: %s" % e) 36 | 37 | def on_ui_update(self, ui): 38 | try: 39 | self._update_ui_colors(ui) 40 | except Exception as e: 41 | logging.error("darkmode.on_ui_update: %s" % e) 42 | -------------------------------------------------------------------------------- /gps_error.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi.plugins as plugins 3 | 4 | 5 | class GPSError(plugins.Plugin): 6 | __author__ = "sliim@mailoo.org" 7 | __version__ = "1.0.0" 8 | __license__ = "GPL3" 9 | __description__ = """ 10 | Display error when GPS is not running. 11 | Requires gps plugin enabled. 12 | """ 13 | 14 | def __init__(self): 15 | self.gps = None 16 | 17 | def on_loaded(self): 18 | try: 19 | logging.info("[gps_error] plugin loaded") 20 | except Exception as e: 21 | logging.error("gps_error.on_loaded: %s" % e) 22 | 23 | def on_ready(self, agent): 24 | try: 25 | self.gps = plugins.loaded["gps"] 26 | if not self.gps: 27 | logging.error("[gps_error] gps plugin not loaded!") 28 | except Exception as e: 29 | logging.error("gps_error.on_ready: %s" % e) 30 | 31 | def on_ui_update(self, ui): 32 | try: 33 | if not self.gps: 34 | ui.set("latitude", "Not loaded") 35 | elif not self.gps.running: 36 | ui.set("latitude", "Not running") 37 | elif not self.gps.coordinates: 38 | ui.set("latitude", "No data") 39 | elif not all([self.gps.coordinates["Latitude"], 40 | self.gps.coordinates["Longitude"], 41 | self.gps.coordinates["FixQuality"]]): 42 | ui.set("latitude", "Not fixed") 43 | except Exception as e: 44 | logging.error("gps_error.on_ui_update: %s" % e) 45 | -------------------------------------------------------------------------------- /gps_fix.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi.ui.fonts as fonts 3 | import pwnagotchi.plugins as plugins 4 | from pwnagotchi.ui.components import LabeledValue 5 | from pwnagotchi.ui.view import BLACK 6 | 7 | 8 | class GPSFix(plugins.Plugin): 9 | __author__ = "sliim@mailoo.org" 10 | __version__ = "1.0.0" 11 | __license__ = "GPL3" 12 | __description__ = "Display GPS fix quality. Requires gps plugin enabled." 13 | 14 | def __init__(self): 15 | self.gps = None 16 | 17 | def on_loaded(self): 18 | try: 19 | if "position" not in self.options or \ 20 | not self.options["position"] or \ 21 | len(self.options["position"].split(",")) != 2: 22 | self.options["position"] = "0,83" 23 | logging.info("[gps_fix] plugin loaded") 24 | except Exception as e: 25 | logging.error("gps_fix.on_loaded: %s" % e) 26 | 27 | def on_ready(self, agent): 28 | try: 29 | self.gps = plugins.loaded["gps"] 30 | if not self.gps: 31 | logging.error("[gps_fix] gps plugin not loaded!") 32 | except Exception as e: 33 | logging.error("gps_fix.on_ready: %s" % e) 34 | 35 | def on_ui_setup(self, ui): 36 | try: 37 | pos = self.options["position"].split(",") 38 | ui.add_element('fixquality', LabeledValue( 39 | color=BLACK, 40 | label='fix', 41 | value="-", 42 | position=(int(pos[0]), int(pos[1])), 43 | label_font=fonts.Small, 44 | text_font=fonts.Small)) 45 | except Exception as e: 46 | logging.error("gps_fix.on_ui_setup: %s" % e) 47 | 48 | def on_ui_update(self, ui): 49 | try: 50 | if self.gps and self.gps.coordinates: 51 | desc = "Not fixed" 52 | fix = self.gps.coordinates["FixQuality"] 53 | if not fix: 54 | fix = "0" 55 | fix = int(fix) 56 | 57 | if fix == 1: 58 | desc = "GNSS" 59 | elif fix == 2: 60 | desc = "DGPS" 61 | elif fix == 3: 62 | desc = "GPS PPS" 63 | elif fix == 4: 64 | desc = "RTK fixed" 65 | elif fix == 5: 66 | desc = "RTK float" 67 | 68 | ui.set("fixquality", "%d (%s)" % (fix, desc)) 69 | else: 70 | ui.set("fixquality", "-") 71 | except Exception as e: 72 | logging.error("gps_fix.on_ui_update: %s" % e) 73 | 74 | def on_unload(self, ui): 75 | try: 76 | with ui._lock: 77 | ui.remove_element("fixquality") 78 | except Exception as e: 79 | logging.error("gps_fix.on_unload: %s" % e) 80 | -------------------------------------------------------------------------------- /gps_fix.yml: -------------------------------------------------------------------------------- 1 | gps_fix: 2 | enabled: false 3 | position: "0,83" 4 | -------------------------------------------------------------------------------- /gps_grid.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi.grid as grid 3 | import pwnagotchi.plugins as plugins 4 | 5 | 6 | class GPSGrid(plugins.Plugin): 7 | __author__ = "sliim@mailoo.org" 8 | __version__ = "1.0.0" 9 | __license__ = "GPL3" 10 | __description__ = """ 11 | Share GPS coordinates with grid. 12 | Requires gps plugin enabled. 13 | """ 14 | 15 | def __init__(self): 16 | self.gps = None 17 | self.coordinates = None 18 | 19 | def _coords(self): 20 | if self.gps and self.gps.running: 21 | coordinates = self.gps.coordinates 22 | if coordinates \ 23 | and all([coordinates["Latitude"], coordinates["Longitude"]]): 24 | return coordinates 25 | return None 26 | 27 | def on_loaded(self): 28 | try: 29 | logging.info("[gps_grid] plugin loaded") 30 | except Exception as e: 31 | logging.error("gps_grid.on_loaded: %s" % e) 32 | 33 | def on_ready(self, agent): 34 | try: 35 | self.gps = plugins.loaded["gps"] 36 | if not self.gps: 37 | logging.error("[gps_grid] gps plugin not loaded!") 38 | except Exception as e: 39 | logging.error("gps_grid.on_ready: %s" % e) 40 | 41 | def on_epoch(self, agent, epoch, data): 42 | try: 43 | coords = self._coords() 44 | if coords and coords != self.coordinates: 45 | grid.call("/mesh/data", {"coordinates": coords}) 46 | self.coordinates = coords 47 | except Exception as e: 48 | logging.error("gps_grid.on_epoch: %s" % e) 49 | -------------------------------------------------------------------------------- /gps_live.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi.plugins as plugins 3 | 4 | 5 | class GPSLive(plugins.Plugin): 6 | __author__ = "sliim@mailoo.org" 7 | __version__ = "1.0.0" 8 | __license__ = "GPL3" 9 | __description__ = """ 10 | Update GPS coordinates on each epoch. 11 | Requires gps plugin enabled. 12 | """ 13 | 14 | def __init__(self): 15 | self.gps = None 16 | 17 | def on_loaded(self): 18 | try: 19 | logging.info("[gps_live] plugin loaded") 20 | except Exception as e: 21 | logging.error("gps_live.on_loaded: %s" % e) 22 | 23 | def on_ready(self, agent): 24 | try: 25 | self.gps = plugins.loaded["gps"] 26 | if not self.gps: 27 | logging.error("[gps_live] gps plugin not loaded!") 28 | except Exception as e: 29 | logging.error("gps_live.on_ready: %s" % e) 30 | 31 | def on_epoch(self, agent, epoch, data): 32 | try: 33 | if self.gps and self.gps.running: 34 | self.gps.coordinates = agent.session()["gps"] 35 | except Exception as e: 36 | logging.error("gps_live.on_epoch: %s" % e) 37 | 38 | def on_ui_update(self, ui): 39 | try: 40 | if self.gps and self.gps.running: 41 | coords = self.gps.coordinates 42 | if not coords or not all([coords["Latitude"], 43 | coords["Longitude"]]): 44 | ui.set("latitude", "-") 45 | ui.set("longitude", "-") 46 | ui.set("altitude", "-") 47 | except Exception as e: 48 | logging.error("gps_live.on_ui_update: %s" % e) 49 | -------------------------------------------------------------------------------- /gps_sat.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi.ui.fonts as fonts 3 | import pwnagotchi.plugins as plugins 4 | from pwnagotchi.ui.components import LabeledValue 5 | from pwnagotchi.ui.view import BLACK 6 | 7 | 8 | class GPSSat(plugins.Plugin): 9 | __author__ = "sliim@mailoo.org" 10 | __version__ = "1.0.0" 11 | __license__ = "GPL3" 12 | __description__ = """ 13 | Display number of satellites. 14 | Requires gps plugin enabled. 15 | """ 16 | 17 | def __init__(self): 18 | self.gps = None 19 | 20 | def on_loaded(self): 21 | try: 22 | if "position" not in self.options or \ 23 | not self.options["position"] or \ 24 | len(self.options["position"].split(",")) != 2: 25 | self.options["position"] = "90,83" 26 | logging.info("[gps_sat] plugin loaded") 27 | except Exception as e: 28 | logging.error("gps_sat.on_loaded: %s" % e) 29 | 30 | def on_ready(self, agent): 31 | try: 32 | self.gps = plugins.loaded["gps"] 33 | if not self.gps: 34 | logging.error("[gps_sat] gps plugin not loaded!") 35 | except Exception as e: 36 | logging.error("gps_sat.on_ready: %s" % e) 37 | 38 | def on_ui_setup(self, ui): 39 | try: 40 | pos = self.options["position"].split(",") 41 | ui.add_element('sattelites', LabeledValue( 42 | color=BLACK, 43 | label='sat', 44 | value="0", 45 | position=(int(pos[0]), int(pos[1])), 46 | label_font=fonts.Small, 47 | text_font=fonts.Small)) 48 | except Exception as e: 49 | logging.error("gps_sat.on_ui_setup: %s" % e) 50 | 51 | def on_ui_update(self, ui): 52 | try: 53 | if self.gps and self.gps.coordinates: 54 | ui.set("sattelites", str(self.gps.coordinates["NumSatellites"])) 55 | except Exception as e: 56 | logging.error("gps_sat.on_ui_update: %s" % e) 57 | 58 | def on_unload(self, ui): 59 | try: 60 | with ui._lock: 61 | ui.remove_element("sattelites") 62 | except Exception as e: 63 | logging.error("gps_sat.on_unload: %s" % e) 64 | -------------------------------------------------------------------------------- /gps_sat.yml: -------------------------------------------------------------------------------- 1 | gps_sat: 2 | enabled: false 3 | position: "90,83" 4 | -------------------------------------------------------------------------------- /pwnagotchi1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sliim/pwnagotchi-plugins/49feb6a0bfa6810d3dd32b07cf361338d357e2ae/pwnagotchi1.jpg -------------------------------------------------------------------------------- /rtc_grid.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import ctypes.util 3 | import datetime 4 | import logging 5 | import smbus 6 | import time 7 | import pwnagotchi.grid as grid 8 | import pwnagotchi.plugins as plugins 9 | import pwnagotchi.ui.fonts as fonts 10 | from pwnagotchi.ui.components import Text 11 | from pwnagotchi.ui.view import BLACK 12 | 13 | CLOCK = "◴" 14 | CLOCK_REALTIME = 0 15 | 16 | 17 | class timespec(ctypes.Structure): 18 | _fields_ = [("tv_sec", ctypes.c_long), 19 | ("tv_nsec", ctypes.c_long)] 20 | 21 | 22 | class RTCGrid(plugins.Plugin): 23 | __author__ = "sliim@mailoo.org" 24 | __version__ = "1.0.0" 25 | __license__ = "GPL3" 26 | __description__ = """ 27 | Share RTC clock with peers 28 | Some part of this code is based from: 29 | https://stackoverflow.com/questions/12081310 30 | """ 31 | 32 | def __init__(self): 33 | self.rtc = False 34 | self.peer_rtc = None 35 | self.friends = {} 36 | 37 | def _set_local_clock(self): 38 | if not self.peer_rtc or not self.peer_rtc.adv.get("rtc"): 39 | return False 40 | 41 | logging.info("[rtc_grid] Set datetime from peer %s", 42 | self.peer_rtc.full_name()) 43 | 44 | time_tuple = datetime.datetime.fromtimestamp( 45 | self.peer_rtc.adv.get("rtc")["timestamp"] 46 | ).timetuple() 47 | 48 | librt = ctypes.CDLL(ctypes.util.find_library("rt")) 49 | 50 | ts = timespec() 51 | ts.tv_sec = int(time.mktime( 52 | datetime.datetime(*time_tuple[:6]).timetuple())) 53 | ts.tv_nsec = time_tuple[6] * 1000000 54 | 55 | # http://linux.die.net/man/3/clock_settime 56 | librt.clock_settime(CLOCK_REALTIME, ctypes.byref(ts)) 57 | 58 | def on_loaded(self): 59 | try: 60 | if "position" not in self.options or \ 61 | not self.options["position"] or \ 62 | len(self.options["position"].split(",")) != 2: 63 | self.options["position"] = "100,-1" 64 | if "peer_position" not in self.options or \ 65 | not self.options["peer_position"] or \ 66 | len(self.options["peer_position"].split(",")) != 2: 67 | self.options["peer_position"] = "242,94" 68 | logging.info("[rtc_grid] plugin loaded") 69 | except Exception as e: 70 | logging.error("rtc_grid.on_loaded: %s" % e) 71 | 72 | def on_ready(self, agent): 73 | try: 74 | bus = smbus.SMBus(1) 75 | bus.read_byte(0x68) 76 | logging.info("RTC module found.") 77 | self.rtc = True 78 | except OSError as e: 79 | if e.errno == 16: 80 | logging.info("RTC module found (busy).") 81 | self.rtc = True 82 | else: 83 | logging.warning("RTC module not found: %s" % e) 84 | except Exception as e: 85 | logging.error("rtc_grid.on_ready: %s" % e) 86 | 87 | def on_epoch(self, agent, epoch, data): 88 | try: 89 | if self.rtc: 90 | grid.call("/mesh/data", {"rtc": {"timestamp": time.time()}}) 91 | except Exception as e: 92 | logging.error("rtc_grid.on_epoch: %s" % e) 93 | 94 | def on_peer_detected(self, agent, peer): 95 | try: 96 | if not peer.identity() in self.friends and peer.is_good_friend(agent.config()): 97 | self.friends[peer.identity()] = peer 98 | except Exception as e: 99 | logging.error("rtc_grid.on_peer_detected: %s" % e) 100 | 101 | def on_ui_setup(self, ui): 102 | try: 103 | pos = self.options["position"].split(",") 104 | ui.add_element('rtc', Text( 105 | color=BLACK, 106 | value=None, 107 | position=(int(pos[0]), int(pos[1])), 108 | font=fonts.Bold)) 109 | pos = self.options["peer_position"].split(",") 110 | ui.add_element('rtc_peer', Text( 111 | color=BLACK, 112 | value=None, 113 | position=(int(pos[0]), int(pos[1])), 114 | font=fonts.Bold)) 115 | except Exception as e: 116 | logging.error("rtc_grid.on_ui_setup: %s" % e) 117 | 118 | def on_ui_update(self, ui): 119 | try: 120 | if not self.rtc and not self.peer_rtc: 121 | for i, p in self.friends.items(): 122 | if p.adv.get("rtc"): 123 | self.peer_rtc = p 124 | 125 | if self.rtc: 126 | ui.set("rtc", CLOCK) 127 | elif self.peer_rtc: 128 | friend = ui.get("friend_name") 129 | if not friend: 130 | ui.set("rtc_peer", None) 131 | return False 132 | 133 | peer = friend.split(" ") 134 | if self.peer_rtc.name() == peer[1] \ 135 | and str(self.peer_rtc.pwnd_run()) == peer[2] \ 136 | and "(%d)" % self.peer_rtc.pwnd_total() == peer[3]: 137 | ui.set("rtc_peer", CLOCK) 138 | else: 139 | ui.set("rtc_peer", None) 140 | 141 | if time.time() < self.peer_rtc.adv.get("rtc")["timestamp"]: 142 | self._set_local_clock() 143 | except Exception as e: 144 | logging.error("rtc_grid.on_ui_update: %s" % e) 145 | 146 | def on_unload(self, ui): 147 | try: 148 | with ui._lock: 149 | ui.remove_element('rtc') 150 | ui.remove_element('rtc_peer') 151 | except Exception as e: 152 | logging.error("rtc_grid.on_unload: %s" % e) 153 | -------------------------------------------------------------------------------- /rtc_grid.yml: -------------------------------------------------------------------------------- 1 | rtc_grid: 2 | enabled: false 3 | position: "100,-1" 4 | peer_position: "242,94" 5 | -------------------------------------------------------------------------------- /tracker.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import pwnagotchi.plugins as plugins 5 | 6 | 7 | class Tracker(plugins.Plugin): 8 | __author__ = "sliim@mailoo.org" 9 | __version__ = "1.0.0" 10 | __license__ = "GPL3" 11 | __description__ = """ 12 | Track seen access points & clients. 13 | If gps plugin is enabled, position will be saved. 14 | """ 15 | 16 | def __init__(self): 17 | self.gps = None 18 | self.tracker_dir = "/var/local/pwnagotchi/tracker" 19 | self.known_devices = {} 20 | 21 | def _coords(self): 22 | if self.gps and self.gps.running: 23 | coordinates = self.gps.coordinates 24 | if coordinates \ 25 | and all([coordinates["Latitude"], coordinates["Longitude"]]): 26 | return coordinates 27 | return None 28 | 29 | def _save_device(self, filename, data): 30 | filename = filename.replace("/", "_").replace(" ", "_") 31 | if not os.path.isdir(self.tracker_dir): 32 | logging.error("[tracker] %s is not a directory. Device not saved." 33 | % self.tracker_dir) 34 | return False 35 | 36 | olddata = {} 37 | 38 | if data["mac"] in self.known_devices: 39 | olddata = self.known_devices[data["mac"]] 40 | elif os.path.exists("%s/%s" % (self.tracker_dir, filename)): 41 | with open("%s/%s" % (self.tracker_dir, filename), "r") as f: 42 | olddata = json.load(f) 43 | self.known_devices[olddata["mac"]] = olddata 44 | 45 | if "rssi" not in data: 46 | logging.debug("[tracker] Device %s not saved because rssi key is missing" 47 | % data["mac"]) 48 | return False 49 | 50 | if olddata and "rssi" in olddata \ 51 | and olddata["rssi"] > data["rssi"] \ 52 | and "coordinates" in olddata: 53 | logging.debug("[tracker] Known device %s not updated because new rssi (%d) < old rssi (%d) and GPS position is already known" 54 | % (data["mac"], data["rssi"], olddata["rssi"])) 55 | return False 56 | 57 | coords = self._coords() 58 | if coords: 59 | data["coordinates"] = coords 60 | elif olddata: 61 | logging.debug("[tracker] Known device %s not updated because we don't have new GPS position" 62 | % data["mac"]) 63 | return False 64 | 65 | olddata.update(data) 66 | 67 | self.known_devices[olddata["mac"]] = olddata 68 | with open("%s/%s" % (self.tracker_dir, filename), "w") as f: 69 | f.write(json.dumps(olddata)) 70 | logging.debug("[tracker] Updated device %s" % data["mac"]) 71 | 72 | def _save_ap(self, ap): 73 | self._save_device("%s_%s.json" % (ap["hostname"], ap["mac"].replace(":", "").lower()), ap) 74 | 75 | def _save_client(self, ap, client): 76 | self._save_device("%s_client_%s.json" % (ap["hostname"], client["mac"].replace(":", "").lower()), client) 77 | 78 | def on_loaded(self): 79 | try: 80 | if not os.path.exists(self.tracker_dir): 81 | os.makedirs(self.tracker_dir, exist_ok=True) 82 | logging.info("[tracker] plugin loaded") 83 | except Exception as e: 84 | logging.error("tracker.on_loaded: %s" % e) 85 | 86 | def on_ready(self, agent): 87 | try: 88 | self.gps = plugins.loaded["gps"] 89 | if not self.gps: 90 | logging.warning("[tracker] gps plugin not loaded! Coordinates will not be saved.") 91 | except Exception as e: 92 | logging.error("tracker.on_ready: %s" % e) 93 | 94 | def on_wifi_update(self, agent, aps): 95 | try: 96 | for ap in aps: 97 | self._save_ap(ap) 98 | if ap["clients"]: 99 | for client in ap["clients"]: 100 | self._save_client(ap, client) 101 | except Exception as e: 102 | logging.error("tracker.on_wifi_update: %s" % e) 103 | 104 | def on_association(self, agent, ap): 105 | try: 106 | self._save_ap(ap) 107 | if ap["clients"]: 108 | for client in ap["clients"]: 109 | self._save_client(ap, client) 110 | except Exception as e: 111 | logging.error("tracker.on_association: %s" % e) 112 | 113 | def on_deauthentication(self, agent, ap, client): 114 | try: 115 | self._save_ap(ap) 116 | self._save_client(ap, client) 117 | if ap["clients"]: 118 | for client in ap["clients"]: 119 | self._save_client(ap, client) 120 | except Exception as e: 121 | logging.error("tracker.on_deauthentication: %s" % e) 122 | 123 | def on_handshake(self, agent, filename, ap, client): 124 | try: 125 | self._save_ap(ap) 126 | self._save_client(ap, client) 127 | if ap["clients"]: 128 | for client in ap["clients"]: 129 | self._save_client(ap, client) 130 | except Exception as e: 131 | logging.error("tracker.on_handshake: %s" % e) 132 | -------------------------------------------------------------------------------- /ups_lite_1_2.py: -------------------------------------------------------------------------------- 1 | # Based on UPS Lite v1.1 from https://github.com/xenDE 2 | # 3 | # functions for get UPS status - needs enable "i2c" in raspi-config 4 | # 5 | # https://github.com/linshuqin329/UPS-Lite 6 | # 7 | # For Raspberry Pi Zero Ups Power Expansion Board with Integrated Serial Port S3U4 8 | # https://www.ebay.de/itm/For-Raspberry-Pi-Zero-Ups-Power-Expansion-Board-with-Integrated-Serial-Port-S3U4/323873804310 9 | # https://www.aliexpress.com/item/32888533624.html 10 | # 11 | # To display external power supply status you need to bridge the necessary pins on the UPS-Lite board. See instructions in the UPS-Lite repo. 12 | import logging 13 | import struct 14 | 15 | import RPi.GPIO as GPIO 16 | 17 | import pwnagotchi 18 | import pwnagotchi.plugins as plugins 19 | import pwnagotchi.ui.fonts as fonts 20 | from pwnagotchi.ui.components import LabeledValue 21 | from pwnagotchi.ui.view import BLACK 22 | 23 | 24 | # TODO: add enable switch in config.yml an cleanup all to the best place 25 | class UPS: 26 | def __init__(self): 27 | # only import when the module is loaded and enabled 28 | import smbus 29 | # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1) 30 | self._bus = smbus.SMBus(1) 31 | 32 | def voltage(self): 33 | try: 34 | address = 0x36 35 | read = self._bus.read_word_data(address, 2) 36 | swapped = struct.unpack("H", read))[0] 37 | return swapped * 1.25 / 1000 / 16 38 | except: 39 | return 0.0 40 | 41 | def capacity(self): 42 | try: 43 | address = 0x36 44 | read = self._bus.read_word_data(address, 4) 45 | swapped = struct.unpack("H", read))[0] 46 | return swapped / 256 47 | except: 48 | return 0.0 49 | 50 | def charging(self): 51 | try: 52 | GPIO.setmode(GPIO.BCM) 53 | GPIO.setup(4, GPIO.IN) 54 | return '+' if GPIO.input(4) == GPIO.HIGH else '-' 55 | except: 56 | return '?' 57 | 58 | 59 | class UPSLite(plugins.Plugin): 60 | __author__ = 'evilsocket@gmail.com' 61 | __version__ = '1.0.0' 62 | __license__ = 'GPL3' 63 | __description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1' 64 | 65 | def __init__(self): 66 | self.ups = None 67 | 68 | def on_loaded(self): 69 | self.ups = UPS() 70 | 71 | def on_ui_setup(self, ui): 72 | ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0), 73 | label_font=fonts.Bold, text_font=fonts.Medium)) 74 | 75 | def on_unload(self, ui): 76 | with ui._lock: 77 | ui.remove_element('ups') 78 | 79 | def on_ui_update(self, ui): 80 | capacity = self.ups.capacity() 81 | charging = self.ups.charging() 82 | ui.set('ups', "%2i%s" % (capacity, charging)) 83 | if capacity <= self.options['shutdown'] and charging == '-': 84 | logging.info('[ups_lite] Empty battery (<= %s%%): shuting down' % self.options['shutdown']) 85 | ui.update(force=True, new_data={'status': 'Battery exhausted, bye ...'}) 86 | pwnagotchi.shutdown() 87 | -------------------------------------------------------------------------------- /ups_lite_1_2.yml: -------------------------------------------------------------------------------- 1 | ups_lite_1_2: 2 | enabled: false 3 | shutdown: 5 4 | -------------------------------------------------------------------------------- /ups_lite_1_3.py: -------------------------------------------------------------------------------- 1 | # Base on pwnagotchi ups_lite plugin, V1.3 implementation. 2 | # 3 | # Based on UPS Lite v1.3 from https://github.com/linshuqin329/UPS-Lite/tree/master/UPS-Lite_V1.3_CW2015 4 | # 5 | # functions to get UPS status - needs enable "i2c" in raspi-config 6 | # 7 | # https://github.com/linshuqin329/UPS-Lite 8 | # 9 | # To display external power supply status you need to bridge the necessary pins on the UPS-Lite board. See instructions in the UPS-Lite repo. 10 | import logging 11 | import struct 12 | 13 | import RPi.GPIO as GPIO 14 | 15 | import pwnagotchi 16 | import pwnagotchi.plugins as plugins 17 | import pwnagotchi.ui.fonts as fonts 18 | from pwnagotchi.ui.components import LabeledValue 19 | from pwnagotchi.ui.view import BLACK 20 | 21 | 22 | class UPS: 23 | def __init__(self): 24 | # only import when the module is loaded and enabled 25 | import smbus 26 | # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1) 27 | self._bus = smbus.SMBus(1) 28 | 29 | def quick_start(self): 30 | try: 31 | address = 0x62 32 | self._bus.write_word_data(address, 0x0A, 0x30) 33 | except Exception as e: 34 | logging.error("[UPS] Failed to wake up the CW2015: %s" % e) 35 | 36 | def voltage(self): 37 | try: 38 | address = 0x62 39 | read = self._bus.read_word_data(address, 0x02) 40 | swapped = struct.unpack("H", read))[0] 41 | return swapped * 0.305 / 1000 42 | except: 43 | return 0.0 44 | 45 | def capacity(self): 46 | try: 47 | address = 0x62 48 | read = self._bus.read_word_data(address, 0x04) 49 | swapped = struct.unpack("H", read))[0] 50 | return swapped / 256 51 | except: 52 | return 0.0 53 | 54 | def charging(self): 55 | try: 56 | GPIO.setmode(GPIO.BCM) 57 | GPIO.setup(4, GPIO.IN) 58 | return '+' if GPIO.input(4) == GPIO.HIGH else '-' 59 | except: 60 | return '?' 61 | 62 | 63 | class UPSLite(plugins.Plugin): 64 | __author__ = 'evilsocket@gmail.com' 65 | __version__ = '1.0.0' 66 | __license__ = 'GPL3' 67 | __description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1' 68 | 69 | def __init__(self): 70 | self.ups = None 71 | 72 | def on_loaded(self): 73 | self.ups = UPS() 74 | self.ups.quick_start() 75 | logging.info("[ups_lite] plugin loaded") 76 | 77 | def on_ui_setup(self, ui): 78 | ui.add_element('ups', LabeledValue(color=BLACK, label='UPS', value='0%/0V', position=(ui.width() / 2 + 15, 0), 79 | label_font=fonts.Bold, text_font=fonts.Medium)) 80 | 81 | def on_unload(self, ui): 82 | with ui._lock: 83 | ui.remove_element('ups') 84 | 85 | def on_ui_update(self, ui): 86 | capacity = self.ups.capacity() 87 | charging = self.ups.charging() 88 | ui.set('ups', "%2i%s" % (capacity, charging)) 89 | if capacity <= self.options['shutdown'] and charging == '-': 90 | logging.info('[ups_lite] Empty battery (<= %s%%): shuting down' % self.options['shutdown']) 91 | ui.update(force=True, new_data={'status': 'Battery exhausted, bye ...'}) 92 | pwnagotchi.shutdown() 93 | -------------------------------------------------------------------------------- /ups_lite_1_3.yml: -------------------------------------------------------------------------------- 1 | ups_lite_1_3: 2 | enabled: false 3 | shutdown: 5 4 | -------------------------------------------------------------------------------- /waveshare_v3_touch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import RPi.GPIO as GPIO 3 | import subprocess 4 | import time 5 | import pwnagotchi.ui.faces as faces 6 | import pwnagotchi.plugins as plugins 7 | 8 | 9 | class TouchCounter(): 10 | 11 | def __init__(self): 12 | self.count = 0 13 | self.last_timestamp = time.time() 14 | self.wait_timestamp = False 15 | 16 | def reset(self): 17 | self.count = 0 18 | self.last_timestamp = time.time() 19 | 20 | def wait(self, seconds): 21 | self.wait_timestamp = time.time() + seconds 22 | 23 | def press(self): 24 | t = time.time() 25 | 26 | if self.wait_timestamp: 27 | if self.wait_timestamp > t: 28 | return 0 29 | else: 30 | self.wait_timestamp = False 31 | 32 | if t - self.last_timestamp < 1: 33 | self.count += 1 34 | self.last_timestamp = t 35 | else: 36 | self.reset() 37 | 38 | return self.count 39 | 40 | 41 | class WaveshareV3Touch(plugins.Plugin): 42 | __author__ = "sliim@mailoo.org" 43 | __version__ = "1.0.0" 44 | __license__ = "GPL3" 45 | __description__ = """ 46 | Experimental plugin for Waveshare V3 2.13inch Touch e-Paper HAT. 47 | Product: https://www.amazon.fr/gp/product/B09H4HZHXF 48 | Resources: https://www.waveshare.com/wiki/2.13inch_Touch_e-Paper_HAT 49 | Examples: https://github.com/waveshareteam/Touch_e-Paper_HAT/ 50 | """ 51 | 52 | def __init__(self): 53 | self.touch = TouchCounter() 54 | self.agent = None 55 | 56 | def callback(self, gpio): 57 | logging.info("CALLBACK %d" % gpio) 58 | if self.touch.press() >= 5: 59 | if self.agent: 60 | self.agent.view().set("status", "HiHiHiii") 61 | self.agent.view().set("face", faces.EXCITED) 62 | self.agent.view().update(force=True) 63 | self.touch.reset() 64 | self.touch.wait(5) 65 | 66 | def on_ready(self, agent): 67 | self.agent = agent 68 | 69 | def on_loaded(self): 70 | GPIO.setmode(GPIO.BCM) 71 | GPIO.setup(17, GPIO.OUT) 72 | GPIO.setup(25, GPIO.OUT) 73 | GPIO.setup(8, GPIO.OUT) 74 | GPIO.setup(24, GPIO.IN) 75 | GPIO.setup(22, GPIO.OUT) 76 | GPIO.setup(27, GPIO.IN) 77 | GPIO.add_event_detect(27, 78 | GPIO.RISING, 79 | callback=self.callback, 80 | bouncetime=200) 81 | GPIO.add_event_detect(24, 82 | GPIO.RISING, 83 | callback=self.callback, 84 | bouncetime=200) 85 | logging.info("Waveshare V3 Touch plugin loaded.") 86 | 87 | def on_unload(self, ui): 88 | GPIO.output(17, 0) 89 | GPIO.output(25, 0) 90 | GPIO.output(8, 0) 91 | GPIO.output(22, 0) 92 | GPIO.cleanup() 93 | -------------------------------------------------------------------------------- /xp.py: -------------------------------------------------------------------------------- 1 | 2 | import copy 3 | import glob 4 | import json 5 | import logging 6 | import os 7 | import time 8 | 9 | import pwnagotchi.plugins as plugins 10 | import pwnagotchi.ui.fonts as fonts 11 | import pwnagotchi.ui.faces as faces 12 | import pwnagotchi.utils as utils 13 | from pwnagotchi.ui.components import LabeledValue, Text, Rect, FilledRect 14 | from pwnagotchi.ui.view import BLACK 15 | 16 | from flask import render_template_string 17 | from flask import abort 18 | from flask import Response 19 | 20 | # TODO: jquery mobile notifications ? 21 | 22 | TEMPLATE = """ 23 | {% extends "base.html" %} 24 | {% set active_page = "plugins" %} 25 | {% block title %} 26 | {{ name }} XP 27 | {% endblock %} 28 | 29 | {% block styles %} 30 | {{ super() }} 31 | 82 | {% endblock %} 83 | 84 | {% block script %} 85 | var level = document.getElementById('level'); 86 | var percents = document.getElementById('percents'); 87 | var xp = document.getElementById('xp'); 88 | var next_level = document.getElementById('next_level'); 89 | var progress = document.getElementById('progress'); 90 | var rank = document.getElementById('rank'); 91 | var face = document.getElementById('face'); 92 | var messages = document.getElementById('messages'); 93 | var xhr = new XMLHttpRequest(); 94 | 95 | function toggledebug() { 96 | var toggle = new XMLHttpRequest(); 97 | toggle.open('GET', '{{ url_for('plugins') }}/xp/toggledebug'); 98 | toggle.send(); 99 | } 100 | 101 | function clearMessages() { 102 | messages.innerHTML = ""; 103 | } 104 | 105 | function error(msg) { 106 | var tr = document.createElement("tr"); 107 | var td = document.createElement("td"); 108 | td.textContent = msg; 109 | tr.appendChild(td); 110 | tr.className = "error"; 111 | messages.prepend(tr); 112 | } 113 | 114 | xhr.onload = function() { 115 | if (xhr.status != 200) { 116 | error("> " + xhr.status + ": " + xhr.response); 117 | return false; 118 | } 119 | 120 | var data = JSON.parse(xhr.response); 121 | level.textContent = "LVL " + data.level; 122 | xp.textContent = data.xp; 123 | next_level.textContent = data.next_level; 124 | rank.textContent = data.rank; 125 | face.textContent = data.face; 126 | progress.setAttribute("style", "width:" + data.percents + "%;"); 127 | percents.textContent = data.percents.toFixed(2) + "%"; 128 | 129 | data.messages.forEach((msg) => { 130 | var tr = document.createElement("tr"); 131 | var td = document.createElement("td"); 132 | td.textContent = "> " + msg; 133 | tr.appendChild(td); 134 | tr.className = "message"; 135 | messages.prepend(tr); 136 | }); 137 | 138 | data.errors.forEach((err) => { 139 | error("> " + err); 140 | }); 141 | } 142 | 143 | xhr.onerror = function() { 144 | error("> Cannot fetch data. Request failed.."); 145 | } 146 | 147 | setInterval(function() { 148 | xhr.open('GET', '{{ url_for('plugins') }}/xp/data'); 149 | xhr.send(); 150 | }, 3000); 151 | {% endblock %} 152 | 153 | {% block content %} 154 |

{{ name }} XP

155 |
156 |
-
157 |
-
158 |
159 | 160 |
161 |
162 |
XP: -/- 163 | 164 | 165 | 166 | 167 |
168 |
Rank: - 
169 |
170 |
171 | {% endblock %} 172 | """ 173 | 174 | 175 | EVENTS_XP = { 176 | "ai_ready": 5, 177 | "ai_best_reward": 20, 178 | "ai_training_step": 5, 179 | "ai_training_end": 15, 180 | "ai_worst_reward": -20, 181 | "association": 5, 182 | "bored": -2, 183 | "deauthentication": 10, 184 | "epoch_rewarded": 1, 185 | "epoch": 0.1, 186 | "excited": 5, 187 | "handshake": 20, 188 | "handshake_new": 100, 189 | "handshakes_100": 500, 190 | "peer_detected": 5, 191 | "good_friend_detected": 10, 192 | "sad": -2 193 | } 194 | 195 | RANKS = { 196 | "newbie": { 197 | "level": 1, 198 | "head": "()", 199 | "face": "look_l" 200 | }, 201 | "rookie": { 202 | "level": 5, 203 | "head": "❪❫", 204 | "face": "look_r" 205 | }, 206 | "kiddie": { 207 | "level": 10, 208 | "head": "❨❩", 209 | "face": "look_l_happy" 210 | }, 211 | "student": { 212 | "level": 15, 213 | "head": "◖◗", 214 | "face": "look_r_happy" 215 | }, 216 | "insider": { 217 | "level": 20, 218 | "head": "⎛⎞", 219 | "face": "motivated" 220 | }, 221 | "infiltrator": { 222 | "level": 25, 223 | "head": "⎝⎠", 224 | "face": "sleep2" 225 | }, 226 | "pineap-fan": { 227 | "level": 30, 228 | "head": "⟪⟫", 229 | "face": "friend" 230 | }, 231 | "parasit": { 232 | "level": 35, 233 | "head": "❰❱", 234 | "face": "intense" 235 | }, 236 | "wardriver": { 237 | "level": 40, 238 | "head": "⎞⎞", 239 | "face": "cool" 240 | }, 241 | "profiler": { 242 | "level": 45, 243 | "head": "⎠⎝", 244 | "face": "happy" 245 | }, 246 | "hunter": { 247 | "level": 50, 248 | "head": "[]", 249 | "face": "grateful" 250 | }, 251 | "tracker": { 252 | "level": 55, 253 | "head": "⁅⁆", 254 | "face": "excited" 255 | }, 256 | "pwner": { 257 | "level": 60, 258 | "head": "⟦⟧", 259 | "face": "smart" 260 | }, 261 | "hacker": { 262 | "level": 65, 263 | "head": "{}", 264 | "face": "awake" 265 | }, 266 | "hAIx0r": { 267 | "level": 70, 268 | "head": "❴❵", 269 | "face": "motivated" 270 | }, 271 | "dumper": { 272 | "level": 75, 273 | "head": "/\\", 274 | "face": "grateful" 275 | }, 276 | "elite": { 277 | "level": 80, 278 | "head": "⎠⎞", 279 | "face": "debug" 280 | }, 281 | "pwnbot": { 282 | "level": 85, 283 | "head": "⎛⎛", 284 | "face": "broken" 285 | }, 286 | "sentinel": { 287 | "level": 90, 288 | "head": "╔╗", 289 | "face": "sleep" 290 | }, 291 | "terminator": { 292 | "level": 95, 293 | "head": "❲❳", 294 | "face": "cool" 295 | }, 296 | "legend": { 297 | "level": 100, 298 | "head": "⦗⦘", 299 | "face": "cool" 300 | } 301 | } 302 | 303 | 304 | MULTIPLIER = 1.05 305 | BASE = 750 306 | 307 | 308 | class XP(plugins.Plugin): 309 | __author__ = "sliim@mailoo.org" 310 | __version__ = "1.0.0" 311 | __license__ = "GPL3" 312 | __description__ = "XP / Leveling implementation. Just for fun!" 313 | 314 | def __init__(self): 315 | self.ready = False 316 | self.xp_file = "/var/local/pwnagotchi/xp.txt" 317 | self.xp = 0 318 | self.level = 0 319 | self.next_level = 0 320 | self.percents = 0 321 | self.rank = list(RANKS.keys())[0] 322 | self.agent = None 323 | self.handshakes = 0 324 | self.ai_ready = False 325 | 326 | # WEBUI 327 | self.messages = [] 328 | self.errors = [] 329 | self.debug = False 330 | 331 | # UI positions 332 | self.level_position = False 333 | self.rank_position = False 334 | self.progressbar_position = False 335 | 336 | def _error(self, msg): 337 | logging.error("[XP] %s" % msg) 338 | self.errors.append(msg) 339 | 340 | def _save_xp(self): 341 | try: 342 | with open(self.xp_file, "w") as f: 343 | f.write(str(self._get_xp())) 344 | except Exception as e: 345 | self._error("Cannot write to %s: %s" % (self.xp_file, str(e))) 346 | 347 | def _update_xp(self, event, multiplier=1): 348 | if event in EVENTS_XP: 349 | if not self.ready: 350 | logging.warning("[XP] Plugin not ready yet!") 351 | return False 352 | 353 | xp = EVENTS_XP[event] * multiplier 354 | if not self.ai_ready: 355 | xp /= 4 356 | logging.info("[XP] Update %.3f xp from event %s" % (xp, event)) 357 | self.xp += float("%.3f" % xp) 358 | self._save_xp() 359 | self._update_level() 360 | 361 | def _get_xp(self): 362 | return float("%.3f" % self.xp) 363 | 364 | def _update_level(self): 365 | level = 1 366 | next_level = BASE 367 | old = 0 368 | 369 | while self.xp >= next_level: 370 | old = next_level 371 | next_level = int(next_level * MULTIPLIER + (BASE * 3)) 372 | level += 1 373 | 374 | self.percents = (100 * (self.xp - old)) / (next_level - old) 375 | 376 | if level > self.level: 377 | if self.level != 0: 378 | plugins.on("level_up", level) 379 | self._update_agent(level) 380 | 381 | self.level = level 382 | self.next_level = next_level 383 | plugins.on("level_update", level) 384 | 385 | def _update_agent(self, level): 386 | current_rank = self.rank 387 | update = None 388 | 389 | for name, rank in RANKS.items(): 390 | if level >= rank["level"]: 391 | self.rank = name 392 | 393 | if self.rank != current_rank: 394 | if self.level != 0: 395 | self.messages.append("New rank: %s :]" % self.rank) 396 | 397 | head = RANKS[self.rank]["head"] 398 | update = copy.copy(self.agent.config()["ui"]["faces"]) 399 | for key, value in update.items(): 400 | update[key] = value.replace("(", head[0]).replace(")", head[1]) 401 | faces.load_from_config(update) 402 | plugins.on("rank_update", self.rank) 403 | 404 | def _progress(self, p): 405 | return [(p[0] + 1, p[1]), 406 | ((self.percents * (p[2] - p[0]) / 100) + p[0] - 2, p[3])] 407 | 408 | def on_loaded(self): 409 | try: 410 | if not os.path.exists(os.path.dirname(self.xp_file)): 411 | os.makedirs(os.path.dirname(self.xp_file), exist_ok=True) 412 | 413 | opts = self.options 414 | 415 | if opts["level_position"] and \ 416 | len(opts["level_position"].split(",")) == 2: 417 | self.level_position = [ 418 | int(i) for i in opts["level_position"].split(",")] 419 | if opts["rank_position"] and \ 420 | len(opts["rank_position"].split(",")) == 2: 421 | self.rank_position = [ 422 | int(i) for i in opts["rank_position"].split(",")] 423 | if opts["progressbar_position"] and \ 424 | len(opts["progressbar_position"].split(",")) == 4: 425 | self.progressbar_position = [ 426 | int(i) for i in opts["progressbar_position"].split(",")] 427 | 428 | logging.info("[XP] plugin loaded.") 429 | except Exception as e: 430 | self._error("on_loaded error: %s" % str(e)) 431 | return False 432 | 433 | def on_ready(self, agent): 434 | try: 435 | self.agent = agent 436 | self.handshakes = utils.total_unique_handshakes( 437 | agent.config()['bettercap']['handshakes']) 438 | 439 | if os.path.exists(self.xp_file): 440 | with open(self.xp_file, "r") as f: 441 | self.xp = float(f.read()) 442 | elif self.options["load_initial_xp"]: 443 | logging.info("[XP] Loading initial XP from last activities..") 444 | xp = 0 445 | 446 | if os.path.exists("/root/brain.json"): 447 | with open("/root/brain.json", "r") as f: 448 | xp = json.load(f)["epochs_lived"] * EVENTS_XP["epoch"] 449 | logging.info("[XP] Loaded %d XP from lived epoch" % xp) 450 | self.xp += xp 451 | 452 | xp = self.handshakes * EVENTS_XP["handshake"] 453 | logging.info("[XP] Loaded %d XP from pwned hanshakes" % xp) 454 | self.xp += xp 455 | 456 | if os.path.exists("/root/peers"): 457 | xp = len(glob.glob( 458 | "/root/peers/*.json")) * EVENTS_XP["peer_detected"] 459 | logging.info("[XP] Loaded %d XP from detected peers" % xp) 460 | self.xp += xp 461 | 462 | self._save_xp() 463 | 464 | logging.info("[XP] plugin initialized: current xp: %f" 465 | % self._get_xp()) 466 | self.ready = True 467 | self._update_level() 468 | except Exception as e: 469 | self._error("on_ready error: %s" % str(e)) 470 | 471 | def on_level_up(self, level): 472 | logging.info("[XP] LEVEL UP! (%d)" % level) 473 | self.messages.append("LEVEL UP! %s" % faces.COOL) 474 | 475 | try: 476 | self.agent.view().set("status", "LEVEL UP!") 477 | self.agent.view().set("face", faces.COOL) 478 | self.agent.view().update(force=True) 479 | except Exception as e: 480 | self._error("Error LVL UP status: %s" % str(e)) 481 | 482 | def on_ui_setup(self, ui): 483 | try: 484 | if self.level_position: 485 | ui.add_element('level', LabeledValue( 486 | color=BLACK, 487 | label='lvl', 488 | value=str(self.level), 489 | position=(self.level_position[0], self.level_position[1]), 490 | label_font=fonts.BoldSmall, 491 | text_font=fonts.Small)) 492 | if self.rank_position: 493 | ui.add_element('rank', Text( 494 | color=BLACK, 495 | value=str(self.rank), 496 | position=(self.rank_position[0], self.rank_position[1]), 497 | font=fonts.Small)) 498 | if self.progressbar_position: 499 | ui.add_element('progressbar', Rect( 500 | color=BLACK, 501 | xy=[(self.progressbar_position[0], 502 | self.progressbar_position[1]), 503 | (self.progressbar_position[2], 504 | self.progressbar_position[3])])) 505 | ui.add_element('progress', FilledRect( 506 | color=BLACK, 507 | xy=self._progress(self.progressbar_position))) 508 | except Exception as e: 509 | self._error("on_ui_setup error: %s" % str(e)) 510 | 511 | def on_ui_update(self, ui): 512 | try: 513 | if self.level_position: 514 | ui.set("level", str(self.level)) 515 | if self.rank_position: 516 | ui.set("rank", str(self.rank)) 517 | if self.progressbar_position: 518 | with ui._lock: 519 | old = ui._state._state["progress"].xy 520 | new = self._progress(self.progressbar_position) 521 | if old != new: 522 | ui._state._state["progress"].xy = new 523 | ui._state._changes["progress"] = True 524 | except Exception as e: 525 | self._error("on_ui_update error: %s" % str(e)) 526 | 527 | def on_unload(self, ui): 528 | try: 529 | self._save_xp() 530 | with ui._lock: 531 | if self.level_position: 532 | ui.remove_element('level') 533 | if self.rank_position: 534 | ui.remove_element('rank') 535 | if self.progressbar_position: 536 | ui.remove_element('progressbar') 537 | ui.remove_element('progress') 538 | except Exception as e: 539 | self._error("on_unload error: %s" % str(e)) 540 | 541 | def on_webhook(self, path, request): 542 | try: 543 | if not path or path == "/": 544 | return render_template_string( 545 | TEMPLATE, 546 | name=self.agent.config()["main"]["name"]) 547 | 548 | if path == "data": 549 | jsonResponse = json.dumps({ 550 | "level": self.level, 551 | "xp": self._get_xp(), 552 | "next_level": self.next_level, 553 | "percents": self.percents, 554 | "rank": self.rank, 555 | "face": getattr(faces, RANKS[self.rank]["face"].upper()), 556 | "messages": self.messages, 557 | "errors": self.errors 558 | }) 559 | 560 | self.messages = [] 561 | self.errors = [] 562 | return Response(jsonResponse, mimetype="application/json") 563 | 564 | if path == "toggledebug": 565 | if self.debug: 566 | self.debug = False 567 | self.messages.append("Debug mode disabled.") 568 | else: 569 | self.debug = True 570 | self.messages.append("Debug mode enabled.") 571 | 572 | return Response(json.dumps({"debug": self.debug}), 573 | mimetype="application/json") 574 | 575 | abort(404) 576 | except Exception as e: 577 | self._error("on_webhook error: %s" % str(e)) 578 | 579 | def on_ai_ready(self, agent): 580 | try: 581 | self._update_xp("ai_ready") 582 | self.ai_ready = True 583 | 584 | if self.debug: 585 | self.messages.append("AGENT: %s" % agent) 586 | self.messages.append("AI ready!") 587 | except Exception as e: 588 | self._error("on_ai_ready error: %s" % str(e)) 589 | 590 | def on_ai_best_reward(self, agent, reward): 591 | try: 592 | self._update_xp("ai_best_reward") 593 | 594 | if self.debug: 595 | self.messages.append("REWARD: %s" % reward) 596 | self.messages.append("Best reward %s" % faces.MOTIVATED) 597 | except Exception as e: 598 | self._error("on_ai_best_reward error: %s" % str(e)) 599 | 600 | def on_ai_worst_reward(self, agent, reward): 601 | try: 602 | self._update_xp("ai_worst_reward") 603 | 604 | if self.debug: 605 | self.messages.append("REWARD: %s" % reward) 606 | self.messages.append("Worst reward %s" % faces.DEMOTIVATED) 607 | except Exception as e: 608 | self._error("on_ai_worst_reward error: %s" % str(e)) 609 | 610 | def on_ai_training_step(self, agent): 611 | try: 612 | self._update_xp("ai_training_step") 613 | 614 | if self.debug: 615 | self.messages.append("AGENT: %s" % agent) 616 | self.messages.append("End of training step %s" % faces.GRATEFUL) 617 | except Exception as e: 618 | self._error("on_ai_training_step error: %s" % str(e)) 619 | 620 | def on_ai_training_end(self, agent): 621 | try: 622 | self._update_xp("ai_training_end") 623 | 624 | if self.debug: 625 | self.messages.append("AGENT: %s" % agent) 626 | self.messages.append("End of training %s" % faces.EXCITED) 627 | except Exception as e: 628 | self._error("on_ai_training_end error: %s" % str(e)) 629 | 630 | def on_association(self, agent, ap): 631 | try: 632 | self._update_xp("association") 633 | 634 | if self.debug: 635 | self.messages.append("AP: %s" % ap) 636 | self.messages.append("Associated with %s (%s) %s" % ( 637 | ap["hostname"], ap["mac"], faces.LOOK_L_HAPPY)) 638 | except Exception as e: 639 | self._error("on_association error: %s" % str(e)) 640 | 641 | def on_bored(self, agent): 642 | try: 643 | self._update_xp("bored") 644 | 645 | if self.debug: 646 | self.messages.append("AGENT: %s" % agent) 647 | self.messages.append("I'm bored %s" % faces.BORED) 648 | except Exception as e: 649 | self._error("on_bored error: %s" % str(e)) 650 | 651 | def on_deauthentication(self, agent, ap, client): 652 | try: 653 | self._update_xp("deauthentication") 654 | 655 | if self.debug: 656 | self.messages.append("AP: %s" % ap) 657 | self.messages.append("CLIENT: %s" % client) 658 | self.messages.append("Deauthenticated %s from %s (%s) %s" % ( 659 | client["mac"], ap["hostname"], ap["mac"], faces.COOL)) 660 | except Exception as e: 661 | self._error("on_deauthentication error: %s" % str(e)) 662 | 663 | def on_epoch(self, agent, epoch, data): 664 | try: 665 | if data["reward"] > 0: 666 | self._update_xp("epoch_rewarded") 667 | else: 668 | self._update_xp("epoch") 669 | 670 | if self.debug: 671 | self.messages.append("DATA: %s" % data) 672 | self.messages.append("Epoch #%s finished!" % epoch) 673 | except Exception as e: 674 | self._error("on_epoch error: %s" % str(e)) 675 | 676 | def on_excited(self, agent): 677 | try: 678 | self._update_xp("excited") 679 | 680 | if self.debug: 681 | self.messages.append("AGENT: %s" % agent) 682 | self.messages.append("I'm so excited %s" % faces.EXCITED) 683 | except Exception as e: 684 | self._error("on_excited error: %s" % str(e)) 685 | 686 | def on_handshake(self, agent, filename, ap, client): 687 | try: 688 | while not os.path.exists(filename): 689 | logging.warning("[XP] on_handshake: Waiting for %s to be created." 690 | % filename) 691 | time.sleep(1) 692 | 693 | handshakes = utils.total_unique_handshakes( 694 | agent.config()['bettercap']['handshakes']) 695 | 696 | if handshakes > self.handshakes: 697 | if handshakes % 100 == 0: 698 | self._update_xp("handshakes_100", handshakes / 100) 699 | else: 700 | self._update_xp("handshake_new") 701 | else: 702 | self._update_xp("handshake") 703 | 704 | self.handshakes = handshakes 705 | 706 | if self.debug: 707 | self.messages.append("AP: %s" % ap) 708 | self.messages.append("CLIENT: %s" % client) 709 | self.messages.append("New handshake by %s for %s (%s) %s" % ( 710 | client["mac"], ap["hostname"], ap["mac"], faces.COOL)) 711 | except Exception as e: 712 | self._error("on_handshake error: %s" % str(e)) 713 | 714 | def on_peer_detected(self, agent, peer): 715 | try: 716 | self._update_xp("peer_detected") 717 | 718 | if peer.is_good_friend(agent.config()): 719 | self._update_xp("good_friend_detected") 720 | 721 | if self.debug: 722 | self.messages.append("PEER: %s" % peer) 723 | self.messages.append("Hey AIx0r %s is in the place %s" % ( 724 | peer.name(), faces.FRIEND)) 725 | except Exception as e: 726 | self._error("on_peer_detected error: %s" % str(e)) 727 | 728 | def on_sad(self, agent): 729 | try: 730 | self._update_xp("sad") 731 | 732 | if self.debug: 733 | self.messages.append("AGENT: %s" % agent) 734 | self.messages.append("I'm sad %s" % faces.SAD) 735 | except Exception as e: 736 | self._error("on_sad error: %s" % str(e)) 737 | -------------------------------------------------------------------------------- /xp.yml: -------------------------------------------------------------------------------- 1 | xp: 2 | enabled: false 3 | load_initial_xp: false 4 | level_position: "10,30" 5 | rank_position: "50,30" 6 | progressbar_position: "20,40,103,44" 7 | -------------------------------------------------------------------------------- /xp_grid.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pwnagotchi.grid as grid 3 | import pwnagotchi.plugins as plugins 4 | import pwnagotchi.ui.fonts as fonts 5 | from pwnagotchi.ui.components import Text 6 | from pwnagotchi.ui.view import BLACK 7 | 8 | 9 | class XPGrid(plugins.Plugin): 10 | __author__ = "sliim@mailoo.org" 11 | __version__ = "1.0.0" 12 | __license__ = "GPL3" 13 | __description__ = """ 14 | Level sharing between peers for xp plugin. 15 | xp plugin must be enabled. 16 | """ 17 | 18 | def __init__(self): 19 | self.known_peers = {} 20 | 21 | def on_loaded(self): 22 | try: 23 | if "name_position" not in self.options or \ 24 | not self.options["name_position"] or \ 25 | len(self.options["name_position"].split(",")) != 2: 26 | self.options["name_position"] = "63,95" 27 | if "position" not in self.options or \ 28 | not self.options["position"] or \ 29 | len(self.options["position"].split(",")) != 2: 30 | self.options["position"] = "36,95" 31 | logging.info("[xp_grid] plugin loaded") 32 | except Exception as e: 33 | logging.error("xp_grid.on_loaded: %s" % e) 34 | 35 | def on_level_update(self, level): 36 | try: 37 | grid.call("/mesh/data", {"level": level}) 38 | except Exception as e: 39 | logging.error("xp_grid.on_level_update: %s" % e) 40 | 41 | def on_rank_update(self, rank): 42 | try: 43 | grid.call("/mesh/data", {"rank": rank}) 44 | except Exception as e: 45 | logging.error("xp_grid.on_rank_update: %s" % e) 46 | 47 | def on_peer_detected(self, agent, peer): 48 | try: 49 | self.known_peers[peer.identity()] = peer 50 | except Exception as e: 51 | logging.error("xp_grid.on_peer_detected: %s" % e) 52 | 53 | def on_ui_setup(self, ui): 54 | try: 55 | pos = self.options["position"].split(",") 56 | name_pos = self.options["name_position"].split(",") 57 | ui.remove_element('friend_name') 58 | ui.add_element('friend_name', Text( 59 | color=BLACK, 60 | value=None, 61 | position=(int(name_pos[0]), int(name_pos[1])), 62 | font=fonts.BoldSmall)) 63 | ui.add_element('friend_level', Text( 64 | color=BLACK, 65 | value=None, 66 | position=(int(pos[0]), int(pos[1])), 67 | font=fonts.BoldSmall)) 68 | except Exception as e: 69 | logging.error("xp_grid.on_ui_setup: %s" % e) 70 | 71 | def on_ui_update(self, ui): 72 | try: 73 | friend = ui.get("friend_name") 74 | if not friend: 75 | ui.set("friend_level", None) 76 | return False 77 | 78 | peer = friend.split(" ") 79 | for i, p in self.known_peers.items(): 80 | if p.name() == peer[1] \ 81 | and str(p.pwnd_run()) == peer[2] \ 82 | and "(%d)" % p.pwnd_total() == peer[3]: 83 | level = "" 84 | rank = "" 85 | 86 | if p.adv.get("level"): 87 | level = p.adv.get("level") 88 | if p.adv.get("rank"): 89 | rank = p.adv.get("rank") 90 | 91 | ui.set("friend_level", "[%d]" % level) 92 | return True 93 | except Exception as e: 94 | logging.error("xp_grid.on_ui_update: %s" % e) 95 | 96 | def on_unload(self, ui): 97 | try: 98 | with ui._lock: 99 | ui.remove_element('friend_level') 100 | except Exception as e: 101 | logging.error("xp_grid.on_unload: %s" % e) 102 | -------------------------------------------------------------------------------- /xp_grid.yml: -------------------------------------------------------------------------------- 1 | xp_grid: 2 | enabled: false 3 | position: "36,95" 4 | name_position: "63,95" 5 | --------------------------------------------------------------------------------