├── .gitignore ├── README.md ├── palnagotchi.jpg ├── palnagotchi ├── mood.cpp ├── mood.h ├── palnagotchi.ino ├── pwnagotchi.h ├── pwngrid.cpp ├── pwngrid.h ├── ui.cpp └── ui.h └── pwngrid_beacon.hexdump /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.bin 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Palnagotchi for M5 Cardputer 2 | 3 | ![Palnagotchi](https://github.com/viniciusbo/m5-palnagotchi/blob/master/palnagotchi.jpg?raw=true) 4 | 5 | A friendly unit for those lonely Pwnagotchis out there. It's written to run on the M5 Cardputer, but I'll try to add support to other M5 devices in the future. 6 | 7 | I reverse engineered the Pwngrid advertisement protocol and made it possible for the Cardputer to advertise to the Pwngrid as a Pwnagotchi. All brain policy parameters that could negatively impact AI learning were removed from the advertisemenet data. 8 | 9 | The Pwngrid works by sending Wifi beacon frames with a JSON serialized payload in Wifi AC headers, containing the Pwnagotchi's data (name, face, pwns, brain policy between others). That's how nearby Pwnagotchis can detect and also learn each other. By crafting a custom beacon frame, this app can appear as a Pwnagotchi to other Pwnagotchis. 10 | 11 | ## Usage 12 | 13 | - Run the app to start advertisement. 14 | - ESC or m toggles the menu. Use arrow keys or tab to navigate and OK to select option. Esc or m to go back to main menu. 15 | - Top bar shows UPS level and uptime. 16 | - Bottom bar shows total friends made in this run, all time total friends between parenthesis (needs EEPROM) and signal strengh indicator of closest friend. 17 | - Nearby pwnagotchis show all nearby units and its signal strength. 18 | - Palnagotchi gets a random mood every minute or so. 19 | 20 | ## Why? 21 | 22 | I don't like to see a sad Pwnagotchi. 23 | 24 | ## Planned features 25 | 26 | - Friend spam? 27 | -------------------------------------------------------------------------------- /palnagotchi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusbo/m5-palnagotchi/b994bca904fff6d614e5eb481afd2cb266f239b9/palnagotchi.jpg -------------------------------------------------------------------------------- /palnagotchi/mood.cpp: -------------------------------------------------------------------------------- 1 | #include "mood.h" 2 | 3 | // ASCII equivalent 4 | const String palnagotchi_moods[] = { 5 | "(v__v)", // 0 - sleeping 6 | "(=__=)", // 1 - awakening 7 | "(O__O)", // 2 - awake 8 | "( O_O)", // 3 - observing (neutral) right 9 | "(O_O )", // 4 - observig (neutral) left 10 | "( 0_0)", // 5 - observing (happy) right 11 | "(0_0 )", // 6 - observing (happy) left 12 | "(+__+)", // 7 - intense 13 | "(-@_@)", // 8 - cool 14 | "(0__0)", // 9 - happy 15 | "(^__^)", // 10 - grateful 16 | "(a__a)", // 11 - excited 17 | "(+__+)", // 12 - smart 18 | "(*__*)", // 13 - friendly 19 | "(@__@)", // 14 - motivated 20 | "(>__<)", // 15 - demotivated 21 | "(-__-)", // 16 - bored 22 | "(T_T )", // 17 - sad 23 | "(;__;)", // 18 - lonely 24 | "(X__X)", // 19 - broken 25 | "(#__#)", // 20 - debugging, 26 | "8====D", // 21 - ultra random easter egg 27 | }; 28 | 29 | const String palnagotchi_moods_desc[] = { 30 | "Zzzz...", // 0 - sleeping 31 | "...", // 1 - awakening 32 | "Let's MAKE FRIENDS!", // 2 - awake 33 | "WANTED: FRIENDS", // 3 - observing (neutral) right 34 | "WANTED: FRIENDS", // 4 - observig (neutral) left 35 | "Can we have even more friends?", // 5 - observing (happy) right 36 | "Can we have even more friends?", // 6 - observing (happy) left 37 | "YEAH! So many pwnagotchis!", // 7 - intense 38 | "The coolest pal in the neighbourhood", // 8 - cool 39 | "Can we have even more friends?", // 9 - happy 40 | "I LOVE PWNAGOTCHIS!", // 10 - grateful 41 | "That's how I like it.", // 11 - excited 42 | "3.1415926535897932384626433832795", // 12 - smart 43 | "HEY YOU! LETS BE FRIENDS!", // 13 - friendly 44 | "IT RUNS! SUCK MA BALLZ!", // 14 - motivated 45 | "Why my life sucks? WHY", // 15 - demotivated 46 | "Seriously, let's go for a walk...", // 16 - bored 47 | "Get your hands off me...", // 17 - sad 48 | "Where are all the Pwnagotchis?", // 18 - lonely 49 | "It works on my end.", // 19 - broken 50 | "Wtf? I didn't even touch it...", // 20 - debugging, 51 | "What?", // 21 - ultra random easter egg 52 | }; 53 | 54 | uint8_t current_mood = 0; 55 | String current_phrase = ""; 56 | String current_face = ""; 57 | bool current_broken = false; 58 | 59 | uint8_t getCurrentMoodId() { return current_mood; } 60 | String getCurrentMoodFace() { return current_face; } 61 | String getCurrentMoodPhrase() { return current_phrase; } 62 | bool isCurrentMoodBroken() { return current_broken; } 63 | 64 | void setMood(uint8_t mood, String face, String phrase, bool broken) { 65 | current_mood = mood; 66 | current_broken = broken; 67 | 68 | if (face != "") { 69 | current_face = face; 70 | } else { 71 | current_face = palnagotchi_moods[current_mood]; 72 | } 73 | 74 | if (phrase != "") { 75 | current_phrase = phrase; 76 | } else { 77 | current_phrase = palnagotchi_moods_desc[current_mood]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /palnagotchi/mood.h: -------------------------------------------------------------------------------- 1 | #include "M5Cardputer.h" 2 | 3 | #define MOOD_BROKEN 19 4 | 5 | void setMood(uint8_t mood, String face = "", String phrase = "", 6 | bool broken = false); 7 | uint8_t getCurrentMoodId(); 8 | String getCurrentMoodFace(); 9 | String getCurrentMoodPhrase(); 10 | bool isCurrentMoodBroken(); 11 | -------------------------------------------------------------------------------- /palnagotchi/palnagotchi.ino: -------------------------------------------------------------------------------- 1 | #include "M5Cardputer.h" 2 | #include "M5Unified.h" 3 | #include "ui.h" 4 | 5 | #define STATE_INIT 0 6 | #define STATE_WAKE 1 7 | #define STATE_HALT 255 8 | 9 | uint8_t state; 10 | 11 | void initM5() { 12 | auto cfg = M5.config(); 13 | M5.begin(); 14 | M5.Display.begin(); 15 | M5Cardputer.begin(cfg); 16 | M5Cardputer.Keyboard.begin(); 17 | } 18 | 19 | void setup() { 20 | initM5(); 21 | initPwngrid(); 22 | initUi(); 23 | state = STATE_INIT; 24 | } 25 | 26 | uint8_t current_channel = 1; 27 | uint32_t last_mood_switch = 10001; 28 | 29 | void wakeUp() { 30 | for (uint8_t i = 0; i < 3; i++) { 31 | setMood(i); 32 | updateUi(); 33 | delay(1250); 34 | } 35 | } 36 | 37 | void advertise(uint8_t channel) { 38 | uint32_t elapsed = millis() - last_mood_switch; 39 | if (elapsed > 50000) { 40 | setMood(random(2, 21)); 41 | last_mood_switch = millis(); 42 | } 43 | 44 | esp_err_t result = pwngridAdvertise(channel, getCurrentMoodFace()); 45 | 46 | if (result == ESP_ERR_WIFI_IF) { 47 | setMood(MOOD_BROKEN, "", "Error: invalid interface", true); 48 | state = STATE_HALT; 49 | } else if (result == ESP_ERR_INVALID_ARG) { 50 | setMood(MOOD_BROKEN, "", "Error: invalid argument", true); 51 | state = STATE_HALT; 52 | } else if (result != ESP_OK) { 53 | setMood(MOOD_BROKEN, "", "Error: unknown", true); 54 | state = STATE_HALT; 55 | } 56 | } 57 | 58 | void loop() { 59 | M5.update(); 60 | M5Cardputer.update(); 61 | 62 | if (state == STATE_HALT) { 63 | return; 64 | } 65 | 66 | if (state == STATE_INIT) { 67 | wakeUp(); 68 | state = STATE_WAKE; 69 | } 70 | 71 | if (state == STATE_WAKE) { 72 | checkPwngridGoneFriends(); 73 | advertise(current_channel++); 74 | if (current_channel == 15) { 75 | current_channel = 1; 76 | } 77 | } 78 | 79 | updateUi(true); 80 | } 81 | -------------------------------------------------------------------------------- /palnagotchi/pwnagotchi.h: -------------------------------------------------------------------------------- 1 | const String pwnagotchi_moods[] = { 2 | "(⇀‿‿↼)", // 0 - sleeping 3 | "(≖‿‿≖)", // 1 - awakening 4 | "(◕‿‿◕)", // 2 - awake 5 | "( ⚆_⚆)", // 3 - observing (neutral) right 6 | "(☉_☉ )", // 4 - observig (neutral) left 7 | "( ◕‿◕)", // 5 - observing (happy) right 8 | "(◕‿◕ )", // 6 - observing (happy) left 9 | "(°▃▃°)", // 7 - intense 10 | "(⌐■_■)", // 8 - cool 11 | "(•‿‿•)", // 9 - happy 12 | "(^‿‿^)", // 10 - grateful 13 | "(ᵔ◡◡ᵔ)", // 11 - excited 14 | "(✜‿‿✜)", // 12 - smart 15 | "(♥‿‿♥)", // 13 - friendly 16 | "(☼‿‿☼)", // 14 - motivated 17 | "(≖__≖)", // 15 - demotivated 18 | "(-__-)", // 16 - bored 19 | "(╥☁╥ )", // 17 - sad 20 | "(ب__ب)", // 18 - lonely 21 | "(☓‿‿☓)", // 19 - broken 22 | "(#__#)" // 20 - debugging 23 | }; 24 | -------------------------------------------------------------------------------- /palnagotchi/pwngrid.cpp: -------------------------------------------------------------------------------- 1 | #include "pwngrid.h" 2 | 3 | uint8_t pwngrid_friends_tot = 0; 4 | pwngrid_peer pwngrid_peers[255]; 5 | String pwngrid_last_friend_name = ""; 6 | 7 | uint8_t getPwngridTotalPeers() { return EEPROM.read(0) + pwngrid_friends_tot; } 8 | uint8_t getPwngridRunTotalPeers() { return pwngrid_friends_tot; } 9 | String getPwngridLastFriendName() { return pwngrid_last_friend_name; } 10 | pwngrid_peer *getPwngridPeers() { return pwngrid_peers; } 11 | 12 | // Had to remove Radiotap headers, since its automatically added 13 | // Also had to remove the last 4 bytes (frame check sequence) 14 | const uint8_t pwngrid_beacon_raw[] = { 15 | 0x80, 0x00, // FC 16 | 0x00, 0x00, // Duration 17 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // DA (broadcast) 18 | 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, // SA 19 | 0xa1, 0x00, 0x64, 0xe6, 0x0b, 0x8b, // BSSID 20 | 0x40, 0x43, // Sequence number/fragment number/seq-ctl 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Timestamp 22 | 0x64, 0x00, // Beacon interval 23 | 0x11, 0x04, // Capability info 24 | // 0xde (AC = 222) + 1 byte payload len + payload (AC Header) 25 | // For each 255 bytes of the payload, a new AC header should be set 26 | }; 27 | 28 | const int raw_beacon_len = sizeof(pwngrid_beacon_raw); 29 | 30 | esp_err_t esp_wifi_80211_tx(wifi_interface_t ifx, const void *buffer, int len, 31 | bool en_sys_seq); 32 | 33 | esp_err_t pwngridAdvertise(uint8_t channel, String face) { 34 | DynamicJsonDocument pal_json(2048); 35 | String pal_json_str = ""; 36 | 37 | pal_json["pal"] = true; // Also detect other Palnagotchis 38 | pal_json["name"] = "Palnagotchi"; 39 | pal_json["face"] = face; 40 | pal_json["epoch"] = 1; 41 | pal_json["grid_version"] = "1.10.3"; 42 | pal_json["identity"] = 43 | "32e9f315e92d974342c93d0fd952a914bfb4e6838953536ea6f63d54db6b9610"; 44 | pal_json["pwnd_run"] = 0; 45 | pal_json["pwnd_tot"] = 0; 46 | pal_json["session_id"] = "a2:00:64:e6:0b:8b"; 47 | pal_json["timestamp"] = 0; 48 | pal_json["uptime"] = 0; 49 | pal_json["version"] = "1.8.4"; 50 | pal_json["policy"]["advertise"] = true; 51 | pal_json["policy"]["bond_encounters_factor"] = 20000; 52 | pal_json["policy"]["bored_num_epochs"] = 0; 53 | pal_json["policy"]["sad_num_epochs"] = 0; 54 | pal_json["policy"]["excited_num_epochs"] = 9999; 55 | 56 | serializeJson(pal_json, pal_json_str); 57 | uint16_t pal_json_len = measureJson(pal_json); 58 | uint8_t header_len = 2 + ((uint8_t)(pal_json_len / 255) * 2); 59 | uint8_t pwngrid_beacon_frame[raw_beacon_len + pal_json_len + header_len]; 60 | memcpy(pwngrid_beacon_frame, pwngrid_beacon_raw, raw_beacon_len); 61 | 62 | // Iterate through json string and copy it to beacon frame 63 | int frame_byte = raw_beacon_len; 64 | for (int i = 0; i < pal_json_len; i++) { 65 | // Write AC and len tags before every 255 bytes 66 | if (i == 0 || i % 255 == 0) { 67 | pwngrid_beacon_frame[frame_byte++] = 0xde; // AC = 222 68 | uint8_t payload_len = 255; 69 | if (pal_json_len - i < 255) { 70 | payload_len = pal_json_len - i; 71 | } 72 | 73 | pwngrid_beacon_frame[frame_byte++] = payload_len; 74 | } 75 | 76 | // Append json byte to frame 77 | // If current byte is not ascii, add ? instead 78 | uint8_t next_byte = (uint8_t)'?'; 79 | if (isAscii(pal_json_str[i])) { 80 | next_byte = (uint8_t)pal_json_str[i]; 81 | } 82 | 83 | pwngrid_beacon_frame[frame_byte++] = next_byte; 84 | } 85 | 86 | // Channel switch not working? 87 | // vTaskDelay(500 / portTICK_PERIOD_MS); 88 | esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); 89 | delay(102); 90 | // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417esp_wifi_80211_tx16wifi_interface_tPKvib 91 | // vTaskDelay(103 / portTICK_PERIOD_MS); 92 | esp_err_t result = esp_wifi_80211_tx(WIFI_IF_AP, pwngrid_beacon_frame, 93 | sizeof(pwngrid_beacon_frame), false); 94 | return result; 95 | } 96 | 97 | void pwngridAddPeer(DynamicJsonDocument &json, signed int rssi) { 98 | String identity = json["identity"].as(); 99 | 100 | for (uint8_t i = 0; i < pwngrid_friends_tot; i++) { 101 | // Check if peer identity is already in peers array 102 | if (pwngrid_peers[i].identity == identity) { 103 | pwngrid_peers[i].last_ping = millis(); 104 | pwngrid_peers[i].gone = false; 105 | pwngrid_peers[i].rssi = rssi; 106 | return; 107 | } 108 | } 109 | 110 | pwngrid_peers[pwngrid_friends_tot].rssi = rssi; 111 | pwngrid_peers[pwngrid_friends_tot].last_ping = millis(); 112 | pwngrid_peers[pwngrid_friends_tot].gone = false; 113 | pwngrid_peers[pwngrid_friends_tot].name = json["name"].as(); 114 | pwngrid_peers[pwngrid_friends_tot].face = json["face"].as(); 115 | pwngrid_peers[pwngrid_friends_tot].epoch = json["epoch"].as(); 116 | pwngrid_peers[pwngrid_friends_tot].grid_version = 117 | json["grid_version"].as(); 118 | pwngrid_peers[pwngrid_friends_tot].identity = identity; 119 | pwngrid_peers[pwngrid_friends_tot].pwnd_run = json["pwnd_run"].as(); 120 | pwngrid_peers[pwngrid_friends_tot].pwnd_tot = json["pwnd_tot"].as(); 121 | pwngrid_peers[pwngrid_friends_tot].session_id = 122 | json["session_id"].as(); 123 | pwngrid_peers[pwngrid_friends_tot].timestamp = json["timestamp"].as(); 124 | pwngrid_peers[pwngrid_friends_tot].uptime = json["uptime"].as(); 125 | pwngrid_peers[pwngrid_friends_tot].version = json["version"].as(); 126 | pwngrid_last_friend_name = pwngrid_peers[pwngrid_friends_tot].name; 127 | pwngrid_friends_tot++; 128 | EEPROM.write(0, pwngrid_friends_tot); 129 | } 130 | 131 | const int away_threshold = 120000; 132 | 133 | void checkPwngridGoneFriends() { 134 | for (uint8_t i = 0; i < pwngrid_friends_tot; i++) { 135 | // Check if peer is away for more then 136 | int away_secs = pwngrid_peers[i].last_ping - millis(); 137 | if (away_secs > away_threshold) { 138 | pwngrid_peers[i].gone = true; 139 | return; 140 | } 141 | } 142 | } 143 | 144 | signed int getPwngridClosestRssi() { 145 | signed int closest = -1000; 146 | 147 | for (uint8_t i = 0; i < pwngrid_friends_tot; i++) { 148 | // Check if peer is away for more then 149 | if (pwngrid_peers[i].gone == false && pwngrid_peers[i].rssi > closest) { 150 | closest = pwngrid_peers[i].rssi; 151 | } 152 | } 153 | 154 | return closest; 155 | } 156 | 157 | // Detect pwnagotchi adapted from Marauder 158 | // https://github.com/justcallmekoko/ESP32Marauder/wiki/detect-pwnagotchi 159 | // https://github.com/justcallmekoko/ESP32Marauder/blob/master/esp32_marauder/WiFiScan.cpp#L2255 160 | typedef struct { 161 | int16_t fctl; 162 | int16_t duration; 163 | uint8_t da; 164 | uint8_t sa; 165 | uint8_t bssid; 166 | int16_t seqctl; 167 | unsigned char payload[]; 168 | } __attribute__((packed)) WifiMgmtHdr; 169 | 170 | typedef struct { 171 | uint8_t payload[0]; 172 | WifiMgmtHdr hdr; 173 | } wifi_ieee80211_packet_t; 174 | 175 | void getMAC(char *addr, uint8_t *data, uint16_t offset) { 176 | sprintf(addr, "%02x:%02x:%02x:%02x:%02x:%02x", data[offset + 0], 177 | data[offset + 1], data[offset + 2], data[offset + 3], 178 | data[offset + 4], data[offset + 5]); 179 | } 180 | 181 | void pwnSnifferCallback(void *buf, wifi_promiscuous_pkt_type_t type) { 182 | wifi_promiscuous_pkt_t *snifferPacket = (wifi_promiscuous_pkt_t *)buf; 183 | WifiMgmtHdr *frameControl = (WifiMgmtHdr *)snifferPacket->payload; 184 | 185 | String src = ""; 186 | String essid = ""; 187 | 188 | if (type == WIFI_PKT_MGMT) { 189 | // Remove frame check sequence bytes 190 | int len = snifferPacket->rx_ctrl.sig_len - 4; 191 | int fctl = ntohs(frameControl->fctl); 192 | const wifi_ieee80211_packet_t *ipkt = 193 | (wifi_ieee80211_packet_t *)snifferPacket->payload; 194 | const WifiMgmtHdr *hdr = &ipkt->hdr; 195 | 196 | // if ((snifferPacket->payload[0] == 0x80) && (buf == 0)) { 197 | if ((snifferPacket->payload[0] == 0x80)) { 198 | char addr[] = "00:00:00:00:00:00"; 199 | getMAC(addr, snifferPacket->payload, 10); 200 | src.concat(addr); 201 | if (src == "de:ad:be:ef:de:ad") { 202 | // Just grab the first 255 bytes of the pwnagotchi beacon 203 | // because that is where the name is 204 | for (int i = 38; i < len; i++) { 205 | if (isAscii(snifferPacket->payload[i])) { 206 | essid.concat((char)snifferPacket->payload[i]); 207 | } 208 | } 209 | 210 | DynamicJsonDocument sniffed_json(2048); // ArduinoJson v6s 211 | ArduinoJson::V6215PB2::DeserializationError result = 212 | deserializeJson(sniffed_json, essid); 213 | 214 | if (result == ArduinoJson::V6215PB2::DeserializationError::Ok) { 215 | // Serial.println("\nSuccessfully parsed json"); 216 | // serializeJson(json, Serial); // ArduinoJson v6 217 | pwngridAddPeer(sniffed_json, snifferPacket->rx_ctrl.rssi); 218 | } else if (result == ArduinoJson::V6215PB2::DeserializationError:: 219 | IncompleteInput) { 220 | Serial.println("Deserialization error: incomplete input"); 221 | } else if (result == 222 | ArduinoJson::V6215PB2::DeserializationError::NoMemory) { 223 | Serial.println("Deserialization error: no memory"); 224 | } else if (result == 225 | ArduinoJson::V6215PB2::DeserializationError::InvalidInput) { 226 | Serial.println("Deserialization error: invalid input"); 227 | } else if (result == 228 | ArduinoJson::V6215PB2::DeserializationError::TooDeep) { 229 | Serial.println("Deserialization error: too deep"); 230 | } else { 231 | Serial.println(essid); 232 | Serial.println("Deserialization error"); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | const wifi_promiscuous_filter_t filter = { 240 | .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA}; 241 | 242 | void initPwngrid() { 243 | wifi_init_config_t WIFI_INIT_CONFIG = WIFI_INIT_CONFIG_DEFAULT(); 244 | esp_wifi_init(&WIFI_INIT_CONFIG); 245 | esp_wifi_set_storage(WIFI_STORAGE_RAM); 246 | esp_wifi_set_mode(WIFI_MODE_AP); 247 | esp_wifi_start(); 248 | esp_wifi_set_promiscuous_filter(&filter); 249 | esp_wifi_set_promiscuous(true); 250 | esp_wifi_set_promiscuous_rx_cb(&pwnSnifferCallback); 251 | // esp_wifi_set_ps(WIFI_PS_NONE); 252 | esp_wifi_set_channel(random(0, 14), WIFI_SECOND_CHAN_NONE); 253 | delay(1); 254 | } 255 | -------------------------------------------------------------------------------- /palnagotchi/pwngrid.h: -------------------------------------------------------------------------------- 1 | #include "ArduinoJson.h" 2 | #include "EEPROM.h" 3 | #include "M5Cardputer.h" 4 | #include "M5Unified.h" 5 | #include "esp_wifi.h" 6 | #include "esp_wifi_types.h" 7 | // #include "freertos/FreeRTOS.h" 8 | 9 | typedef struct { 10 | int epoch; 11 | String face; 12 | String grid_version; 13 | String identity; 14 | String name; 15 | int pwnd_run; 16 | int pwnd_tot; 17 | String session_id; 18 | int timestamp; 19 | int uptime; 20 | String version; 21 | signed int rssi; 22 | int last_ping; 23 | bool gone; 24 | } pwngrid_peer; 25 | 26 | void initPwngrid(); 27 | esp_err_t pwngridAdvertise(uint8_t channel, String face); 28 | pwngrid_peer* getPwngridPeers(); 29 | uint8_t getPwngridRunTotalPeers(); 30 | uint8_t getPwngridTotalPeers(); 31 | String getPwngridLastFriendName(); 32 | signed int getPwngridClosestRssi(); 33 | void checkPwngridGoneFriends(); 34 | -------------------------------------------------------------------------------- /palnagotchi/ui.cpp: -------------------------------------------------------------------------------- 1 | #include "ui.h" 2 | 3 | M5Canvas canvas_top(&M5.Display); 4 | M5Canvas canvas_main(&M5.Display); 5 | M5Canvas canvas_bot(&M5.Display); 6 | // M5Canvas canvas_peers_menu(&M5.Display); 7 | 8 | int32_t display_w; 9 | int32_t display_h; 10 | int32_t canvas_h; 11 | int32_t canvas_center_x; 12 | int32_t canvas_top_h; 13 | int32_t canvas_bot_h; 14 | int32_t canvas_peers_menu_h; 15 | int32_t canvas_peers_menu_w; 16 | 17 | struct menu { 18 | char name[25]; 19 | int command; 20 | }; 21 | 22 | menu main_menu[] = { 23 | {"Nearby Pwnagotchis", 2}, 24 | // {"Settings", 4}, 25 | {"About", 8} 26 | // {"Friend spam", 16}, 27 | }; 28 | 29 | menu settings_menu[] = { 30 | {"Change name", 40}, 31 | {"Display brightness", 41}, 32 | {"Sound", 42}, 33 | }; 34 | 35 | int main_menu_len = sizeof(main_menu) / sizeof(menu); 36 | int settings_menu_len = sizeof(settings_menu) / sizeof(menu); 37 | 38 | bool menu_open = false; 39 | uint8_t menu_current_cmd = 0; 40 | uint8_t menu_current_opt = 0; 41 | 42 | void initUi() { 43 | M5.Display.setRotation(1); 44 | M5.Display.setTextFont(&fonts::Font0); 45 | M5.Display.setTextSize(1); 46 | M5.Display.fillScreen(TFT_BLACK); 47 | M5.Display.setTextColor(GREEN); 48 | M5.Display.setColor(GREEN); 49 | 50 | display_w = M5.Display.width(); 51 | display_h = M5.Display.height(); 52 | canvas_h = display_h * .8; 53 | canvas_center_x = display_w / 2; 54 | canvas_top_h = display_h * .1; 55 | canvas_bot_h = display_h * .1; 56 | canvas_peers_menu_h = display_h * .8; 57 | canvas_peers_menu_w = display_w * .8; 58 | 59 | canvas_top.createSprite(display_w, canvas_top_h); 60 | canvas_bot.createSprite(display_w, canvas_bot_h); 61 | canvas_main.createSprite(display_w, canvas_h); 62 | } 63 | 64 | bool keyboard_changed = false; 65 | 66 | bool toggleMenuBtnPressed() { 67 | return M5Cardputer.BtnA.isPressed() || 68 | (keyboard_changed && (M5Cardputer.Keyboard.isKeyPressed('m') || 69 | M5Cardputer.Keyboard.isKeyPressed('`'))); 70 | } 71 | 72 | bool isOkPressed() { 73 | return M5Cardputer.BtnA.isPressed() || 74 | (keyboard_changed && M5Cardputer.Keyboard.isKeyPressed(KEY_ENTER)); 75 | } 76 | 77 | bool isNextPressed() { 78 | return keyboard_changed && (M5Cardputer.Keyboard.isKeyPressed('.') || 79 | M5Cardputer.Keyboard.isKeyPressed('/') || 80 | M5Cardputer.Keyboard.isKeyPressed(KEY_TAB)); 81 | } 82 | bool isPrevPressed() { 83 | return keyboard_changed && (M5Cardputer.Keyboard.isKeyPressed(',') || 84 | M5Cardputer.Keyboard.isKeyPressed(';')); 85 | } 86 | 87 | void updateUi(bool show_toolbars) { 88 | keyboard_changed = M5Cardputer.Keyboard.isChange(); 89 | 90 | if (toggleMenuBtnPressed()) { 91 | // If menu is open, return to main menu 92 | // If not, toggle menu 93 | if (menu_open == true && menu_current_cmd != 0) { 94 | menu_current_cmd = 0; 95 | menu_current_opt = 0; 96 | } else { 97 | menu_open = !menu_open; 98 | } 99 | } 100 | 101 | uint8_t mood_id = getCurrentMoodId(); 102 | String mood_face = getCurrentMoodFace(); 103 | String mood_phrase = getCurrentMoodPhrase(); 104 | bool mood_broken = isCurrentMoodBroken(); 105 | 106 | drawTopCanvas(); 107 | drawBottomCanvas(getPwngridRunTotalPeers(), getPwngridTotalPeers(), 108 | getPwngridLastFriendName(), getPwngridClosestRssi()); 109 | 110 | if (menu_open) { 111 | drawMenu(); 112 | } else { 113 | drawMood(mood_face, mood_phrase, mood_broken); 114 | } 115 | 116 | M5.Display.startWrite(); 117 | if (show_toolbars) { 118 | canvas_top.pushSprite(0, 0); 119 | canvas_bot.pushSprite(0, canvas_top_h + canvas_h); 120 | } 121 | canvas_main.pushSprite(0, canvas_top_h); 122 | M5.Display.endWrite(); 123 | } 124 | 125 | void drawTopCanvas() { 126 | canvas_top.fillSprite(BLACK); 127 | canvas_top.setTextSize(1); 128 | canvas_top.setTextColor(GREEN); 129 | canvas_top.setColor(GREEN); 130 | canvas_top.setTextDatum(top_left); 131 | canvas_top.drawString("CH *", 0, 3); 132 | canvas_top.setTextDatum(top_right); 133 | unsigned long ellapsed = millis() / 1000; 134 | int8_t h = ellapsed / 3600; 135 | int sr = ellapsed % 3600; 136 | int8_t m = sr / 60; 137 | int8_t s = sr % 60; 138 | char right_str[50] = "UPS 0% UP 00:00:00"; 139 | sprintf(right_str, "UPS %i%% UP %02d:%02d:%02d", M5.Power.getBatteryLevel(), 140 | h, m, s); 141 | canvas_top.drawString(right_str, display_w, 3); 142 | canvas_top.drawLine(0, canvas_top_h - 1, display_w, canvas_top_h - 1); 143 | } 144 | 145 | String getRssiBars(signed int rssi) { 146 | String rssi_bars = ""; 147 | 148 | if (rssi != -1000) { 149 | if (rssi >= -67) { 150 | rssi_bars = "||||"; 151 | } else if (rssi >= -70) { 152 | rssi_bars = "|||"; 153 | } else if (rssi >= -80) { 154 | rssi_bars = "||"; 155 | } else { 156 | rssi_bars = "|"; 157 | } 158 | } 159 | 160 | return rssi_bars; 161 | } 162 | 163 | void drawBottomCanvas(uint8_t friends_run, uint8_t friends_tot, 164 | String last_friend_name, signed int rssi) { 165 | canvas_bot.fillSprite(BLACK); 166 | canvas_bot.setTextSize(1); 167 | canvas_bot.setTextColor(GREEN); 168 | canvas_bot.setColor(GREEN); 169 | canvas_bot.setTextDatum(top_left); 170 | 171 | // https://github.com/evilsocket/pwnagotchi/blob/2122af4e264495d32ee415c074da8efd905901f0/pwnagotchi/ui/view.py#L191 172 | String rssi_bars = getRssiBars(rssi); 173 | char stats[25] = "FRND 0 (0)"; 174 | if (friends_run > 0) { 175 | sprintf(stats, "FRND %d (%d) [%s] %s", friends_run, friends_tot, 176 | last_friend_name, rssi_bars); 177 | } 178 | 179 | canvas_bot.drawString(stats, 0, 5); 180 | canvas_bot.setTextDatum(top_right); 181 | canvas_bot.drawString("NOT AI", display_w, 5); 182 | canvas_bot.drawLine(0, 0, display_w, 0); 183 | } 184 | 185 | void drawMood(String face, String phrase, bool broken) { 186 | if (broken == true) { 187 | canvas_main.setTextColor(RED); 188 | } else { 189 | canvas_main.setTextColor(GREEN); 190 | } 191 | 192 | canvas_main.setTextSize(4); 193 | canvas_main.setTextDatum(middle_center); 194 | canvas_main.fillSprite(BLACK); 195 | canvas_main.drawString(face, canvas_center_x, canvas_h / 2); 196 | canvas_main.setTextDatum(bottom_center); 197 | canvas_main.setTextSize(1); 198 | canvas_main.drawString(phrase, canvas_center_x, canvas_h - 23); 199 | } 200 | 201 | #define ROW_SIZE 40 202 | #define PADDING 10 203 | 204 | void drawMainMenu() { 205 | canvas_main.fillSprite(BLACK); 206 | canvas_main.setTextSize(2); 207 | canvas_main.setTextColor(GREEN); 208 | canvas_main.setColor(GREEN); 209 | canvas_main.setTextDatum(top_left); 210 | 211 | char display_str[50] = ""; 212 | for (uint8_t i = 0; i < main_menu_len; i++) { 213 | sprintf(display_str, "%s %s", (menu_current_opt == i) ? ">" : " ", 214 | main_menu[i].name); 215 | int y = PADDING + (i * ROW_SIZE / 2); 216 | canvas_main.drawString(display_str, 0, y); 217 | } 218 | } 219 | 220 | void drawNearbyMenu() { 221 | canvas_main.clear(BLACK); 222 | canvas_main.setTextSize(2); 223 | canvas_main.setTextColor(GREEN); 224 | canvas_main.setColor(GREEN); 225 | canvas_main.setTextDatum(top_left); 226 | 227 | pwngrid_peer* pwngrid_peers = getPwngridPeers(); 228 | uint8_t len = getPwngridRunTotalPeers(); 229 | 230 | if (len == 0) { 231 | canvas_main.setTextColor(TFT_DARKGRAY); 232 | canvas_main.setCursor(0, PADDING); 233 | canvas_main.println("No nearby Pwnagotchis. Seriously?"); 234 | } 235 | 236 | char display_str[50] = ""; 237 | for (uint8_t i = 0; i < len; i++) { 238 | sprintf(display_str, "%s %s [%s]", (menu_current_opt == i) ? ">" : " ", 239 | pwngrid_peers[i].name, getRssiBars(pwngrid_peers[i].rssi)); 240 | int y = PADDING + (i * ROW_SIZE / 2); 241 | canvas_main.drawString(display_str, 0, y); 242 | } 243 | } 244 | 245 | void drawSettingsMenu() { 246 | canvas_main.fillSprite(BLACK); 247 | canvas_main.setTextSize(2); 248 | canvas_main.setTextColor(GREEN); 249 | canvas_main.setColor(GREEN); 250 | canvas_main.setTextDatum(top_left); 251 | 252 | char display_str[50] = ""; 253 | for (uint8_t i = 0; i < settings_menu_len; i++) { 254 | sprintf(display_str, "%s %s", (menu_current_opt == i) ? ">" : " ", 255 | settings_menu[i].name); 256 | int y = PADDING + (i * ROW_SIZE / 2); 257 | canvas_main.drawString(display_str, 0, y); 258 | } 259 | } 260 | 261 | void drawAboutMenu() { 262 | canvas_main.clear(BLACK); 263 | canvas_main.qrcode("https://github.com/viniciusbo/m5-palnagotchi", 264 | (display_w / 2) - (display_h * 0.3), PADDING, 265 | display_h * 0.65); 266 | } 267 | 268 | void drawMenu() { 269 | if (isNextPressed()) { 270 | // if (menu_current_opt < menu_current_size - 1) { 271 | menu_current_opt++; 272 | // } else { 273 | // menu_current_opt = 0; 274 | // } 275 | } 276 | 277 | if (isPrevPressed()) { 278 | if (menu_current_opt > 0) { 279 | menu_current_opt--; 280 | } 281 | } 282 | 283 | // Change menu 284 | 285 | switch (menu_current_cmd) { 286 | case 0: 287 | if (isOkPressed()) { 288 | menu_current_cmd = main_menu[menu_current_opt].command; 289 | menu_current_opt = 0; 290 | } 291 | drawMainMenu(); 292 | break; 293 | case 2: 294 | drawNearbyMenu(); 295 | break; 296 | case 4: 297 | if (isOkPressed()) { 298 | menu_current_cmd = settings_menu[menu_current_opt].command; 299 | menu_current_opt = 0; 300 | } 301 | drawSettingsMenu(); 302 | break; 303 | case 8: 304 | drawAboutMenu(); 305 | break; 306 | default: 307 | drawMainMenu(); 308 | break; 309 | } 310 | } 311 | 312 | // bool check_prev_press() { 313 | // if (M5.Keyboard.isKeyPressed(ARROW_UP)) { 314 | // return true; 315 | // } 316 | // 317 | // return false; 318 | // } 319 | // 320 | // bool check_next_press() { 321 | // if (M5.Keyboard.isKeyPressed(ARROW_DOWN)) { 322 | // return true; 323 | // } 324 | // 325 | // return false; 326 | // } 327 | -------------------------------------------------------------------------------- /palnagotchi/ui.h: -------------------------------------------------------------------------------- 1 | #include "M5Cardputer.h" 2 | #include "mood.h" 3 | #include "pwngrid.h" 4 | 5 | void initUi(); 6 | void wakeUp(); 7 | void drawMood(String face, String phrase, bool broken = false); 8 | void drawTopCanvas(); 9 | void drawBottomCanvas(uint8_t friends_run = 0, uint8_t friends_tot = 0, 10 | String last_friend_name = "", signed int rssi = -1000); 11 | void drawMenu(); 12 | void updateUi(bool show_toolbars = false); 13 | -------------------------------------------------------------------------------- /pwngrid_beacon.hexdump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viniciusbo/m5-palnagotchi/b994bca904fff6d614e5eb481afd2cb266f239b9/pwngrid_beacon.hexdump --------------------------------------------------------------------------------