A Low Cost ESP32 Network Threat Monitor
I had an old ESP WROVER KIT sitting around and wanted to do something useful with it.
So I turned it into a small live network threat monitor.
The ESP32 only handles the display. The actual monitoring runs on the PC with Python, Npcap and Scapy, then sends the interesting data to the WROVER over USB. I also added passive lookups using AbuseIPDB, Shodan, Censys and ZoomEye.
The idea was not to make another terminal full of scrolling IP addresses, but something I could leave on the desk and understand at a glance.
Normal traffic stays on the world map. More interesting traffic can open a small CTF style screen with the IP, port, process, ASN and the reason it was flagged.
It is still only a proof of concept, but I like how it turned out.
#include <SPI.h>
#include <math.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
// ============================================================
// MAP TYPES / SAFE FOR ARDUINO PREPROCESSOR
// ============================================================
struct MapPoint {
int16_t x;
int16_t y;
};
void fillPolygon(
const MapPoint *pts,
int count,
uint16_t fillColor,
uint16_t outlineColor
);
// ============================================================
// ESP-WROVER-KIT V3 / ST7789V
// ============================================================
#define LCD_MOSI 23
#define LCD_MISO 25
#define LCD_CLK 19
#define LCD_CS 22
#define LCD_DC 21
#define LCD_RST 18
#define LCD_BL 5
// ESP-WROVER-KIT rear RGB LED
#define RGB_LED_R 0
#define RGB_LED_G 2
#define RGB_LED_B 4
// ESP-WROVER-KIT V3 onboard RGB diagnostic LED (active LOW)
#define BOARD_LED_R 0
#define BOARD_LED_G 2
#define BOARD_LED_B 4
SPIClass lcdSPI(VSPI);
// ============================================================
// COLORS RGB565
// ============================================================
#define C_BLACK 0x0000
#define C_WHITE 0xFFFF
#define C_BG 0x0000 // black background
#define C_PANEL 0x0000 // black badge background
#define C_GRID 0x0000 // disabled
#define C_TEXTDIM 0x07E0 // green UI text
#define C_CLOCK 0x07E0
#define C_LAND_FILL 0x4A69 // ~30% gray on black
#define C_LAND_OUTLINE 0x4A69
#define C_LAND_MINOR 0x4A69
#define C_COUNTRY_BORDER 0x632C // ~40% gray actual country borders
#define C_CYAN 0x07FF
#define C_GREEN 0x07E0
#define C_YELLOW 0xFFE0
#define C_RED 0xF800
#define C_MAGENTA 0xF81F
#define C_BLUE 0x3DDF
// ============================================================
// REGIONS
// ============================================================
enum RegionIndex {
REG_US = 0,
REG_EU,
REG_RU,
REG_CN,
REG_IL,
REG_JP,
REG_COUNT
};
const char* regionLabel[REG_COUNT] = {
"US", "EU", "RU", "CN", "IL", "JP"
};
// Region colors are assigned dynamically:
// old = blue, latest = green, alert = red.
// marker center points on map
const int markerX[REG_COUNT] = { 76, 167, 228, 251, 190, 283 };
const int markerY[REG_COUNT] = { 87, 76, 65, 91, 95, 91 };
// badge positions
const int badgeX[REG_COUNT] = { 85, 176, 237, 260, 199, 291 };
const int badgeY[REG_COUNT] = { 79, 68, 57, 83, 87, 83 };
// ============================================================
// LIVE DATA
// ============================================================
int countUS = 14;
int countEU = 8;
int countRU = 3;
int countCN = 6;
int countIL = 2;
int countJP = 2;
int totalIPs = 0;
int alertCount = 0;
// Traffic is stored in bytes so the display can choose B / KB / MB / GB.
uint64_t uploadBytes = 0;
uint64_t downloadBytes = 0;
// Marker state
int latestRegion = -1;
int alertRegion = -1;
bool pulseState = true; // kept for compatibility; region markers are now static
// ping state
bool pingAlive = true;
int pingMs = 24;
bool pingBlinkState = true;
unsigned long lastPingBlinkTick = 0;
unsigned long lastPingUpdateTick = 0;
const unsigned long PING_BLINK_MS = 500;
const unsigned long PING_STALE_MS = 15000; // no host refresh for 15 s => dead/red
// clock
int clkH = 0;
int clkM = 0;
int clkS = 0;
unsigned long lastClockTick = 0;
unsigned long lastPcClockSync = 0;
bool clockStale = true;
unsigned long lastPulseTick = 0;
// date: MON SEP 07
int dateDow = 1; // SUN=0, MON=1 ...
int dateMon = 8; // JAN=0
int dateDay = 7;
// serial line buffer
char serialBuf[128];
int serialLen = 0;
// ============================================================
// LIVE REGION AGGREGATION
// The PC groups real remote IPs into coarse regions so the small
// 320x240 display never stacks many labels on top of one another.
// ============================================================
enum LiveRegionIndex {
LREG_US = 0, // United States / Canada
LREG_EU, // Europe
LREG_RU, // Russia / nearby Eurasia
LREG_CN, // China
LREG_IL, // Israel / Middle East
LREG_JP, // Japan / Korea
LREG_IN, // India / South Asia
LREG_SEA, // Southeast Asia
LREG_SA, // South America
LREG_AF, // Africa
LREG_AU, // Australia / New Zealand / Oceania
LREG_COUNT
};
const char* liveRegionLabel[LREG_COUNT] = {
"US","EU","RU","CN","IL","JP","IN","SEA","SA","AF","AU"
};
// Marker positions chosen for readability on the 300x153 map,
// not exact centroids. This intentionally prevents overlaps.
const int liveRegionX[LREG_COUNT] = {
58, 145, 210, 252, 180, 286, 220, 255, 92, 156, 274
};
const int liveRegionY[LREG_COUNT] = {
80, 70, 54, 92, 108, 104, 128, 146, 166, 128, 182
};
uint16_t liveRegionCount[LREG_COUNT] = {0};
int latestLiveRegion = -1;
int alertLiveRegion = -1;
enum ScreenMode {
SCREEN_WORLD = 0,
SCREEN_CTF
};
ScreenMode currentScreen = SCREEN_WORLD;
char ctfRegion[8] = "--";
char ctfCode[8] = "--";
char ctfIp[20] = "0.0.0.0";
char ctfProc[16] = "UNKNOWN";
char ctfLevel = 'W'; // W=watch, A=alert
int ctfPort = 0;
int ctfConns = 0;
int ctfAsn = 0;
int ctfFlags = 0;
unsigned long ctfTxKB = 0;
unsigned long ctfRxKB = 0;
char ctfIntelTag[16] = "LOOKUP";
int ctfIntelVulns = 0;
int ctfIntelPorts = 0; // bitmask: 1 GEO, 2 PORT, 4 BURST, 8 THREAT-INTEL
int ctfCount = 0;
int ctfPhase = 0;
unsigned long ctfShownAt = 0;
unsigned long ctfAnimTick = 0;
bool ctfBlink = false;
const unsigned long CTF_AUTO_RETURN_MS = 7000; // readable CTF dwell time // 12 seconds: readable, not a flash
void forceRgbLedOff()
{
pinMode(RGB_LED_R, OUTPUT);
pinMode(RGB_LED_G, OUTPUT);
pinMode(RGB_LED_B, OUTPUT);
// WROVER-KIT RGB LED is active-low.
digitalWrite(RGB_LED_R, LOW);
digitalWrite(RGB_LED_G, LOW);
digitalWrite(RGB_LED_B, LOW);
}
// ============================================================
// LOW LEVEL LCD
// ============================================================
void lcdCommand(uint8_t cmd)
{
digitalWrite(LCD_DC, LOW);
digitalWrite(LCD_CS, LOW);
lcdSPI.transfer(cmd);
digitalWrite(LCD_CS, HIGH);
}
void lcdData(const uint8_t *data, size_t len)
{
digitalWrite(LCD_DC, HIGH);
digitalWrite(LCD_CS, LOW);
while (len--) {
lcdSPI.transfer(*data++);
}
digitalWrite(LCD_CS, HIGH);
}
void lcdCommandData(uint8_t cmd, const uint8_t *data, size_t len)
{
lcdCommand(cmd);
if (len) lcdData(data, len);
}
void lcdReset()
{
digitalWrite(LCD_RST, HIGH);
delay(20);
digitalWrite(LCD_RST, LOW);
delay(100);
digitalWrite(LCD_RST, HIGH);
delay(150);
}
void lcdInit()
{
lcdReset();
{
uint8_t d[] = { 0x60 };
lcdCommandData(0x36, d, sizeof(d));
}
{
uint8_t d[] = { 0x55 };
lcdCommandData(0x3A, d, sizeof(d));
}
{
uint8_t d[] = { 0x0C, 0x0C, 0x00, 0x33, 0x33 };
lcdCommandData(0xB2, d, sizeof(d));
}
{
uint8_t d[] = { 0x45 };
lcdCommandData(0xB7, d, sizeof(d));
}
{
uint8_t d[] = { 0x2B };
lcdCommandData(0xBB, d, sizeof(d));
}
{
uint8_t d[] = { 0x2C };
lcdCommandData(0xC0, d, sizeof(d));
}
{
uint8_t d[] = { 0x01, 0xFF };
lcdCommandData(0xC2, d, sizeof(d));
}
{
uint8_t d[] = { 0x11 };
lcdCommandData(0xC3, d, sizeof(d));
}
{
uint8_t d[] = { 0x20 };
lcdCommandData(0xC4, d, sizeof(d));
}
{
uint8_t d[] = { 0x0F };
lcdCommandData(0xC6, d, sizeof(d));
}
{
uint8_t d[] = { 0xA4, 0xA1 };
lcdCommandData(0xD0, d, sizeof(d));
}
{
uint8_t d[] = {
0xD0,0x00,0x05,0x0E,
0x15,0x0D,0x37,0x43,
0x47,0x09,0x15,0x12,
0x16,0x19
};
lcdCommandData(0xE0, d, sizeof(d));
}
{
uint8_t d[] = {
0xD0,0x00,0x05,0x0D,
0x0C,0x06,0x2D,0x44,
0x40,0x0E,0x1C,0x18,
0x16,0x19
};
lcdCommandData(0xE1, d, sizeof(d));
}
lcdCommand(0x11);
delay(120);
lcdCommand(0x29);
delay(100);
}
void lcdWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
uint8_t d[4];
d[0] = x0 >> 8;
d[1] = x0 & 0xFF;
d[2] = x1 >> 8;
d[3] = x1 & 0xFF;
lcdCommandData(0x2A, d, 4);
d[0] = y0 >> 8;
d[1] = y0 & 0xFF;
d[2] = y1 >> 8;
d[3] = y1 & 0xFF;
lcdCommandData(0x2B, d, 4);
lcdCommand(0x2C);
}
// ============================================================
// BASIC DRAWING
// ============================================================
void drawPixel(int x, int y, uint16_t color)
{
if (x < 0 || x >= 320 || y < 0 || y >= 240) return;
lcdWindow(x, y, x, y);
digitalWrite(LCD_DC, HIGH);
digitalWrite(LCD_CS, LOW);
lcdSPI.transfer(color >> 8);
lcdSPI.transfer(color & 0xFF);
digitalWrite(LCD_CS, HIGH);
}
void fillRect(int x, int y, int w, int h, uint16_t color)
{
if (w <= 0 || h <= 0) return;
if (x < 0) { w += x; x = 0; }
if (y < 0) { h += y; y = 0; }
if (x >= 320 || y >= 240) return;
if (x + w > 320) w = 320 - x;
if (y + h > 240) h = 240 - y;
if (w <= 0 || h <= 0) return;
lcdWindow(x, y, x + w - 1, y + h - 1);
uint8_t hi = color >> 8;
uint8_t lo = color & 0xFF;
digitalWrite(LCD_DC, HIGH);
digitalWrite(LCD_CS, LOW);
int32_t pixels = (int32_t)w * h;
while (pixels--) {
lcdSPI.transfer(hi);
lcdSPI.transfer(lo);
}
digitalWrite(LCD_CS, HIGH);
}
void fillScreen(uint16_t color)
{
fillRect(0, 0, 320, 240, color);
}
void drawLine(int x0, int y0, int x1, int y1, uint16_t color)
{
int dx = abs(x1 - x0);
int sx = (x0 < x1) ? 1 : -1;
int dy = -abs(y1 - y0);
int sy = (y0 < y1) ? 1 : -1;
int err = dx + dy;
while (true)
{
drawPixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x0 += sx;
}
if (e2 <= dx) {
err += dx;
y0 += sy;
}
}
}
void drawCircle(int cx, int cy, int r, uint16_t color)
{
int x = r;
int y = 0;
int err = 0;
while (x >= y)
{
drawPixel(cx+x, cy+y, color);
drawPixel(cx+y, cy+x, color);
drawPixel(cx-y, cy+x, color);
drawPixel(cx-x, cy+y, color);
drawPixel(cx-x, cy-y, color);
drawPixel(cx-y, cy-x, color);
drawPixel(cx+y, cy-x, color);
drawPixel(cx+x, cy-y, color);
y++;
if (err <= 0) err += 2*y + 1;
if (err > 0) {
x--;
err -= 2*x + 1;
}
}
}
void fillCircle(int cx, int cy, int r, uint16_t color)
{
for (int y = -r; y <= r; y++) {
int xx = (int)sqrt((float)(r*r - y*y));
fillRect(cx - xx, cy + y, xx * 2 + 1, 1, color);
}
}
void roundBox(int x, int y, int w, int h, uint16_t color)
{
fillRect(x + 4, y, w - 8, h, color);
fillRect(x, y + 4, w, h - 8, color);
fillCircle(x + 4, y + 4, 4, color);
fillCircle(x + w - 5, y + 4, 4, color);
fillCircle(x + 4, y + h - 5, 4, color);
fillCircle(x + w - 5, y + h - 5, 4, color);
}
// ============================================================
// 3x5 FONT
// ============================================================
const uint8_t* glyph(char c)
{
static const uint8_t blank[5] = {0,0,0,0,0};
static const uint8_t A[5] = {2,5,7,5,5};
static const uint8_t B[5] = {6,5,6,5,6};
static const uint8_t C[5] = {7,4,4,4,7};
static const uint8_t D[5] = {6,5,5,5,6};
static const uint8_t E[5] = {7,4,6,4,7};
static const uint8_t F[5] = {7,4,6,4,4};
static const uint8_t G[5] = {7,4,5,5,7};
static const uint8_t H[5] = {5,5,7,5,5};
static const uint8_t I[5] = {7,2,2,2,7};
static const uint8_t J[5] = {1,1,1,5,2};
static const uint8_t K[5] = {5,5,6,5,5};
static const uint8_t L[5] = {4,4,4,4,7};
static const uint8_t M[5] = {5,7,7,5,5};
static const uint8_t N[5] = {5,7,7,7,5};
static const uint8_t O[5] = {7,5,5,5,7};
static const uint8_t P[5] = {6,5,6,4,4};
static const uint8_t Q[5] = {7,5,5,7,1};
static const uint8_t R[5] = {6,5,6,5,5};
static const uint8_t S[5] = {7,4,7,1,7};
static const uint8_t T[5] = {7,2,2,2,2};
static const uint8_t U[5] = {5,5,5,5,7};
static const uint8_t V[5] = {5,5,5,5,2};
static const uint8_t W[5] = {5,5,7,7,5};
static const uint8_t X[5] = {5,5,2,5,5};
static const uint8_t Y[5] = {5,5,2,2,2};
static const uint8_t Z[5] = {7,1,2,4,7};
static const uint8_t n0[5] = {7,5,5,5,7};
static const uint8_t n1[5] = {2,6,2,2,7};
static const uint8_t n2[5] = {7,1,7,4,7};
static const uint8_t n3[5] = {7,1,7,1,7};
static const uint8_t n4[5] = {5,5,7,1,1};
static const uint8_t n5[5] = {7,4,7,1,7};
static const uint8_t n6[5] = {7,4,7,5,7};
static const uint8_t n7[5] = {7,1,1,1,1};
static const uint8_t n8[5] = {7,5,7,5,7};
static const uint8_t n9[5] = {7,5,7,1,7};
static const uint8_t colon[5] = {0,2,0,2,0};
static const uint8_t period[5] = {0,0,0,0,2};
static const uint8_t dash[5] = {0,0,7,0,0};
static const uint8_t mLow[5] = {0,7,7,5,5};
static const uint8_t sLow[5] = {0,3,6,1,6};
switch (c)
{
case 'A': return A;
case 'B': return B;
case 'C': return C;
case 'D': return D;
case 'E': return E;
case 'F': return F;
case 'G': return G;
case 'H': return H;
case 'I': return I;
case 'J': return J;
case 'K': return K;
case 'L': return L;
case 'M': return M;
case 'N': return N;
case 'O': return O;
case 'P': return P;
case 'Q': return Q;
case 'R': return R;
case 'S': return S;
case 'T': return T;
case 'U': return U;
case 'V': return V;
case 'W': return W;
case 'X': return X;
case 'Y': return Y;
case 'Z': return Z;
case '0': return n0;
case '1': return n1;
case '2': return n2;
case '3': return n3;
case '4': return n4;
case '5': return n5;
case '6': return n6;
case '7': return n7;
case '8': return n8;
case '9': return n9;
case ':': return colon;
case '.': return period;
case '-': return dash;
case 'm': return mLow;
case 's': return sLow;
}
return blank;
}
void drawChar(int x, int y, char c, uint16_t color, int scale)
{
const uint8_t *g = glyph(c);
for (int row = 0; row < 5; row++)
{
for (int col = 0; col < 3; col++)
{
if (g[row] & (1 << (2-col)))
{
fillRect(
x + col * scale,
y + row * scale,
scale,
scale,
color
);
}
}
}
}
void text(int x, int y, const char *s, uint16_t color, int scale)
{
while (*s)
{
if (*s == ' ')
{
x += 4 * scale;
}
else
{
drawChar(x, y, *s, color, scale);
x += 4 * scale;
}
s++;
}
}
int textWidth(const char *s, int scale)
{
return (int)strlen(s) * 4 * scale;
}
// ============================================================
// 5x7 TERMINAL FONT (CTF SCREEN ONLY)
// ============================================================
const uint8_t* ctfGlyph(char c)
{
static const uint8_t blank[7]={0,0,0,0,0,0,0};
static const uint8_t A[7]={14,17,17,31,17,17,17};
static const uint8_t B[7]={30,17,17,30,17,17,30};
static const uint8_t C[7]={14,17,16,16,16,17,14};
static const uint8_t D[7]={30,17,17,17,17,17,30};
static const uint8_t E[7]={31,16,16,30,16,16,31};
static const uint8_t F[7]={31,16,16,30,16,16,16};
static const uint8_t G[7]={14,17,16,23,17,17,14};
static const uint8_t H[7]={17,17,17,31,17,17,17};
static const uint8_t I[7]={31,4,4,4,4,4,31};
static const uint8_t J[7]={7,2,2,2,18,18,12};
static const uint8_t K[7]={17,18,20,24,20,18,17};
static const uint8_t L[7]={16,16,16,16,16,16,31};
static const uint8_t M[7]={17,27,21,21,17,17,17};
static const uint8_t N[7]={17,25,21,19,17,17,17};
static const uint8_t O[7]={14,17,17,17,17,17,14};
static const uint8_t P[7]={30,17,17,30,16,16,16};
static const uint8_t Q[7]={14,17,17,17,21,18,13};
static const uint8_t R[7]={30,17,17,30,20,18,17};
static const uint8_t S[7]={15,16,16,14,1,1,30};
static const uint8_t T[7]={31,4,4,4,4,4,4};
static const uint8_t U[7]={17,17,17,17,17,17,14};
static const uint8_t V[7]={17,17,17,17,17,10,4};
static const uint8_t W[7]={17,17,17,21,21,21,10};
static const uint8_t X[7]={17,17,10,4,10,17,17};
static const uint8_t Y[7]={17,17,10,4,4,4,4};
static const uint8_t Z[7]={31,1,2,4,8,16,31};
static const uint8_t n0[7]={14,17,19,21,25,17,14};
static const uint8_t n1[7]={4,12,4,4,4,4,14};
static const uint8_t n2[7]={14,17,1,2,4,8,31};
static const uint8_t n3[7]={30,1,1,14,1,1,30};
static const uint8_t n4[7]={2,6,10,18,31,2,2};
static const uint8_t n5[7]={31,16,16,30,1,1,30};
static const uint8_t n6[7]={14,16,16,30,17,17,14};
static const uint8_t n7[7]={31,1,2,4,8,8,8};
static const uint8_t n8[7]={14,17,17,14,17,17,14};
static const uint8_t n9[7]={14,17,17,15,1,1,14};
static const uint8_t colon[7]={0,4,4,0,4,4,0};
static const uint8_t dot[7]={0,0,0,0,0,4,4};
static const uint8_t dash[7]={0,0,0,31,0,0,0};
static const uint8_t slash[7]={1,2,2,4,8,8,16};
static const uint8_t plus[7]={0,4,4,31,4,4,0};
switch(c) {
case 'A': return A; case 'B': return B; case 'C': return C; case 'D': return D;
case 'E': return E; case 'F': return F; case 'G': return G; case 'H': return H;
case 'I': return I; case 'J': return J; case 'K': return K; case 'L': return L;
case 'M': return M; case 'N': return N; case 'O': return O; case 'P': return P;
case 'Q': return Q; case 'R': return R; case 'S': return S; case 'T': return T;
case 'U': return U; case 'V': return V; case 'W': return W; case 'X': return X;
case 'Y': return Y; case 'Z': return Z;
case '0': return n0; case '1': return n1; case '2': return n2; case '3': return n3;
case '4': return n4; case '5': return n5; case '6': return n6; case '7': return n7;
case '8': return n8; case '9': return n9;
case ':': return colon; case '.': return dot; case '-': return dash;
case '/': return slash; case '+': return plus;
}
return blank;
}
void ctfChar(int x, int y, char c, uint16_t color, int scale)
{
const uint8_t *g = ctfGlyph(c);
for (int row=0; row<7; row++) {
for (int col=0; col<5; col++) {
if (g[row] & (1 << (4-col)))
fillRect(x + col*scale, y + row*scale, scale, scale, color);
}
}
}
void ctfText(int x, int y, const char *s, uint16_t color, int scale)
{
while (*s) {
if (*s == ' ') x += 6*scale;
else {
ctfChar(x,y,*s,color,scale);
x += 6*scale;
}
s++;
}
}
int ctfTextWidth(const char *s, int scale)
{
return (int)strlen(s) * 6 * scale;
}
void ctfTextCentered(int cx, int y, const char *s, uint16_t color, int scale)
{
ctfText(cx - ctfTextWidth(s,scale)/2, y, s, color, scale);
}
// ============================================================
// WORLD MAP SILHOUETTE
// ============================================================
// Generated from the supplied reference silhouette and reduced to
// horizontal runs. It gives a much more recognizable world map
// than the earlier coarse polygons while remaining tiny in flash.
// Antarctica is intentionally absent.
#define MAP_X 10
#define MAP_Y 42
#define MAP_W 300
#define MAP_H 153
const uint16_t mapRowOffset[MAP_H + 1] = {
0,2,3,4,6,8,10,13,15,18,23,31,39,47,60,69,
75,82,88,95,100,106,112,119,128,134,139,144,148,152,156,160,
164,167,169,171,174,176,179,181,185,190,196,200,204,210,217,223,
228,234,238,243,247,250,253,257,262,266,272,277,283,289,296,301,
306,310,314,320,327,334,341,346,351,356,362,367,372,377,382,389,
397,404,408,410,412,415,419,424,428,432,436,440,444,448,453,459,
465,469,473,477,480,483,487,490,493,496,498,502,507,513,519,524,
529,533,537,541,545,549,553,557,561,565,569,572,575,578,581,584,
587,590,594,598,603,606,608,610,612,614,615,616,617,618,619,620,
621,622,623,624,625,626,627,628,629,630
};
const uint16_t mapRuns[] = {
90,95,109,109,87,128,83,129,78,92,98,128,78,88,93,127,79,86,94,126,
82,83,98,126,213,216,99,125,209,221,50,52,100,121,207,220,48,54,75,76,
78,81,100,121,201,220,48,55,57,61,74,85,100,121,191,194,200,226,231,235,
247,249,51,61,67,68,75,75,78,85,101,121,192,195,200,239,247,251,17,28,
51,61,67,69,81,86,101,120,157,164,192,195,201,262,11,29,36,46,52,54,
66,69,74,75,82,86,101,116,155,168,185,187,193,195,198,198,200,271,274,279,
9,49,55,60,65,74,82,87,97,111,154,171,179,189,193,195,198,284,9,52,
54,72,83,87,97,110,152,171,177,291,5,7,9,69,82,85,98,108,151,167,
174,287,289,292,4,67,80,86,98,105,150,168,173,285,292,292,7,66,80,84,
86,86,98,104,149,156,160,169,172,286,5,63,81,84,99,104,148,155,160,288,
1,61,75,75,83,84,99,103,147,155,159,289,0,59,74,78,100,102,145,154,
159,272,279,286,0,10,15,58,73,79,145,155,164,272,279,282,285,285,1,6,
20,57,72,79,83,84,145,145,147,155,163,263,268,272,279,281,2,4,21,57,
71,84,149,154,161,262,278,281,22,57,71,84,150,154,161,261,278,283,22,58,
72,84,151,153,159,261,278,284,22,61,71,84,158,261,279,284,22,64,69,85,
158,261,281,284,22,63,67,87,147,262,281,284,21,63,67,87,144,268,282,284,
21,64,67,86,144,269,283,284,22,85,142,269,284,284,22,77,141,270,22,74,
139,270,22,73,75,77,137,270,21,77,136,270,20,76,80,80,136,270,20,80,
137,270,19,73,75,77,138,150,152,270,18,71,75,76,138,145,148,150,154,269,
18,69,131,134,137,142,144,145,149,151,155,266,17,69,130,141,150,152,157,266,
16,68,130,140,152,154,158,266,15,65,130,139,153,155,158,163,165,257,260,265,
15,64,130,138,154,154,158,160,165,256,260,261,263,265,15,63,130,138,154,154,
159,161,165,256,263,266,15,62,130,137,160,161,165,255,264,267,15,61,131,136,
148,148,160,160,166,259,265,269,15,61,142,148,174,259,266,269,16,60,135,136,
139,148,175,259,266,268,16,58,132,148,175,259,267,267,17,57,131,148,174,259,
18,55,129,149,174,260,19,54,129,152,159,161,173,261,19,19,21,53,128,154,
159,168,172,262,19,19,22,46,50,53,128,263,19,19,22,39,44,45,52,53,
127,188,190,263,19,20,22,38,52,54,126,188,192,263,20,20,23,36,52,54,
125,172,175,188,193,263,20,20,23,36,52,54,124,172,176,189,198,263,20,21,
24,36,53,53,123,173,177,189,196,196,199,263,24,35,123,173,177,190,195,196,
207,262,25,35,122,174,178,191,194,197,209,261,26,35,122,174,178,198,210,260,
26,34,121,175,179,199,211,257,27,34,121,175,179,200,212,230,235,250,253,256,
27,34,43,45,121,176,180,199,216,229,236,250,254,255,27,35,42,45,121,176,
181,198,216,228,237,249,254,255,27,36,41,44,121,176,182,197,216,227,237,249,
254,254,29,44,121,177,182,196,216,225,238,249,30,43,121,178,183,195,216,224,
239,251,32,43,121,179,183,194,217,223,240,251,38,47,120,180,184,193,217,223,
240,240,242,252,39,47,120,180,184,190,218,223,243,253,41,47,120,181,184,188,
218,222,243,253,44,47,120,182,185,186,218,223,243,254,45,47,120,182,219,223,
243,244,246,254,46,47,59,60,63,63,121,182,219,223,243,244,247,253,46,47,
57,60,62,65,69,69,122,191,219,223,243,244,248,252,47,48,56,60,62,70,
123,191,220,222,243,244,249,251,48,49,55,71,124,191,220,221,55,74,124,191,
55,75,125,190,55,76,126,138,142,189,55,77,127,135,144,188,262,264,55,80,
129,131,145,188,241,242,261,264,54,81,148,187,242,243,259,264,54,82,148,186,
243,243,258,263,53,82,148,185,243,245,258,264,53,83,148,184,244,247,257,264,
52,82,148,183,245,247,255,264,51,84,148,183,245,248,256,263,51,87,148,182,
246,249,256,263,280,281,51,88,148,181,247,249,256,262,281,281,286,286,51,94,
149,180,247,250,257,261,281,281,285,289,51,96,150,180,248,251,281,291,50,97,
151,179,248,251,285,293,51,99,151,179,249,250,286,294,51,99,151,179,287,295,
52,99,152,179,286,295,53,99,152,179,288,291,294,296,53,98,152,179,295,297,
54,97,152,180,296,298,54,96,153,180,298,299,55,96,153,180,55,96,152,180,
279,282,290,290,56,95,152,180,190,190,278,282,290,290,57,95,151,180,190,190,
272,272,276,282,289,291,58,95,151,180,188,190,271,273,276,282,289,291,59,95,
151,179,186,190,270,282,288,292,61,95,150,178,185,190,270,284,288,292,62,95,
151,177,185,190,268,292,64,95,151,176,185,189,267,292,65,94,152,175,185,189,
266,293,65,94,152,174,185,188,264,293,64,94,153,174,185,188,261,294,64,93,
153,174,184,187,259,294,64,92,153,174,184,187,258,295,64,89,154,174,184,187,
258,297,64,87,154,174,184,187,258,297,64,86,154,172,185,185,258,297,64,87,
154,172,257,297,64,86,154,171,257,297,65,86,154,171,257,297,65,85,155,170,
257,296,65,85,156,169,257,296,65,85,157,168,257,296,65,84,157,167,257,295,
65,84,157,166,257,268,275,294,65,83,157,165,257,265,276,293,65,82,158,162,
257,261,276,277,280,293,65,78,257,257,279,291,65,79,280,290,65,80,280,289,
65,80,280,288,65,79,281,286,65,76,66,76,66,76,67,73,68,74,68,74,
68,74,68,73,68,73,68,75,68,75,69,75,70,74,70,74,71,74,73,74
};
void drawGrid()
{
// Intentionally blank. The final UI is cleaner without the old grid.
}
struct MapRun {
uint16_t x0;
uint16_t x1;
};
// Real land silhouette and international borders share the SAME equirectangular
// projection, so the borders line up with the coastlines pixel-for-pixel.
// Antarctica is intentionally excluded.
const MapRun realLandRuns[] = {
{81,97},{111,131},{73,105},{107,128},{136,139},{70,72},{74,129},{132,137},{69,136},{161,172},
{231,233},{62,66},{70,90},{92,133},{158,169},{227,233},{235,236},{62,67},{72,132},{160,167},
{233,237},{50,54},{76,85},{90,134},{161,164},{234,238},{49,54},{58,59},{65,68},{70,84},
{92,134},{200,206},{227,229},{232,242},{52,62},{64,69},{73,83},{94,95},{101,133},{196,200},
{222,244},{263,270},{46,49},{54,61},{66,68},{73,84},{103,134},{195,199},{221,241},{264,269},
{47,54},{65,75},{78,85},{102,133},{194,198},{216,238},{241,243},{246,248},{46,58},{60,62},
{64,69},{71,73},{75,87},{103,129},{193,197},{207,210},{217,257},{267,271},{45,52},{54,62},
{66,69},{75,89},{103,131},{192,195},{206,209},{212,257},{260,262},{264,274},{17,23},{51,63},
{69,72},{75,93},{104,132},{170,174},{194,197},{205,281},{15,31},{52,66},{70,74},{77,94},
{106,131},{165,177},{199,200},{205,282},{291,296},{11,65},{67,76},{78,82},{86,94},{106,128},
{164,179},{194,195},{199,204},{206,299},{0,2},{11,55},{58,64},{68,76},{78,81},{88,96},
{105,125},{162,183},{186,188},{190,299},{0,7},{13,82},{89,98},{105,122},{161,184},{186,209},
{211,299},{0,9},{13,80},{88,99},{105,121},{130,137},{160,178},{182,299},{0,8},{10,79},
{85,97},{105,119},{129,138},{160,168},{171,178},{182,299},{4,6},{11,76},{78,82},{85,95},
{106,116},{130,137},{159,167},{170,178},{180,299},{15,74},{77,79},{81,83},{89,96},{107,116},
{131,135},{157,298},{13,74},{108,115},{154,165},{167,299},{12,72},{85,90},{108,115},{154,164},
{168,294},{12,22},{24,71},{85,92},{110,114},{154,164},{167,279},{282,291},{13,21},{23,26},
{30,71},{85,92},{95,96},{154,165},{168,169},{171,278},{283,286},{15,23},{35,72},{84,98},
{145,147},{154,165},{169,268},{275,278},{282,284},{18,21},{36,72},{86,98},{145,146},{155,156},
{158,163},{168,267},{280,285},{39,74},{86,99},{144,147},{157,158},{160,163},{168,265},{279,285},
{15,18},{40,76},{86,99},{145,148},{157,163},{167,264},{279,285},{14,17},{40,81},{85,101},
{143,148},{156,157},{160,161},{166,262},{279,284},{41,81},{83,103},{141,149},{157,160},{162,266},
{279,282},{42,81},{84,103},{141,144},{146,151},{153,268},{279,282},{43,82},{84,103},{141,144},
{146,151},{153,268},{280,281},{43,103},{141,143},{145,266},{43,100},{102,103},{146,266},{268,269},
{45,95},{101,105},{148,266},{268,269},{45,91},{93,96},{100,105},{146,266},{46,96},{100,106},
{146,265},{267,268},{46,96},{103,105},{148,265},{267,269},{46,100},{148,179},{181,264},{46,94},
{96,99},{149,174},{176,263},{267,268},{46,93},{148,160},{163,173},{182,263},{267,270},{46,90},
{142,155},{159,161},{163,173},{184,262},{266,271},{46,91},{142,152},{159,163},{165,173},{177,179},
{184,258},{266,268},{46,91},{142,150},{156,157},{161,163},{166,257},{266,267},{46,88},{142,150},
{157,158},{163,249},{251,256},{266,267},{47,88},{142,149},{157,158},{163,165},{167,169},{171,248},
{250,251},{254,255},{266,267},{47,87},{142,150},{160,163},{167,248},{253,256},{265,267},{47,87},
{142,149},{158,163},{167,169},{172,251},{253,256},{263,267},{48,86},{145,148},{150,158},{168,169},
{172,251},{254,257},{263,267},{49,87},{149,158},{179,250},{255,257},{260,266},{49,86},{145,159},
{179,249},{254,257},{259,266},{51,84},{144,158},{179,249},{258,263},{52,83},{142,161},{168,169},
{179,250},{257,261},{52,83},{142,162},{166,170},{175,176},{178,251},{258,259},{53,54},{56,82},
{141,164},{166,251},{258,259},{53,54},{56,82},{141,251},{54,71},{73,76},{81,82},{141,190},
{192,251},{55,70},{81,83},{139,189},{192,251},{54,56},{58,69},{81,83},{139,190},{193,249},
{55,57},{59,69},{82,83},{137,178},{180,192},{194,249},{56,57},{59,69},{82,83},{137,178},
{180,192},{195,250},{56,58},{60,68},{137,179},{181,197},{205,248},{250,251},{58,59},{61,68},
{136,179},{181,198},{206,247},{249,251},{61,68},{80,83},{136,179},{182,198},{207,246},{249,250},
{62,68},{76,77},{79,80},{82,84},{135,180},{182,199},{208,244},{62,69},{75,77},{85,87},
{135,180},{182,198},{208,222},{226,239},{20,21},{62,69},{75,77},{85,91},{136,180},{183,198},
{210,221},{227,238},{240,242},{63,70},{73,77},{88,93},{136,181},{184,198},{210,220},{227,237},
{240,241},{64,77},{84,86},{88,92},{136,182},{184,196},{211,219},{228,237},{240,241},{250,251},
{66,76},{136,182},{185,195},{211,218},{228,238},{250,251},{68,76},{136,182},{185,193},{211,218},
{228,239},{249,250},{73,80},{136,183},{185,193},{211,217},{228,229},{231,240},{249,250},{74,80},
{135,190},{212,216},{231,240},{249,252},{76,80},{136,184},{186,187},{212,216},{231,240},{250,253},
{77,80},{90,91},{136,187},{213,216},{231,240},{253,254},{78,80},{87,93},{136,186},{189,192},
{213,216},{231,232},{235,240},{251,254},{78,80},{87,94},{96,98},{137,192},{213,216},{231,232},
{235,239},{251,252},{78,81},{83,84},{87,99},{138,192},{213,216},{231,232},{237,238},{251,252},
{80,99},{138,191},{213,217},{231,232},{247,248},{252,254},{81,100},{139,191},{216,217},{231,233},
{251,254},{82,83},{85,101},{139,190},{216,218},{233,234},{246,247},{251,255},{85,105},{141,190},
{216,217},{233,234},{246,248},{253,254},{85,106},{142,148},{154,189},{229,230},{233,235},{245,249},
{85,107},{154,189},{230,231},{233,235},{244,248},{85,107},{158,188},{231,232},{234,235},{243,247},
{84,107},{158,188},{231,233},{241,248},{84,108},{157,187},{232,236},{240,250},{255,256},{83,107},
{157,186},{232,236},{240,253},{256,257},{83,111},{157,185},{233,235},{240,247},{251,252},{258,261},
{82,112},{157,185},{233,238},{240,247},{249,251},{258,261},{263,264},{82,113},{157,184},{234,238},
{241,246},{248,251},{259,261},{263,266},{83,118},{158,183},{234,238},{241,246},{248,251},{256,258},
{260,268},{82,118},{159,182},{235,238},{251,252},{260,270},{275,276},{82,120},{160,182},{236,237},
{249,251},{264,271},{273,276},{82,120},{160,182},{236,239},{249,250},{264,272},{274,275},{83,121},
{160,182},{237,243},{265,272},{84,120},{161,182},{239,245},{264,273},{84,120},{160,182},{244,251},
{253,255},{267,269},{272,273},{85,119},{161,182},{248,250},{252,253},{272,274},{85,119},{161,183},
{274,275},{85,118},{161,183},{259,263},{86,118},{161,183},{258,263},{267,269},{86,118},{160,183},
{189,191},{254,255},{257,263},{267,269},{86,117},{160,183},{189,191},{253,262},{267,270},{87,117},
{159,183},{188,191},{253,262},{267,270},{89,117},{159,183},{186,191},{252,263},{267,270},{90,117},
{159,181},{186,191},{251,271},{91,117},{159,180},{186,191},{251,271},{297,298},{91,116},{160,180},
{186,191},{251,271},{91,116},{160,178},{186,190},{248,273},{91,115},{161,178},{186,190},{247,273},
{286,287},{91,115},{161,179},{186,190},{244,274},{286,287},{91,115},{162,179},{185,190},{244,275},
{91,112},{162,179},{186,189},{244,276},{91,110},{162,177},{186,189},{244,276},{91,109},{162,176},
{244,277},{91,109},{162,177},{243,277},{91,109},{163,176},{244,277},{90,109},{163,176},{244,277},
{90,108},{164,175},{245,277},{90,107},{164,174},{245,277},{90,107},{165,174},{246,277},{90,106},
{165,173},{246,254},{261,276},{90,106},{164,173},{246,253},{261,275},{90,105},{165,172},{245,252},
{262,275},{90,105},{245,247},{262,275},{293,294},{89,102},{263,274},{89,102},{266,274},{294,295},
{88,102},{266,274},{295,298},{89,102},{269,272},{295,297},{89,99},{294,297},{89,98},{295,296},
{88,95},{97,98},{270,272},{292,296},{89,97},{270,273},{292,293},{89,96},{271,272},{291,293},
{89,95},{289,293},{88,95},{288,291},{87,93},{288,291},{87,95},{87,95},{88,93},{88,93},
{87,93},{88,92},{88,93},{89,92},{90,95},
};
const uint16_t realLandRowOffset[154] = {
0,2,5,8,11,17,21,26,34,42,50,58,68,78,86,95,
104,114,121,128,136,145,152,156,161,168,177,185,193,200,207,215,
221,227,233,236,240,244,248,252,256,259,264,269,275,282,288,294,
303,309,316,323,328,333,337,342,348,353,356,361,365,370,376,382,
388,394,400,407,414,423,431,440,446,452,459,464,470,476,484,492,
500,506,511,518,524,530,535,540,544,549,554,560,567,574,581,587,
593,599,603,607,613,618,621,624,628,634,639,644,649,653,658,662,
666,671,676,680,684,688,691,694,697,700,703,706,709,713,717,721,
725,727,730,733,736,738,740,744,747,750,752,754,756,757,758,759,
760,761,762,763,764,765,765,765,765,765,
};
const MapPoint realCountryBorders[] = {
{215,53},{213,50},{211,50},{212,49},{211,46},{216,45},{217,43},{216,41},
{218,41},{218,39},{221,39},{222,36},{225,38},{225,41},{228,41},{229,43},
{233,43},{236,45},{241,44},{242,41},{249,39},{249,38},{246,38},{246,35},
{239,36},{-32768,-32768},{92,108},{93,110},{92,112},{94,115},{91,121},{92,122},
{91,122},{91,126},{92,126},{90,129},{91,130},{90,140},{88,143},{90,146},
{-32768,-32768},{174,99},{173,99},{173,103},{174,103},{173,104},{170,101},{168,102},
{168,97},{164,98},{163,96},{160,96},{-32768,-32768},{201,62},{202,59},{200,58},
{201,57},{200,51},{203,51},{203,49},{206,50},{208,48},{209,50},{212,49},
{-32768,-32768},{160,35},{162,34},{168,37},{-32768,-32768},{161,39},{167,40},{168,42},
{173,42},{-32768,-32768},{169,78},{171,83},{175,85},{174,91},{179,91},{182,95},
{-32768,-32768},{224,66},{223,66},{223,61},{226,62},{225,64},{226,65},{226,64},
{226,67},{-32768,-32768},{182,70},{180,71},{180,76},{177,80},{179,85},{183,86},
{189,82},{188,80},{185,79},{185,77},{-32768,-32768},{179,58},{181,57},{180,55},
{182,55},{182,53},{180,55},{179,54},{-32768,-32768},{234,66},{234,67},{234,65},
{237,65},{237,64},{239,66},{-32768,-32768},{84,88},{87,89},{89,92},{91,92},
{91,94},{92,93},{91,89},{92,89},{-32768,-32768},{156,38},{156,36},{154,35},
{155,32},{-32768,-32768},{154,75},{162,76},{159,83},{157,82},{157,84},{-32768,-32768},
{170,55},{170,68},{168,68},{165,65},{163,64},{161,65},{157,62},{158,61},
{157,57},{159,54},{-32768,-32768},{222,36},{224,35},{231,36},{232,33},{234,35},
{239,35},{239,36},{-32768,-32768},{181,102},{179,102},{-32768,-32768},{179,102},{178,102},
{179,108},{177,105},{177,100},{179,102},{-32768,-32768},{93,101},{95,100},{95,102},
{99,104},{99,107},{101,107},{102,109},{101,111},{-32768,-32768},{211,50},{209,50},
{208,52},{209,53},{204,58},{200,57},{-32768,-32768},{166,40},{172,37},{174,41},
{-32768,-32768},{91,94},{89,95},{88,98},{89,100},{93,101},{-32768,-32768},{232,78},
{232,74},{231,74},{232,72},{230,69},{232,67},{233,68},{-32768,-32768},{215,53},
{216,54},{215,56},{217,57},{216,59},{222,61},{-32768,-32768},{73,74},{74,70},
{76,72},{-32768,-32768},{156,42},{155,40},{156,39},{158,40},{157,38},{-32768,-32768},
{228,58},{230,58},{229,58},{230,60},{228,61},{228,64},{226,66},{-32768,-32768},
{52,54},{57,55},{57,56},{61,55},{63,58},{65,57},{67,61},{69,61},
{-32768,-32768},{159,87},{161,87},{160,88},{162,89},{161,89},{162,91},{161,92},
{160,91},{159,94},{-32768,-32768},{187,47},{186,47},{188,51},{187,53},{189,54},
{189,57},{190,57},{-32768,-32768},{135,67},{135,66},{139,66},{140,61},{142,61},
{142,58},{146,57},{146,55},{149,55},{148,55},{148,52},{-32768,-32768},{100,84},
{100,88},{103,87},{101,85},{102,84},{-32768,-32768},{169,68},{169,73},{168,73},
{168,78},{169,78},{167,78},{167,80},{164,81},{-32768,-32768},{168,37},{170,35},
{169,35},{169,31},{166,31},{-32768,-32768},{235,70},{237,71},{236,72},{238,74},
{-32768,-32768},{142,44},{144,44},{143,49},{-32768,-32768},{158,38},{158,39},{161,37},
{160,35},{-32768,-32768},{230,59},{232,60},{231,64},{232,63},{232,66},{233,66},
{-32768,-32768},{80,73},{77,75},{-32768,-32768},{172,84},{169,84},{169,85},{169,84},
{168,85},{165,84},{165,86},{-32768,-32768},{165,87},{163,94},{159,95},{-32768,-32768},
{241,87},{242,89},{242,88},{245,88},{245,85},{247,85},{-32768,-32768},{222,61},
{223,59},{223,60},{-32768,-32768},{223,60},{225,61},{226,59},{223,60},{-32768,-32768},
{248,36},{250,34},{249,32},{254,32},{255,35},{-32768,-32768},{234,65},{237,68},
{236,68},{-32768,-32768},{211,55},{212,56},{210,59},{207,60},{209,63},{207,63},
{-32768,-32768},{46,37},{70,37},{71,36},{74,38},{-32768,-32768},{163,39},{166,37},
{169,38},{-32768,-32768},{167,45},{171,45},{171,44},{172,45},{171,46},{-32768,-32768},
{162,81},{161,83},{162,83},{163,88},{161,87},{-32768,-32768},{81,44},{80,44},
{81,45},{84,42},{91,41},{91,40},{-32768,-32768},{147,79},{145,79},{145,76},
{147,75},{-32768,-32768},{235,77},{235,74},{237,74},{-32768,-32768},{90,77},{89,77},
{89,82},{93,83},{-32768,-32768},{155,40},{155,39},{154,40},{155,38},{158,38},
{-32768,-32768},{185,49},{179,51},{-32768,-32768},{258,44},{257,43},{256,45},{255,44},
{253,46},{-32768,-32768},{261,37},{261,40},{258,41},{259,42},{258,44},{-32768,-32768},
{166,47},{167,46},{166,44},{-32768,-32768},{236,68},{238,72},{-32768,-32768},{92,101},
{92,109},{91,109},{-32768,-32768},{167,15},{161,18},{161,20},{159,21},{160,25},
{158,26},{-32768,-32768},{105,119},{102,121},{101,124},{-32768,-32768},{178,56},{178,58},
{-32768,-32768},{41,30},{42,29},{40,29},{37,25},{35,26},{34,24},{32,24},
{32,14},{-32768,-32768},{200,51},{197,48},{194,49},{-32768,-32768},{87,90},{84,93},
{84,95},{83,94},{-32768,-32768},{173,14},{171,14},{170,16},{167,15},{169,16},
{170,19},{-32768,-32768},{165,36},{164,37},{-32768,-32768},{103,87},{106,87},{107,85},
{-32768,-32768},{174,93},{175,94},{175,92},{174,93},{-32768,-32768},{103,113},{104,115},
{104,119},{103,119},{-32768,-32768},{94,114},{94,113},{96,114},{98,113},{98,115},
{100,116},{-32768,-32768},{255,35},{258,38},{261,38},{261,37},{-32768,-32768},{96,85},
{97,87},{95,89},{94,88},{-32768,-32768},{200,45},{198,43},{196,45},{-32768,-32768},
{170,109},{172,109},{175,105},{177,105},{-32768,-32768},{155,36},{154,35},{153,36},
{152,34},{-32768,-32768},{167,109},{159,108},{-32768,-32768},{233,68},{234,68},{233,69},
{234,71},{-32768,-32768},{238,77},{236,78},{-32768,-32768},{214,51},{214,52},{211,52},
{212,55},{-32768,-32768},{74,38},{76,37},{80,39},{81,43},{-32768,-32768},{105,126},
{105,124},{102,122},{-32768,-32768},{161,40},{161,39},{158,39},{-32768,-32768},{184,45},
{186,45},{187,47},{-32768,-32768},{174,15},{173,15},{175,19},{174,21},{176,22},
{-32768,-32768},{139,74},{138,72},{136,72},{-32768,-32768},{152,73},{153,73},{153,69},
{150,66},{-32768,-32768},{170,66},{177,66},{-32768,-32768},{238,72},{239,74},{-32768,-32768},
{139,75},{140,76},{136,76},{-32768,-32768},{169,45},{168,42},{-32768,-32768},{217,35},
{219,34},{222,37},{-32768,-32768},{171,117},{167,118},{-32768,-32768},{162,35},{161,31},
{-32768,-32768},{167,29},{172,29},{-32768,-32768},{167,29},{172,29},{-32768,-32768},{99,84},
{97,86},{96,85},{-32768,-32768},{199,33},{201,32},{200,31},{202,31},{-32768,-32768},
{157,57},{157,54},{156,54},{156,50},{157,50},{-32768,-32768},{187,47},{188,48},
{190,47},{189,47},{190,48},{-32768,-32768},{142,77},{143,77},{142,77},{143,80},
{-32768,-32768},{169,108},{168,108},{169,101},{-32768,-32768},{169,109},{167,109},{167,113},
{166,113},{167,118},{-32768,-32768},{174,94},{174,97},{175,97},{174,99},{-32768,-32768},
{233,66},{234,66},{233,68},{-32768,-32768},{161,37},{164,37},{164,38},{-32768,-32768},
{142,81},{143,85},{-32768,-32768},{267,92},{267,99},{-32768,-32768},{150,78},{150,82},
{-32768,-32768},{179,55},{178,56},{179,58},{-32768,-32768},{100,116},{102,117},{101,119},
{-32768,-32768},{170,108},{171,111},{173,112},{-32768,-32768},{143,30},{144,31},{-32768,-32768},
{81,79},{81,81},{-32768,-32768},{154,35},{152,34},{-32768,-32768},{163,81},{161,80},
{163,79},{-32768,-32768},{239,75},{238,77},{-32768,-32768},{152,76},{153,77},{152,81},
{-32768,-32768},{136,75},{138,75},{136,75},{-32768,-32768},{85,80},{85,82},{-32768,-32768},
{97,113},{99,110},{101,111},{101,113},{-32768,-32768},{234,71},{235,70},{-32768,-32768},
{166,120},{166,121},{163,120},{-32768,-32768},{145,73},{139,74},{-32768,-32768},{171,30},
{169,31},{-32768,-32768},{171,30},{169,31},{-32768,-32768},{205,49},{201,46},{-32768,-32768},
{205,49},{201,46},{-32768,-32768},{179,84},{175,86},{-32768,-32768},{192,34},{190,34},
{190,36},{189,35},{-32768,-32768},{138,80},{141,79},{141,81},{-32768,-32768},{171,116},
{174,113},{-32768,-32768},{188,48},{187,45},{-32768,-32768},{209,45},{208,44},{-32768,-32768},
{100,84},{99,84},{99,81},{-32768,-32768},{232,83},{233,82},{234,83},{-32768,-32768},
{208,46},{210,46},{209,45},{-32768,-32768},{179,35},{181,36},{-32768,-32768},{178,85},
{179,87},{178,88},{-32768,-32768},{145,73},{144,63},{146,63},{142,60},{-32768,-32768},
{104,87},{105,83},{-32768,-32768},{161,75},{162,70},{163,70},{162,65},{-32768,-32768},
{165,86},{163,87},{-32768,-32768},{78,78},{80,78},{-32768,-32768},{179,54},{179,55},
{-32768,-32768},{185,49},{186,50},{187,49},{-32768,-32768},{208,44},{209,43},{211,44},
{211,43},{-32768,-32768},{212,43},{216,44},{-32768,-32768},{208,46},{207,46},{208,47},
{-32768,-32768},{77,75},{75,74},{-32768,-32768},{172,122},{173,123},{174,122},{173,120},
{-32768,-32768},{143,80},{142,82},{142,81},{-32768,-32768},{195,65},{196,63},{-32768,-32768},
{198,34},{197,35},{195,34},{195,35},{-32768,-32768},{206,45},{207,46},{208,44},
{-32768,-32768},{163,79},{162,79},{162,76},{-32768,-32768},{93,83},{94,86},{93,86},
{-32768,-32768},{158,87},{159,88},{157,88},{-32768,-32768},{226,60},{228,58},{-32768,-32768},
{217,57},{219,57},{220,59},{223,59},{-32768,-32768},{174,39},{173,38},{-32768,-32768},
{176,33},{178,34},{-32768,-32768},{175,114},{176,114},{175,119},{177,118},{-32768,-32768},
{94,88},{94,87},{91,88},{-32768,-32768},{172,27},{170,27},{-32768,-32768},{142,81},
{141,80},{141,81},{-32768,-32768},{175,30},{173,29},{-32768,-32768},{175,30},{173,29},
{-32768,-32768},{188,44},{186,43},{-32768,-32768},{188,44},{186,43},{-32768,-32768},{188,44},
{186,43},{-32768,-32768},{150,78},{151,83},{-32768,-32768},{201,34},{201,35},{198,35},
{198,34},{-32768,-32768},{190,59},{186,58},{-32768,-32768},{176,112},{177,112},{177,109},
{-32768,-32768},{150,73},{151,77},{-32768,-32768},{175,106},{177,108},{-32768,-32768},{189,37},
{190,40},{-32768,-32768},{208,47},{211,47},{-32768,-32768},{196,45},{193,44},{-32768,-32768},
{238,74},{239,74},{-32768,-32768},{172,34},{174,34},{-32768,-32768},{149,43},{148,43},
{-32768,-32768},{104,117},{105,119},{-32768,-32768},{205,47},{206,49},{-32768,-32768},{181,36},
{183,36},{-32768,-32768},{186,43},{183,43},{-32768,-32768},{186,43},{183,43},{-32768,-32768},
{175,98},{177,100},{-32768,-32768},{187,45},{188,44},{-32768,-32768},{176,22},{173,24},
{-32768,-32768},{173,29},{172,27},{-32768,-32768},{101,119},{103,119},{-32768,-32768},{147,78},
{147,80},{-32768,-32768},{169,109},{170,109},{-32768,-32768},{173,44},{171,45},{-32768,-32768},
{207,46},{207,45},{208,46},{-32768,-32768},{75,73},{75,74},{-32768,-32768},{173,112},
{174,113},{-32768,-32768},{173,14},{174,15},{175,14},{-32768,-32768},{77,70},{75,70},
{-32768,-32768},{215,34},{217,35},{-32768,-32768},{188,44},{188,45},{190,44},{-32768,-32768},
{151,77},{150,78},{-32768,-32768},{182,53},{185,50},{-32768,-32768},{174,113},{176,114},
{176,112},{-32768,-32768},{182,55},{184,55},{186,58},{-32768,-32768},{137,78},{138,76},
{-32768,-32768},{90,68},{90,70},{-32768,-32768},{209,31},{211,31},{210,32},{-32768,-32768},
{185,78},{184,78},{185,76},{-32768,-32768},{205,47},{207,46},{-32768,-32768},{175,31},
{175,30},{-32768,-32768},{181,102},{183,101},{-32768,-32768},{92,39},{93,38},{93,40},
{-32768,-32768},{144,78},{143,79},{-32768,-32768},{173,120},{172,121},{-32768,-32768},{171,34},
{169,34},{-32768,-32768},{93,86},{94,88},{-32768,-32768},{141,80},{140,82},{-32768,-32768},
{236,78},{236,79},{-32768,-32768},{83,94},{83,93},{-32768,-32768},{244,85},{245,84},
{-32768,-32768},{151,44},{150,43},{-32768,-32768},{155,35},{154,36},{-32768,-32768},{204,31},
{207,30},{-32768,-32768},{171,34},{172,34},{-32768,-32768},{176,32},{176,33},{-32768,-32768},
{187,47},{188,48},{-32768,-32768},{103,113},{101,113},{-32768,-32768},{193,34},{192,34},
{-32768,-32768},{152,44},{151,44},{-32768,-32768},{147,80},{147,83},{-32768,-32768},{182,38},
{183,37},{-32768,-32768},{147,75},{149,73},{-32768,-32768},{189,35},{188,36},{189,37},
{-32768,-32768},{177,32},{176,32},{-32768,-32768},{246,36},{248,36},{-32768,-32768},{101,124},
{101,126},{-32768,-32768},{75,74},{75,75},{-32768,-32768},{182,38},{181,39},{-32768,-32768},
{172,29},{171,30},{-32768,-32768},{172,29},{171,30},{-32768,-32768},{182,37},{183,36},
{-32768,-32768},{141,77},{142,76},{-32768,-32768},{201,46},{200,45},{-32768,-32768},{160,94},
{160,96},{-32768,-32768},{91,40},{92,38},{92,39},{-32768,-32768},{212,31},{214,32},
{-32768,-32768},{93,146},{92,149},{-32768,-32768},{207,63},{206,64},{-32768,-32768},{244,65},
{245,66},{244,65},{-32768,-32768},{90,146},{93,146},{-32768,-32768},{172,37},{173,38},
{-32768,-32768},{189,57},{188,58},{-32768,-32768},{75,73},{76,73},{-32768,-32768},{178,34},
{179,35},{-32768,-32768},{156,67},{159,64},{-32768,-32768},{175,92},{175,91},{-32768,-32768},
{174,34},{175,34},{-32768,-32768},{140,76},{141,77},{-32768,-32768},{149,77},{147,78},
{-32768,-32768},{152,83},{152,81},{-32768,-32768},{195,35},{194,34},{-32768,-32768},{146,63},
{149,66},{-32768,-32768},{91,88},{92,89},{-32768,-32768},{139,74},{139,75},{-32768,-32768},
{177,32},{176,31},{-32768,-32768},{178,85},{179,84},{-32768,-32768},{201,43},{204,42},
{204,44},{-32768,-32768},{201,43},{204,42},{204,44},{-32768,-32768},{187,45},{186,45},
{-32768,-32768},{187,45},{186,45},{-32768,-32768},{166,116},{166,120},{-32768,-32768},{196,45},
{196,41},{-32768,-32768},{175,33},{175,34},{-32768,-32768},{168,30},{167,30},{-32768,-32768},
{174,91},{174,92},{-32768,-32768},{203,31},{204,31},{-32768,-32768},{203,31},{204,31},
{-32768,-32768},{245,84},{245,85},{245,84},{-32768,-32768},{231,79},{232,78},{-32768,-32768},
{151,77},{152,76},{-32768,-32768},{212,55},{211,55},{-32768,-32768},{211,43},{212,43},
{-32768,-32768},{177,108},{177,109},{-32768,-32768},{168,31},{168,30},{-32768,-32768},{173,26},
{172,27},{-32768,-32768},{208,30},{209,31},{-32768,-32768},{163,81},{164,81},{-32768,-32768},
{176,33},{175,33},{-32768,-32768},{179,65},{177,66},{-32768,-32768},{178,88},{178,89},
{-32768,-32768},{152,73},{150,73},{-32768,-32768},{152,77},{154,74},{-32768,-32768},{176,31},
{175,31},{-32768,-32768},{170,108},{167,109},{-32768,-32768},{208,46},{209,46},{-32768,-32768},
{178,89},{178,91},{-32768,-32768},{142,76},{142,77},{-32768,-32768},{153,69},{156,67},
{-32768,-32768},{196,41},{198,40},{199,41},{-32768,-32768},{99,81},{100,80},{-32768,-32768},
{193,69},{196,67},{196,65},{193,65},{192,63},{-32768,-32768},{145,78},{144,78},
{-32768,-32768},{207,30},{208,30},{-32768,-32768},{147,83},{147,84},{-32768,-32768},{173,29},
{172,29},{-32768,-32768},{173,29},{172,29},{-32768,-32768},{200,34},{199,33},{-32768,-32768},
{255,48},{256,48},{-32768,-32768},{210,32},{212,31},{-32768,-32768},{165,86},{165,87},
{-32768,-32768},{174,50},{174,51},{-32768,-32768},{139,60},{142,60},{-32768,-32768},{200,41},
{201,43},{-32768,-32768},{204,44},{205,45},{-32768,-32768},{214,32},{215,34},{-32768,-32768},
{94,40},{94,41},{-32768,-32768},{150,83},{150,82},{-32768,-32768},{196,61},{196,62},
{-32768,-32768},{196,61},{196,62},{-32768,-32768},{178,56},{178,55},{-32768,-32768},{162,114},
{162,115},{162,114},{-32768,-32768},{194,34},{193,34},{-32768,-32768},{205,45},{206,45},
{-32768,-32768},{156,30},{158,30},{-32768,-32768},{185,49},{185,50},{-32768,-32768},{172,121},
{172,122},{-32768,-32768},{182,37},{183,37},{-32768,-32768},{171,117},{171,116},{-32768,-32768},
{81,43},{81,44},{-32768,-32768},{200,34},{201,34},{-32768,-32768},{184,86},{184,91},
{-32768,-32768},{158,43},{157,43},{-32768,-32768},{162,76},{161,75},{-32768,-32768},{180,40},
{180,41},{-32768,-32768},{174,39},{175,39},{-32768,-32768},{202,31},{203,31},{-32768,-32768},
{184,85},{184,86},{-32768,-32768},{176,118},{176,117},{-32768,-32768},{253,47},{253,46},
{-32768,-32768},{171,46},{171,47},{-32768,-32768},{173,26},{173,25},{-32768,-32768},{255,49},
{255,48},{-32768,-32768},{239,74},{239,75},{-32768,-32768},{255,49},{254,49},{-32768,-32768},
{172,47},{172,48},{-32768,-32768},{158,44},{158,43},{-32768,-32768},{189,57},{190,57},
{-32768,-32768},{149,78},{149,77},{-32768,-32768},{253,48},{253,49},{-32768,-32768},{173,24},
{172,25},{-32768,-32768},{253,48},{253,49},{-32768,-32768},{165,44},{166,42},{165,41},
{166,41},{163,41},{165,44},{-32768,-32768},{168,44},{167,44},{-32768,-32768},{165,43},
{166,44},{-32768,-32768},{165,40},{166,41},{165,41},{-32768,-32768},{163,39},{161,40},
{-32768,-32768},{194,72},{193,69},{190,69},{189,71},{185,71},{185,72},{-32768,-32768},
{179,54},{180,53},{179,52},{-32768,-32768},{179,55},{179,56},{-32768,-32768},{185,76},
{181,73},{180,74},{-32768,-32768},{175,39},{173,40},{-32768,-32768},{170,80},{170,78},
{176,79},{177,76},{178,79},{-32768,-32768},
};
void drawRealLand()
{
for (int y = 0; y < MAP_H; y++)
{
uint16_t a = realLandRowOffset[y];
uint16_t b = realLandRowOffset[y + 1];
for (uint16_t i = a; i < b; i++)
{
int x0 = realLandRuns[i].x0;
int x1 = realLandRuns[i].x1;
fillRect(MAP_X + x0, MAP_Y + y, x1 - x0 + 1, 1, C_LAND_FILL);
}
}
}
void drawRealCountryBorders()
{
bool havePrev = false;
int px = 0, py = 0;
const int n = sizeof(realCountryBorders) / sizeof(realCountryBorders[0]);
for (int i = 0; i < n; i++)
{
int x = realCountryBorders[i].x;
int y = realCountryBorders[i].y;
if (x == -32768 && y == -32768)
{
havePrev = false;
continue;
}
x += MAP_X;
y += MAP_Y;
if (havePrev)
drawLine(px, py, x, y, C_COUNTRY_BORDER);
px = x;
py = y;
havePrev = true;
}
}
void drawWorldMap()
{
drawGrid();
drawRealLand();
drawRealCountryBorders();
}
// ============================================================
// UI
// ============================================================
void textCentered(int cx, int y, const char *s, uint16_t color, int scale)
{
text(cx - textWidth(s, scale) / 2, y, s, color, scale);
}
uint16_t regionDisplayColor(int idx)
{
if (alertCount > 0 && idx == alertRegion) return C_RED;
if (idx == latestRegion) return C_GREEN;
return C_BLUE;
}
void drawPingDotOnly()
{
// The ping dot belongs ONLY to the WORLD screen.
// Without this guard, the 500 ms ping blink writes into the
// top-left corner of the CTF screen and clips WATCH/ALERT.
if (currentScreen != SCREEN_WORLD) return;
// Only touch the tiny dot area. Keep it vertically centered
// with the 2x header font.
fillRect(4, 7, 10, 12, C_BLACK);
if (pingAlive)
{
if (pingBlinkState) fillCircle(9, 13, 2, C_GREEN);
}
else
{
fillCircle(9, 13, 2, C_RED);
}
}
void drawPing()
{
if (currentScreen != SCREEN_WORLD) return;
// Fixed-position header pieces so "MS" can never be clipped or
// overwritten by the centered clock.
fillRect(0, 0, 107, 30, C_BLACK);
uint16_t color = pingAlive ? C_GREEN : C_RED;
text(16, 8, "PING", color, 2);
char n[8];
if (pingAlive)
{
// Keep the field compact on the 320px header.
if (pingMs > 999)
snprintf(n, sizeof(n), "999");
else
snprintf(n, sizeof(n), "%d", pingMs);
}
else
{
strcpy(n, "---");
}
// Value and unit are rendered separately at fixed positions.
text(52, 8, n, color, 2);
text(82, 8, "ms", color, 2);
drawPingDotOnly();
}
void drawClock()
{
if (currentScreen != SCREEN_WORLD) return;
char t[16];
snprintf(t, sizeof(t), "%02d:%02d:%02d", clkH, clkM, clkS);
int w = textWidth(t, 2);
int x = (320 - w) / 2;
fillRect(108, 6, 104, 18, C_BLACK);
text(x, 8, t, clockStale ? C_RED : C_CLOCK, 2);
}
const char* const dowName[7] = {
"SUN","MON","TUE","WED","THU","FRI","SAT"
};
const char* const monName[12] = {
"JAN","FEB","MAR","APR","MAY","JUN",
"JUL","AUG","SEP","OCT","NOV","DEC"
};
void drawDate()
{
if (currentScreen != SCREEN_WORLD) return;
char s[20];
snprintf(s, sizeof(s), "%s %s %02d",
dowName[dateDow], monName[dateMon], dateDay);
// Same 2x font scale as the clock.
int w = textWidth(s, 2);
fillRect(214, 0, 106, 30, C_BLACK);
text(318 - w, 8, s, C_GREEN, 2);
}
void drawHeader()
{
fillRect(0, 0, 320, 30, C_BLACK);
drawPing();
drawClock();
drawDate();
}
void formatBytes(uint64_t bytes, char *out, size_t outLen)
{
const uint64_t KB = 1024ULL;
const uint64_t MB = 1024ULL * KB;
const uint64_t GB = 1024ULL * MB;
const uint64_t TB = 1024ULL * GB;
if (bytes < KB)
{
snprintf(out, outLen, "%llu B",
(unsigned long long)bytes);
}
else if (bytes < MB)
{
uint64_t whole = bytes / KB;
if (whole < 10 && (bytes % KB))
{
uint64_t tenth = ((bytes % KB) * 10ULL + KB / 2) / KB;
if (tenth == 10) { whole++; tenth = 0; }
snprintf(out, outLen, "%llu.%llu KB",
(unsigned long long)whole,
(unsigned long long)tenth);
}
else
{
snprintf(out, outLen, "%llu KB",
(unsigned long long)whole);
}
}
else if (bytes < GB)
{
uint64_t tenths = (bytes * 10ULL + MB / 2) / MB;
uint64_t whole = tenths / 10ULL;
uint64_t frac = tenths % 10ULL;
if (whole < 100)
snprintf(out, outLen, "%llu.%llu MB",
(unsigned long long)whole,
(unsigned long long)frac);
else
snprintf(out, outLen, "%llu MB",
(unsigned long long)((bytes + MB / 2) / MB));
}
else if (bytes < TB)
{
uint64_t hundredths = (bytes * 100ULL + GB / 2) / GB;
uint64_t whole = hundredths / 100ULL;
uint64_t frac = hundredths % 100ULL;
if (whole < 10)
snprintf(out, outLen, "%llu.%02llu GB",
(unsigned long long)whole,
(unsigned long long)frac);
else
{
uint64_t tenths = (bytes * 10ULL + GB / 2) / GB;
snprintf(out, outLen, "%llu.%llu GB",
(unsigned long long)(tenths / 10ULL),
(unsigned long long)(tenths % 10ULL));
}
}
else
{
uint64_t tenths = (bytes * 10ULL + TB / 2) / TB;
snprintf(out, outLen, "%llu.%llu TB",
(unsigned long long)(tenths / 10ULL),
(unsigned long long)(tenths % 10ULL));
}
}
void drawUpArrow(int x, int y, uint16_t color)
{
// Symmetric solid 8x10 pixel arrow, same visual height as scale-2 text.
fillRect(x + 3, y + 0, 2, 2, color);
fillRect(x + 2, y + 2, 4, 2, color);
fillRect(x + 1, y + 4, 6, 2, color);
fillRect(x + 3, y + 6, 2, 4, color);
}
void drawDownArrow(int x, int y, uint16_t color)
{
fillRect(x + 3, y + 0, 2, 4, color);
fillRect(x + 1, y + 4, 6, 2, color);
fillRect(x + 2, y + 6, 4, 2, color);
fillRect(x + 3, y + 8, 2, 2, color);
}
void drawTrafficField(int centerX, bool upload, uint64_t bytes)
{
char s[24];
formatBytes(bytes, s, sizeof(s));
const int arrowW = 8;
const int gap = 3;
const int scale = 2;
int totalW = arrowW + gap + textWidth(s, scale);
int x = centerX - totalW / 2;
int y = 221;
if (upload)
drawUpArrow(x, y, C_GREEN);
else
drawDownArrow(x, y, C_GREEN);
text(x + arrowW + gap, y, s, C_GREEN, scale);
}
void drawIpsField()
{
if (currentScreen != SCREEN_WORLD) return;
fillRect(0, 208, 76, 32, C_BLACK);
char s[16];
textCentered(38, 211, "IPS", C_GREEN, 1);
snprintf(s, sizeof(s), "%d", totalIPs);
textCentered(38, 222, s, C_GREEN, 2);
}
void drawUploadField()
{
if (currentScreen != SCREEN_WORLD) return;
fillRect(76, 208, 86, 32, C_BLACK);
drawTrafficField(119, true, uploadBytes);
}
void drawDownloadField()
{
if (currentScreen != SCREEN_WORLD) return;
fillRect(162, 208, 86, 32, C_BLACK);
drawTrafficField(205, false, downloadBytes);
}
void drawAlertField()
{
if (currentScreen != SCREEN_WORLD) return;
fillRect(248, 208, 72, 32, C_BLACK);
char s[16];
textCentered(284, 211, "ALERT", alertCount ? C_RED : C_GREEN, 1);
snprintf(s, sizeof(s), "%d", alertCount);
textCentered(284, 222, s, alertCount ? C_RED : C_GREEN, 2);
}
void drawFooter()
{
drawIpsField();
drawUploadField();
drawDownloadField();
drawAlertField();
}
int getRegionCount(int idx)
{
switch (idx)
{
case REG_US: return countUS;
case REG_EU: return countEU;
case REG_RU: return countRU;
case REG_CN: return countCN;
case REG_IL: return countIL;
case REG_JP: return countJP;
}
return 0;
}
void drawRegionBadge(int idx, bool pulsing)
{
(void)pulsing; // no radar/pulse rings on country markers
int mx = markerX[idx];
int my = markerY[idx];
uint16_t color = regionDisplayColor(idx);
int value = getRegionCount(idx);
char label[16];
snprintf(label, sizeof(label), "%s %d", regionLabel[idx], value);
// One small solid country dot only.
// Old IP = blue, latest = green, alert = red.
fillCircle(mx, my, 2, color);
// Plain country code/count beside the dot; no ring, badge, or radar target.
int tx = mx + 5;
int ty = my - 3;
int tw = textWidth(label, 1);
// Keep labels on-screen. Flip to the left near the right edge.
if (tx + tw > 318) tx = mx - 5 - tw;
if (tx < 2) tx = 2;
text(tx, ty, label, color, 1);
}
void drawAllRegionBadges()
{
for (int i = 0; i < REG_COUNT; i++)
{
drawRegionBadge(i, (i == latestRegion) ? pulseState : false);
}
}
int liveRegionFromLabel(const char *lab)
{
for (int i = 0; i < LREG_COUNT; i++)
if (!strcmp(lab, liveRegionLabel[i])) return i;
return -1;
}
uint16_t liveRegionColor(int idx)
{
if (idx == alertLiveRegion && alertCount > 0) return C_RED;
if (idx == latestLiveRegion) return C_GREEN;
return C_BLUE;
}
void eraseLiveRegion(int idx, uint16_t count)
{
if (currentScreen != SCREEN_WORLD) return;
if (idx < 0 || idx >= LREG_COUNT) return;
int x = liveRegionX[idx];
int y = liveRegionY[idx];
char label[18];
snprintf(label, sizeof(label), "%s %u",
liveRegionLabel[idx], (unsigned)count);
int tx = x + 7;
int ty = y - 5;
int tw = textWidth(label, 2);
if (tx + tw > 318) tx = x - 7 - tw;
if (tx < 2) tx = 2;
if (ty < 32) ty = 32;
if (ty > 196) ty = 196;
// Erase only the pixels occupied by the old marker/text.
// This prevents old blue glyph fragments surviving under a new green/red state.
fillCircle(x, y, 4, C_BLACK);
text(tx, ty, label, C_BLACK, 2);
}
void drawLiveRegion(int idx)
{
if (currentScreen != SCREEN_WORLD) return;
if (idx < 0 || idx >= LREG_COUNT) return;
int x = liveRegionX[idx];
int y = liveRegionY[idx];
uint16_t color = liveRegionColor(idx);
// Larger solid point: much easier to see on the 320x240 TFT.
fillCircle(x, y, 4, color);
char label[18];
snprintf(label, sizeof(label), "%s %u",
liveRegionLabel[idx], (unsigned)liveRegionCount[idx]);
// 2x font for region code + count.
// Labels now occupy more space, so keep them offset from the point.
int tx = x + 7;
int ty = y - 5;
int tw = textWidth(label, 2);
// Flip labels to the left only when needed near the right edge.
if (tx + tw > 318) tx = x - 7 - tw;
if (tx < 2) tx = 2;
// Keep labels out of header/footer.
if (ty < 32) ty = 32;
if (ty > 196) ty = 196;
text(tx, ty, label, color, 2);
}
void drawAllLiveRegions()
{
for (int i = 0; i < LREG_COUNT; i++)
if (liveRegionCount[i] > 0) drawLiveRegion(i);
}
void updateLiveRegion(const char *lab, int count)
{
int idx = liveRegionFromLabel(lab);
if (idx < 0 || count < 0) return;
uint16_t oldCount = liveRegionCount[idx];
eraseLiveRegion(idx, oldCount);
liveRegionCount[idx] = (uint16_t)count;
drawLiveRegion(idx);
}
void setLatestLiveRegion(const char *lab)
{
int idx = liveRegionFromLabel(lab);
if (idx < 0) return;
int old = latestLiveRegion;
// Erase the currently rendered state before changing color ownership.
if (old >= 0) eraseLiveRegion(old, liveRegionCount[old]);
if (idx >= 0 && idx != old) eraseLiveRegion(idx, liveRegionCount[idx]);
latestLiveRegion = idx;
if (old >= 0 && old != idx) drawLiveRegion(old);
drawLiveRegion(idx);
}
void setAlertLiveRegion(const char *lab)
{
int old = alertLiveRegion;
int incoming = (!strcmp(lab, "NONE")) ? -1 : liveRegionFromLabel(lab);
if (old >= 0) eraseLiveRegion(old, liveRegionCount[old]);
if (incoming >= 0 && incoming != old)
eraseLiveRegion(incoming, liveRegionCount[incoming]);
alertLiveRegion = incoming;
if (old >= 0 && old != alertLiveRegion) drawLiveRegion(old);
if (alertLiveRegion >= 0) drawLiveRegion(alertLiveRegion);
}
void drawFlagIcon(int x, int y, uint16_t color)
{
// Tiny CTF flag, sized for the TFT.
drawLine(x, y, x, y + 25, color);
fillRect(x + 2, y + 2, 16, 8, color);
drawLine(x, y + 25, x + 8, y + 25, color);
}
void drawCtfStatic()
{
fillScreen(C_BLACK);
uint16_t accent = (ctfLevel == 'A') ? C_RED : C_YELLOW;
// Header
ctfText(8, 7, (ctfLevel == 'A') ? "ALERT" : "WATCH", accent, 2);
char s[32];
snprintf(s, sizeof(s), "%02d:%02d:%02d", clkH, clkM, clkS);
ctfText(320 - ctfTextWidth(s,1) - 8, 10, s, C_GREEN, 1);
drawLine(8, 29, 311, 29, C_TEXTDIM);
// Target identity is immediately visible.
ctfText(10, 39, "TARGET", C_TEXTDIM, 1);
ctfText(78, 35, ctfCode, accent, 3);
snprintf(s, sizeof(s), "/%s", ctfRegion);
ctfText(166, 43, s, C_GREEN, 1);
ctfPhase = 0;
ctfAnimTick = millis();
}
void drawCtfPhase(int phase)
{
uint16_t accent = (ctfLevel == 'A') ? C_RED : C_YELLOW;
char s[40];
if (phase == 1) {
ctfText(10, 73, "DST", C_TEXTDIM, 1);
snprintf(s, sizeof(s), "%s:%d", ctfIp, ctfPort);
ctfText(58, 70, s, C_GREEN, 1);
}
else if (phase == 2) {
ctfText(10, 96, "PROC", C_TEXTDIM, 1);
ctfText(58, 93, ctfProc, C_GREEN, 1);
}
else if (phase == 3) {
ctfText(10, 119, "ASN", C_TEXTDIM, 1);
snprintf(s, sizeof(s), "AS%d", ctfAsn);
ctfText(58, 116, s, C_GREEN, 1);
ctfText(178, 119, "CONNS", C_TEXTDIM, 1);
snprintf(s, sizeof(s), "%d", ctfConns);
ctfText(242, 116, s, C_GREEN, 1);
// Rolling 30 s flow from Npcap/Scapy.
ctfText(10, 136, "FLOW", C_TEXTDIM, 1);
snprintf(s, sizeof(s), "TX%luK RX%luK", ctfTxKB, ctfRxKB);
ctfText(58, 133, s, C_GREEN, 1);
}
else if (phase == 4) {
drawLine(8, 153, 311, 153, C_TEXTDIM);
ctfText(10, 163, "WHY", C_TEXTDIM, 1);
int y = 160;
int x = 58;
if (ctfFlags & 1) {
ctfText(x, y, "GEO WATCH", C_YELLOW, 1);
y += 17;
}
if (ctfFlags & 2) {
ctfText(x, y, "ODD PORT", C_YELLOW, 1);
y += 17;
}
if (ctfFlags & 4) {
ctfText(x, y, "CONN BURST", C_YELLOW, 1);
y += 17;
}
if (ctfFlags & 8) {
ctfText(x, y, "THREAT INTEL", C_RED, 1);
y += 17;
}
if ((ctfFlags & 16) && y <= 201) {
ctfText(x, y, "BEACON", C_YELLOW, 1);
y += 17;
}
if ((ctfFlags & 64) && y <= 201) {
ctfText(x, y, "ODD PROTO", C_YELLOW, 1);
y += 17;
}
if ((ctfFlags & 128) && y <= 201) {
ctfText(x, y, "ASYM OUT", C_YELLOW, 1);
y += 17;
}
if ((ctfFlags & 256) && y <= 201) {
ctfText(x, y, "OUT XFER", C_RED, 1);
y += 17;
}
if (ctfFlags == 0)
ctfText(x, y, "POLICY MATCH", C_YELLOW, 1);
}
else if (phase == 5) {
drawLine(8, 215, 311, 215, C_TEXTDIM);
if (ctfLevel == 'A') drawCtfIntel();
ctfText(10, 220, (ctfLevel == 'A') ? "ESCALATE" : "INVESTIGATE", accent, 1);
}
}
void drawCtfIntel()
{
if (currentScreen != SCREEN_CTF || ctfLevel != 'A') return;
fillRect(8, 196, 304, 18, C_BLACK);
char s[40];
ctfText(10, 198, "OSINT", C_TEXTDIM, 1);
snprintf(s, sizeof(s), "%s P%d V%d",
ctfIntelTag, ctfIntelPorts, ctfIntelVulns);
ctfText(58, 198, s, C_RED, 1);
}
void drawCtfScreen()
{
drawCtfStatic();
}
void showWorldScreen()
{
currentScreen = SCREEN_WORLD;
renderHome();
}
void showCtfScreen()
{
currentScreen = SCREEN_CTF;
ctfShownAt = millis();
drawCtfScreen();
}
void renderHome()
{
fillScreen(C_BG);
drawHeader();
drawWorldMap();
drawAllLiveRegions();
drawFooter();
}
// ============================================================
// CLOCK
// ============================================================
void advanceDateOneDay()
{
static const uint8_t daysInMonth[12] = {
31,28,31,30,31,30,31,31,30,31,30,31
};
dateDow = (dateDow + 1) % 7;
dateDay++;
if (dateDay > daysInMonth[dateMon])
{
dateDay = 1;
dateMon = (dateMon + 1) % 12;
}
drawDate();
}
void tickClock()
{
clkS++;
if (clkS >= 60) {
clkS = 0;
clkM++;
}
if (clkM >= 60) {
clkM = 0;
clkH++;
}
if (clkH >= 24) {
clkH = 0;
advanceDateOneDay();
}
drawClock();
}
void setClock(int h, int m, int s)
{
if (h < 0 || h > 23) return;
if (m < 0 || m > 59) return;
if (s < 0 || s > 59) return;
clkH = h;
clkM = m;
clkS = s;
// Align the fractional display to .000 whenever TIME is synchronized.
lastClockTick = millis();
lastPcClockSync = millis();
clockStale = false;
drawClock();
}
int dowFromName(const char *s)
{
for (int i = 0; i < 7; i++)
if (!strcmp(s, dowName[i])) return i;
return -1;
}
int monFromName(const char *s)
{
for (int i = 0; i < 12; i++)
if (!strcmp(s, monName[i])) return i;
return -1;
}
void setDateText(const char *dow, const char *mon, int day)
{
int d = dowFromName(dow);
int m = monFromName(mon);
if (d < 0 || m < 0 || day < 1 || day > 31) return;
dateDow = d;
dateMon = m;
dateDay = day;
drawDate();
}
// ============================================================
// SERIAL COMMANDS
// ============================================================
// TIME 15:35:49
// DATE MON SEP 07
// PING 24
// PING DEAD
// DATA 14 8 3 6 2 2 35 142 716 1 (traffic values are KB)
// TRAFFIC 145408 733184 (traffic values are bytes)
// TRACE JP
// ALERT CN
// ALERT NONE
// ============================================================
int regionFromLabel(const char *lab)
{
if (!strcmp(lab, "US")) return REG_US;
if (!strcmp(lab, "EU")) return REG_EU;
if (!strcmp(lab, "RU")) return REG_RU;
if (!strcmp(lab, "CN")) return REG_CN;
if (!strcmp(lab, "IL")) return REG_IL;
if (!strcmp(lab, "JP")) return REG_JP;
return -1;
}
void setLatestRegion(int idx)
{
// Legacy six-region command retained for compatibility only.
// Never redraw the whole screen.
if (idx < 0 || idx >= REG_COUNT) return;
int old = latestRegion;
latestRegion = idx;
if (old >= 0) drawRegionBadge(old, false);
drawRegionBadge(latestRegion, false);
}
void setAlertRegion(int idx)
{
// Legacy command retained for compatibility only.
int old = alertRegion;
alertRegion = idx;
if (old >= 0) drawRegionBadge(old, false);
if (idx >= 0) drawRegionBadge(idx, false);
drawAlertField();
}
void handleLine(char *line)
{
while (*line == ' ') line++;
if (!strncmp(line, "TIME ", 5))
{
int h, m, s;
if (sscanf(line + 5, "%d:%d:%d", &h, &m, &s) == 3)
setClock(h, m, s);
}
else if (!strncmp(line, "DATE ", 5))
{
char dow[4] = {0};
char mon[4] = {0};
int day;
if (sscanf(line + 5, "%3s %3s %d", dow, mon, &day) == 3)
setDateText(dow, mon, day);
}
else if (!strncmp(line, "PING ", 5))
{
char arg[16] = {0};
if (sscanf(line + 5, "%15s", arg) == 1)
{
if (!strcmp(arg, "DEAD") || !strcmp(arg, "TIMEOUT"))
{
pingAlive = false;
}
else
{
int p = atoi(arg);
if (p >= 0)
{
pingMs = p;
pingAlive = true;
}
}
// Every PING command is treated as a fresh ping sample.
lastPingUpdateTick = millis();
pingBlinkState = true;
drawPing();
}
}
else if (!strncmp(line, "TRACE ", 6))
{
char lab[8] = {0};
if (sscanf(line + 6, "%7s", lab) == 1)
{
int idx = regionFromLabel(lab);
if (idx >= 0) setLatestRegion(idx);
}
}
else if (!strncmp(line, "ALERT ", 6))
{
char lab[8] = {0};
if (sscanf(line + 6, "%7s", lab) == 1)
{
if (!strcmp(lab, "NONE"))
setAlertRegion(-1);
else
{
int idx = regionFromLabel(lab);
if (idx >= 0) setAlertRegion(idx);
}
}
}
else if (!strncmp(line, "IPS ", 4))
{
int n;
if (sscanf(line + 4, "%d", &n) == 1 && n >= 0)
{
totalIPs = n;
drawIpsField();
}
}
else if (!strncmp(line, "ALERTS ", 7))
{
int n;
if (sscanf(line + 7, "%d", &n) == 1 && n >= 0)
{
alertCount = n;
drawAlertField();
if (alertLiveRegion >= 0) drawLiveRegion(alertLiveRegion);
}
}
else if (!strncmp(line, "REGION ", 7))
{
char lab[8] = {0};
int count;
if (sscanf(line + 7, "%7s %d", lab, &count) == 2)
updateLiveRegion(lab, count);
}
else if (!strncmp(line, "LATEST ", 7))
{
char lab[8] = {0};
if (sscanf(line + 7, "%7s", lab) == 1)
setLatestLiveRegion(lab);
}
else if (!strncmp(line, "ALERTNODE ", 10))
{
char lab[8] = {0};
if (sscanf(line + 10, "%7s", lab) == 1)
setAlertLiveRegion(lab);
}
else if (!strncmp(line, "SCREEN ", 7))
{
char which[12] = {0};
if (sscanf(line + 7, "%11s", which) == 1)
{
if (!strcmp(which, "WORLD")) showWorldScreen();
else if (!strcmp(which, "CTF")) showCtfScreen();
}
}
else if (!strncmp(line, "CTF ", 4))
{
char level[4] = {0};
char region[8] = {0};
char code[8] = {0};
char ip[20] = {0};
char proc[16] = {0};
int port = 0;
int conns = 0;
int asn = 0;
int flags = 0;
unsigned long txKB = 0;
unsigned long rxKB = 0;
// New format:
// CTF A RU RU 1.2.3.4 443 7 12345 145 CHROME 5120 82
// Older format without TX/RX is still accepted.
int got = sscanf(line + 4,
"%3s %7s %7s %19s %d %d %d %d %15s %lu %lu",
level, region, code, ip, &port, &conns, &asn,
&flags, proc, &txKB, &rxKB);
if (got >= 8)
{
char incoming = level[0];
// Red has priority over amber. Red may refresh/re-open red with new data.
if (currentScreen == SCREEN_CTF && ctfLevel == 'A' && incoming != 'A')
return;
ctfLevel = incoming;
strncpy(ctfRegion, region, sizeof(ctfRegion)-1);
strncpy(ctfCode, code, sizeof(ctfCode)-1);
strncpy(ctfIp, ip, sizeof(ctfIp)-1);
ctfPort = port;
ctfConns = conns;
ctfAsn = asn;
ctfFlags = flags;
if (got >= 10) ctfTxKB = txKB; else ctfTxKB = 0;
if (got >= 11) ctfRxKB = rxKB; else ctfRxKB = 0;
strncpy(ctfIntelTag, "LOOKUP", sizeof(ctfIntelTag)-1);
ctfIntelVulns = 0;
ctfIntelPorts = 0;
if (got >= 9)
strncpy(ctfProc, proc, sizeof(ctfProc)-1);
else
strncpy(ctfProc, "UNKNOWN", sizeof(ctfProc)-1);
ctfCount++;
showCtfScreen();
}
}
else if (!strncmp(line, "INTEL ", 6))
{
char tag[16] = {0};
int ports = 0;
int vulns = 0;
if (sscanf(line + 6, "%15s %d %d", tag, &ports, &vulns) == 3)
{
strncpy(ctfIntelTag, tag, sizeof(ctfIntelTag)-1);
ctfIntelPorts = ports;
ctfIntelVulns = vulns;
drawCtfIntel();
}
}
else if (!strncmp(line, "TRAFFIC ", 8))
{
unsigned long long up, dn;
if (sscanf(line + 8, "%llu %llu", &up, &dn) == 2)
{
uploadBytes = (uint64_t)up;
downloadBytes = (uint64_t)dn;
drawUploadField();
drawDownloadField();
}
}
else if (!strncmp(line, "DATA ", 5))
{
int us, eu, ru, cn, il, jp, ips, al;
unsigned long long upK, dnK;
if (sscanf(
line + 5,
"%d %d %d %d %d %d %d %llu %llu %d",
&us, &eu, &ru, &cn, &il, &jp,
&ips, &upK, &dnK, &al) == 10)
{
countUS = us;
countEU = eu;
countRU = ru;
countCN = cn;
countIL = il;
countJP = jp;
totalIPs = ips;
uploadBytes = (uint64_t)upK * 1024ULL;
downloadBytes = (uint64_t)dnK * 1024ULL;
alertCount = al;
drawIpsField();
drawUploadField();
drawDownloadField();
drawAlertField();
}
}
}
void processSerial()
{
while (Serial.available())
{
char c = (char)Serial.read();
if (c == '\r') continue;
if (c == '\n')
{
serialBuf[serialLen] = 0;
if (serialLen > 0)
{
handleLine(serialBuf);
}
serialLen = 0;
}
else
{
if (serialLen < (int)sizeof(serialBuf) - 1)
{
serialBuf[serialLen++] = c;
}
}
}
}
// ============================================================
// SETUP / LOOP
// ============================================================
void setup()
{
forceRgbLedOff();
Serial.begin(115200);
delay(300);
pinMode(LCD_CS, OUTPUT);
pinMode(LCD_DC, OUTPUT);
pinMode(LCD_RST, OUTPUT);
pinMode(LCD_BL, OUTPUT);
// Turn the controllable RGB LED on the back of the WROVER-KIT off.
// It is active LOW, so HIGH = off.
pinMode(BOARD_LED_R, OUTPUT);
pinMode(BOARD_LED_G, OUTPUT);
pinMode(BOARD_LED_B, OUTPUT);
digitalWrite(BOARD_LED_R, HIGH);
digitalWrite(BOARD_LED_G, HIGH);
digitalWrite(BOARD_LED_B, HIGH);
digitalWrite(LCD_CS, HIGH);
// backlight OFF during init
digitalWrite(LCD_BL, HIGH);
lcdSPI.begin(
LCD_CLK,
LCD_MISO,
LCD_MOSI,
LCD_CS
);
lcdSPI.beginTransaction(
SPISettings(
10000000,
MSBFIRST,
SPI_MODE0
)
);
lcdInit();
// backlight ON
digitalWrite(LCD_BL, LOW);
renderHome();
lastClockTick = millis();
lastPulseTick = millis();
lastPingBlinkTick = millis();
lastPingUpdateTick = millis();
Serial.println("READY");
Serial.println("Use commands:");
Serial.println("TIME 15:35:49 // authoritative PC clock");
Serial.println("DATE MON SEP 07");
Serial.println("PING 24");
Serial.println("PING DEAD");
Serial.println("Send a fresh PING sample every ~5 s; stale after 15 s turns red.");
Serial.println("LIVE NET / CTF protocol:");
Serial.println("IPS 12");
Serial.println("TRAFFIC 145408 733184");
Serial.println("REGION EU 3");
Serial.println("LATEST EU");
Serial.println("ALERTS 1");
Serial.println("ALERTNODE EU");
Serial.println("ALERTNODE NONE");
}
void loop()
{
forceRgbLedOff();
processSerial();
unsigned long now = millis();
// Clock is PC-synchronized. Do not free-run it here.
// Python sends an authoritative TIME command once per second.
// Blink the green ping dot continuously so the operator can see it is alive.
// If ping dies, drawPing() leaves a solid red dot instead.
if (now - lastPingBlinkTick >= PING_BLINK_MS)
{
lastPingBlinkTick = now;
pingBlinkState = !pingBlinkState;
drawPingDotOnly();
}
// The host should send PING <ms> about every 5 seconds.
// If no ping sample arrives for 15 seconds, show dead/red automatically.
if (pingAlive && (now - lastPingUpdateTick >= PING_STALE_MS))
{
pingAlive = false;
pingBlinkState = true;
drawPing();
}
// PC clock heartbeat timeout:
// freeze on the last received time and turn it red.
if (!clockStale && millis() - lastPcClockSync > 3000)
{
clockStale = true;
drawClock();
}
// CTF terminal reveal: adds one real-data line every 350 ms.
if (currentScreen == SCREEN_CTF && ctfPhase < 5 &&
now - ctfAnimTick >= 350)
{
ctfAnimTick = now;
ctfPhase++;
drawCtfPhase(ctfPhase);
}
if (currentScreen == SCREEN_CTF &&
millis() - ctfShownAt >= CTF_AUTO_RETURN_MS)
{
showWorldScreen();
}
}
#!/usr/bin/env python3
"""
WROVER LIVE NETWORK BRIDGE v14 - CLEAN REGION COLOR + ZOOMEYE
Real remote IPs are geolocated on the PC, then grouped into a small number
of fixed display regions so labels do not overlap on the 320x240 TFT.
TFT regions:
US = United States + Canada
EU = Europe
RU = Russia + nearby Eurasia
CN = China
IL = Israel + Middle East
JP = Japan + Korea
IN = India + South Asia
SEA = Southeast Asia
SA = South America
AF = Africa
AU = Australia/New Zealand/Oceania
Install:
py -m pip install pyserial psutil requests
Run:
py wrover_live_net_v3_regions.py
or:
py wrover_live_net_v3_regions.py --port COM7
"""
from __future__ import annotations
import argparse, datetime as dt, ipaddress, json, platform, re, subprocess, time, os, statistics, threading
from collections import defaultdict, deque
from pathlib import Path
from typing import Dict, Optional, Set
import psutil, requests, serial
from serial.tools import list_ports
BAUD = 115200
PING_HOST = "1.1.1.1"
PING_UPDATE_S = 5.0
SOCKET_POLL_S = 1.0
TRAFFIC_UPDATE_S = 1.0
GEO_TIMEOUT_S = 3.0
CACHE_PATH = Path.home() / ".wrover_geo_cache.json"
# CTF policy: geography is a WATCH signal, never sufficient by itself for ALERT.
WATCHLIST_COUNTRIES = {"CN","RU","IR","KP","SY","BY"}
# Common destination ports that normally should not trigger a CTF event by themselves.
COMMON_REMOTE_PORTS = {
20,21,22,25,53,80,110,123,143,443,465,587,993,995,
853,3389,5222,5223,8080,8443
}
# Current simultaneous sockets to one remote IP.
BURST_CONNECTIONS = 6
# Optional real threat-intelligence enrichment.
# Set in Windows before running:
# set ABUSEIPDB_KEY=your_key_here
ABUSEIPDB_KEY = os.getenv("ABUSEIPDB_KEY","").strip()
ABUSE_ALERT_SCORE = 50
SHODAN_API_KEY = os.getenv("SHODAN_API_KEY","").strip()
CENSYS_PAT = os.getenv("CENSYS_PAT","").strip()
CENSYS_ORG_ID = os.getenv("CENSYS_ORG_ID","").strip()
ZOOMEYE_API_KEY = os.getenv("ZOOMEYE_API_KEY","").strip()
BEACON_MIN_EVENTS = 5
BEACON_MIN_INTERVAL = 5.0
BEACON_MAX_INTERVAL = 900.0
BEACON_CV_MAX = 0.18
FLAG_GEO = 1
FLAG_ODDPORT = 2
FLAG_BURST = 4
FLAG_TI = 8
FLAG_BEACON = 16
FLAG_EXPOSED = 32
FLAG_ODDPROTO = 64
FLAG_ASYMOUT = 128
FLAG_OUTXFER = 256
FLOW_WINDOW_S = 30.0
ASYM_MIN_OUT_BYTES = 512 * 1024
ASYM_RATIO = 8.0
OUT_XFER_BYTES = 5 * 1024 * 1024
ENABLE_NPCAP = True
EU = {
"AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT",
"LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE","IS","NO","CH",
"GB","AL","AD","BA","BY","FO","GI","GG","IM","JE","LI","MC","MD","ME","MK",
"RS","SM","UA","VA"
}
RU_NEAR = {"RU","KZ","KG","TJ","TM","UZ","AM","AZ","GE"}
MIDDLE_EAST = {"IL","AE","BH","EG","IQ","IR","JO","KW","LB","OM","PS","QA","SA","SY","TR","YE"}
JP_KR = {"JP","KR","KP"}
SOUTH_ASIA = {"IN","PK","BD","LK","NP","BT","MV","AF"}
SEA = {"BN","KH","ID","LA","MY","MM","PH","SG","TH","TL","VN"}
SOUTH_AMERICA = {"AR","BO","BR","CL","CO","EC","FK","GF","GY","PE","PY","SR","UY","VE"}
AFRICA = {
"DZ","AO","BJ","BW","BF","BI","CV","CM","CF","TD","KM","CD","CG","CI","DJ","GQ",
"ER","SZ","ET","GA","GM","GH","GN","GW","KE","LS","LR","LY","MG","MW","ML","MR",
"MU","MA","MZ","NA","NE","NG","RW","ST","SN","SC","SL","SO","ZA","SS","SD","TZ",
"TG","TN","UG","EH","ZM","ZW"
}
OCEANIA = {"AU","NZ","FJ","PG","SB","VU","NC","PF","WS","TO","KI","FM","MH","PW","NR","TV"}
def region_for(cc: str) -> str:
cc = cc.upper()
if cc in {"US","CA"}: return "US"
if cc in EU: return "EU"
if cc in RU_NEAR: return "RU"
if cc == "CN": return "CN"
if cc in MIDDLE_EAST: return "IL"
if cc in JP_KR: return "JP"
if cc in SOUTH_ASIA: return "IN"
if cc in SEA: return "SEA"
if cc in SOUTH_AMERICA: return "SA"
if cc in AFRICA: return "AF"
if cc in OCEANIA: return "AU"
# Any unclassified Central America/Caribbean country goes with US/NA.
if cc in {"MX","BZ","CR","SV","GT","HN","NI","PA","BS","BB","CU","DO","HT","JM","TT","PR"}:
return "US"
return "EU" # fallback keeps obscure/unclassified destinations visible
def find_serial_port():
ports = list(list_ports.comports())
preferred = ("cp210","silicon labs","usb serial","uart","ch340","wch","esp32","wrover")
for p in ports:
d = f"{p.description} {p.manufacturer or ''}".lower()
if any(x in d for x in preferred):
return p.device
return ports[0].device if len(ports) == 1 else None
class Esp:
def __init__(self, port, baud):
self.ser = serial.Serial(port, baudrate=baud, timeout=0.05)
self.lock = threading.Lock()
time.sleep(2)
def send(self, s):
with self.lock:
self.ser.write((s+"\n").encode("ascii","replace"))
self.ser.flush()
print("[TX ]", s)
def close(self):
try:
self.ser.close()
except Exception:
pass
def public_ip(s):
try:
a=ipaddress.ip_address(s)
return not (a.is_private or a.is_loopback or a.is_link_local or
a.is_multicast or a.is_unspecified or a.is_reserved)
except ValueError:
return False
def current_remote_ips():
"""
Return one record per public remote IP:
ports: remote ports currently in use
pids: process IDs using it
conns: current socket count
"""
out = defaultdict(lambda: {"ports": set(), "pids": set(), "conns": 0})
try:
conns = psutil.net_connections(kind="inet")
except Exception as e:
print("[WARN] sockets:", e)
return out
for c in conns:
if not c.raddr:
continue
try:
ip = c.raddr.ip
port = int(c.raddr.port)
except AttributeError:
ip = c.raddr[0]
port = int(c.raddr[1]) if len(c.raddr) > 1 else 0
if not public_ip(ip):
continue
rec = out[ip]
rec["ports"].add(port)
if c.pid:
rec["pids"].add(int(c.pid))
rec["conns"] += 1
return out
def process_name(pids):
for pid in sorted(pids):
try:
name = psutil.Process(pid).name()
if name:
# Serial protocol uses a single token.
return re.sub(r"[^A-Za-z0-9_.-]", "_", name.upper())[:15]
except Exception:
pass
return "UNKNOWN"
def ping_ms(host):
win=platform.system().lower()=="windows"
cmd=["ping","-n","1","-w","1200",host] if win else ["ping","-c","1","-W","1",host]
try:
kw=dict(capture_output=True,text=True,timeout=3)
if win: kw["creationflags"]=subprocess.CREATE_NO_WINDOW
p=subprocess.run(cmd,**kw)
txt=(p.stdout or "")+"\n"+(p.stderr or "")
m=re.search(r"time[=<]\s*([\d.]+)\s*ms",txt,re.I)
return max(1,round(float(m.group(1)))) if m else None
except Exception:
return None
def load_cache():
try:
return json.loads(CACHE_PATH.read_text("utf-8")) if CACHE_PATH.exists() else {}
except Exception:
return {}
def save_cache(c):
try: CACHE_PATH.write_text(json.dumps(c),encoding="utf-8")
except Exception: pass
def geo(ip, cache):
if ip in cache:
d=cache[ip]
if d.get("cc"): return d
try:
r=requests.get(f"https://ipwho.is/{ip}",timeout=GEO_TIMEOUT_S)
r.raise_for_status()
j=r.json()
if not j.get("success",True): return None
cc=(j.get("country_code") or "").upper()
if len(cc)!=2: return None
conn=j.get("connection") or {}
asn_raw = conn.get("asn") or 0
try:
asn = int(str(asn_raw).upper().replace("AS",""))
except Exception:
asn = 0
d={"cc":cc,"country":j.get("country") or "","city":j.get("city") or "",
"org":conn.get("org") or "", "asn":asn}
cache[ip]=d; save_cache(cache)
return d
except Exception as e:
print("[WARN] GEO",ip,e)
return None
_abuse_cache = {}
def abuse_score(ip):
"""Return AbuseIPDB confidence score, or -1 when not configured/unavailable."""
if not ABUSEIPDB_KEY:
return -1
if ip in _abuse_cache:
return _abuse_cache[ip]
try:
r = requests.get(
"https://api.abuseipdb.com/api/v2/check",
headers={"Key": ABUSEIPDB_KEY, "Accept": "application/json"},
params={"ipAddress": ip, "maxAgeInDays": 90},
timeout=3.0,
)
r.raise_for_status()
score = int(r.json().get("data", {}).get("abuseConfidenceScore", 0))
_abuse_cache[ip] = score
return score
except Exception as e:
print("[WARN] AbuseIPDB", ip, e)
return -1
def signal_flags(cc, remote_port, conns, ti_score, beacon=False, exposed=False,
odd_proto=False, asym_out=False, out_xfer=False):
flags = 0
if cc in WATCHLIST_COUNTRIES:
flags |= FLAG_GEO
if remote_port and remote_port not in COMMON_REMOTE_PORTS:
flags |= FLAG_ODDPORT
if conns >= BURST_CONNECTIONS:
flags |= FLAG_BURST
if ti_score >= ABUSE_ALERT_SCORE:
flags |= FLAG_TI
if beacon:
flags |= FLAG_BEACON
if exposed:
flags |= FLAG_EXPOSED
if odd_proto:
flags |= FLAG_ODDPROTO
if asym_out:
flags |= FLAG_ASYMOUT
if out_xfer:
flags |= FLAG_OUTXFER
return flags
def is_alert(flags):
if flags & FLAG_TI:
return True
if (flags & FLAG_OUTXFER) and (flags & FLAG_ASYMOUT):
return True
if (flags & FLAG_BEACON) and (flags & ~FLAG_BEACON):
return True
relevant = flags & (
FLAG_GEO | FLAG_ODDPORT | FLAG_BURST | FLAG_EXPOSED |
FLAG_ODDPROTO | FLAG_ASYMOUT | FLAG_OUTXFER
)
return bool(relevant and (relevant & (relevant - 1)))
_internetdb_cache = {}
_shodan_cache = {}
_censys_cache = {}
_zoomeye_cache = {}
def internetdb_lookup(ip):
if ip in _internetdb_cache:
return _internetdb_cache[ip]
try:
r = requests.get(f"https://internetdb.shodan.io/{ip}", timeout=3.0)
d = {} if r.status_code == 404 else r.json()
_internetdb_cache[ip] = d
return d
except Exception as e:
print("[WARN] InternetDB", ip, e)
return {}
def shodan_host_lookup(ip):
if not SHODAN_API_KEY:
return {}
if ip in _shodan_cache:
return _shodan_cache[ip]
try:
r = requests.get(
f"https://api.shodan.io/shodan/host/{ip}",
params={"key": SHODAN_API_KEY, "minify": "true"},
timeout=4.0,
)
d = {} if r.status_code == 404 else r.json()
_shodan_cache[ip] = d
return d
except Exception as e:
print("[WARN] Shodan", ip, e)
return {}
def censys_host_lookup(ip):
"""Optional passive Censys v3 host lookup using a Personal Access Token."""
if not CENSYS_PAT:
return {}
if ip in _censys_cache:
return _censys_cache[ip]
try:
headers = {
"Authorization": f"Bearer {CENSYS_PAT}",
"Accept": "application/vnd.censys.api.v3.host.v1+json",
}
if CENSYS_ORG_ID:
headers["X-Organization-ID"] = CENSYS_ORG_ID
r = requests.get(
f"https://api.platform.censys.io/v3/global/asset/host/{ip}",
headers=headers,
timeout=4.0,
)
if r.status_code == 404:
d = {}
else:
r.raise_for_status()
d = r.json()
_censys_cache[ip] = d
return d
except Exception as e:
print("[WARN] Censys", ip, e)
return {}
def zoomeye_host_lookup(ip):
"""
Optional passive ZoomEye host search.
Uses the current API-KEY header style and searches the observed public IP.
No active probing is performed by this script.
"""
if not ZOOMEYE_API_KEY:
return {}
if ip in _zoomeye_cache:
return _zoomeye_cache[ip]
try:
r = requests.get(
"https://api.zoomeye.org/host/search",
headers={
"API-KEY": ZOOMEYE_API_KEY,
"Accept": "application/json",
},
params={
"query": f"ip:{ip}",
"page": 1,
},
timeout=4.0,
)
if r.status_code == 404:
d = {}
else:
r.raise_for_status()
d = r.json()
_zoomeye_cache[ip] = d
return d
except Exception as e:
print("[WARN] ZoomEye", ip, e)
return {}
def _zoomeye_ports_and_apps(data):
"""Best-effort parser across ZoomEye host-search response variants."""
ports = set()
apps = set()
matches = data.get("matches") or data.get("data") or []
if isinstance(matches, dict):
matches = matches.get("matches") or matches.get("list") or []
if not isinstance(matches, list):
return ports, apps
for m in matches:
if not isinstance(m, dict):
continue
# Common older/current host-search shapes.
portinfo = m.get("portinfo") or {}
if isinstance(portinfo, dict):
p = portinfo.get("port")
try:
if p is not None:
ports.add(int(p))
except Exception:
pass
for key in ("app", "service", "product"):
val = portinfo.get(key)
if val:
apps.add(str(val))
for key in ("port", "port_number"):
try:
if m.get(key) is not None:
ports.add(int(m.get(key)))
except Exception:
pass
for key in ("app", "service", "product"):
val = m.get(key)
if val:
apps.add(str(val))
return ports, apps
def passive_intel_summary(ip):
"""
Merge passive public-internet observations from:
- Shodan InternetDB (free)
- optional Shodan host API
- optional Censys
- optional ZoomEye
Returns:
tag, unique_port_count, vulnerability_count, exposed_flag
"""
db = internetdb_lookup(ip)
ports = set(db.get("ports") or [])
vulns = set(db.get("vulns") or [])
tags = [str(x).upper() for x in (db.get("tags") or [])]
tag = "SHODAN"
exposed = False
interesting = (
"C2", "MALWARE", "BOTNET", "VPN", "PROXY",
"TOR", "HONEYPOT", "COMPROMISED"
)
for t in tags:
if any(k in t for k in interesting):
tag = re.sub(r"[^A-Z0-9_-]", "", t)[:15] or "TAG"
exposed = True
break
if vulns:
exposed = True
if tag == "SHODAN":
tag = "VULNS"
# Rich Shodan can add ports.
sh = shodan_host_lookup(ip)
if sh:
for p in (sh.get("ports") or []):
try:
ports.add(int(p))
except Exception:
pass
# Censys currently acts as an additional independent passive source.
ce = censys_host_lookup(ip)
if ce and tag == "SHODAN":
tag = "CENSYS"
# ZoomEye: add observed services/ports and mark when it contributes.
zm = zoomeye_host_lookup(ip)
if zm:
zm_ports, zm_apps = _zoomeye_ports_and_apps(zm)
before = len(ports)
ports.update(zm_ports)
# Only replace the generic source tag if ZoomEye actually adds information.
if zm_apps or len(ports) > before:
if tag in {"SHODAN", "CENSYS"}:
tag = "ZOOMEYE"
exposed = exposed or bool(zm_ports or zm_apps)
# Console-only detail retains app names without overcrowding the TFT.
if zm_apps:
app_preview = ", ".join(sorted(zm_apps)[:4])
print(f"[OSINT] ZoomEye {ip}: {len(zm_ports)} ports | {app_preview}")
else:
print(f"[OSINT] ZoomEye {ip}: {len(zm_ports)} ports")
return tag, len(ports), len(vulns), exposed
def beacon_stats(history):
if len(history) < BEACON_MIN_EVENTS:
return False, 0.0, 999.0
xs = list(history)
intervals = [xs[i] - xs[i-1] for i in range(1, len(xs))]
intervals = [x for x in intervals if BEACON_MIN_INTERVAL <= x <= BEACON_MAX_INTERVAL]
if len(intervals) < BEACON_MIN_EVENTS - 1:
return False, 0.0, 999.0
mean = statistics.mean(intervals)
if mean <= 0:
return False, 0.0, 999.0
cv = statistics.pstdev(intervals) / mean
return cv <= BEACON_CV_MAX, mean, cv
class PacketTelemetry:
def __init__(self):
self.available = False
self.error = ""
self.lock = threading.Lock()
self.events = defaultdict(lambda: deque())
self.protocols = defaultdict(set)
self.packet_times = defaultdict(lambda: deque(maxlen=20))
self.local_ips = self._local_ips()
try:
from scapy.all import sniff, IP, IPv6, TCP, UDP, ICMP
self.sniff = sniff
self.IP = IP
self.IPv6 = IPv6
self.TCP = TCP
self.UDP = UDP
self.ICMP = ICMP
self.available = True
except Exception as e:
self.error = str(e)
def _local_ips(self):
ips = set()
for _, addrs in psutil.net_if_addrs().items():
for a in addrs:
addr = getattr(a, "address", "")
if not addr:
continue
addr = addr.split("%")[0]
try:
ipaddress.ip_address(addr)
ips.add(addr)
except Exception:
pass
return ips
def start(self):
if not ENABLE_NPCAP or not self.available:
if ENABLE_NPCAP:
print("[NPCAP] unavailable:", self.error or "Scapy/Npcap not available")
return
threading.Thread(target=self._run, daemon=True).start()
print("[NPCAP] passive packet telemetry started")
def _run(self):
try:
self.sniff(prn=self._packet, store=False)
except Exception as e:
self.available = False
self.error = str(e)
print("[NPCAP] capture stopped:", e)
def _packet(self, pkt):
try:
src = dst = None
proto_name = "OTHER"
if self.IP in pkt:
src = pkt[self.IP].src
dst = pkt[self.IP].dst
proto_num = int(pkt[self.IP].proto)
if self.TCP in pkt:
proto_name = "TCP"
elif self.UDP in pkt:
proto_name = "UDP"
elif self.ICMP in pkt:
proto_name = "ICMP"
else:
proto_name = f"IP{proto_num}"
elif self.IPv6 in pkt:
src = pkt[self.IPv6].src.split("%")[0]
dst = pkt[self.IPv6].dst.split("%")[0]
if self.TCP in pkt:
proto_name = "TCP"
elif self.UDP in pkt:
proto_name = "UDP"
else:
proto_name = "IPV6"
else:
return
if src in self.local_ips and public_ip(dst):
remote, direction = dst, "tx"
elif dst in self.local_ips and public_ip(src):
remote, direction = src, "rx"
else:
return
now = time.monotonic()
size = len(pkt)
with self.lock:
self.events[remote].append((now, direction, size))
self.protocols[remote].add(proto_name)
self.packet_times[remote].append(now)
cutoff = now - FLOW_WINDOW_S
q = self.events[remote]
while q and q[0][0] < cutoff:
q.popleft()
except Exception:
pass
def snapshot(self, ip):
now = time.monotonic()
with self.lock:
q = self.events[ip]
cutoff = now - FLOW_WINDOW_S
while q and q[0][0] < cutoff:
q.popleft()
q = list(q)
protos = set(self.protocols.get(ip, ()))
times = list(self.packet_times.get(ip, ()))
tx = sum(sz for _, d, sz in q if d == "tx")
rx = sum(sz for _, d, sz in q if d == "rx")
odd_proto = any(p not in {"TCP","UDP","ICMP"} for p in protos)
asym = tx >= ASYM_MIN_OUT_BYTES and (tx / max(rx, 1)) >= ASYM_RATIO
out_xfer = tx >= OUT_XFER_BYTES
pkt_beacon = False
if len(times) >= BEACON_MIN_EVENTS:
ints = [times[i]-times[i-1] for i in range(1, len(times))]
ints = [x for x in ints if BEACON_MIN_INTERVAL <= x <= BEACON_MAX_INTERVAL]
if len(ints) >= BEACON_MIN_EVENTS - 1:
mean = statistics.mean(ints)
if mean > 0:
pkt_beacon = (statistics.pstdev(ints) / mean) <= BEACON_CV_MAX
return {
"tx": tx, "rx": rx, "protocols": protos,
"odd_proto": odd_proto, "asym": asym,
"out_xfer": out_xfer, "beacon": pkt_beacon
}
def clock_heartbeat(esp, stop_event):
last_date = None
while not stop_event.is_set():
now = dt.datetime.now()
try:
esp.send(now.strftime("TIME %H:%M:%S"))
if now.date() != last_date:
esp.send(now.strftime("DATE %a %b %d").upper())
last_date = now.date()
except Exception as e:
print("[CLOCK] heartbeat stopped:", e)
return
delay = 1.0 - (time.time() % 1.0)
stop_event.wait(max(0.05, delay))
def main():
ap=argparse.ArgumentParser()
ap.add_argument("--port")
ap.add_argument("--baud",type=int,default=BAUD)
ap.add_argument("--ping-host",default=PING_HOST)
a=ap.parse_args()
port=a.port or find_serial_port()
if not port:
print("No serial port selected.")
for p in list_ports.comports(): print(p.device,p.description)
return 2
esp=Esp(port,a.baud)
stop_event = threading.Event()
threading.Thread(
target=clock_heartbeat, args=(esp, stop_event), daemon=True
).start()
packet = PacketTelemetry()
packet.start()
cache=load_cache()
mapped={}
region_ips=defaultdict(set)
failed={}
endpoint_history = defaultdict(lambda: deque(maxlen=10))
prev_fingerprints = set()
red_priority_until = 0.0
# Last rendered red-alert state per IP.
# If flags/flow/intel change, the CTF page is re-opened with fresh data.
last_alert_signature = {}
last_alert_refresh = defaultdict(float)
ALERT_REFRESH_MIN_S = 2.0
io0=psutil.net_io_counters()
sent0,recv0=io0.bytes_sent,io0.bytes_recv
last_ping=last_traffic=last_socket=0.0
try:
while True:
mono=time.monotonic()
if mono-last_ping>=PING_UPDATE_S:
ms=ping_ms(a.ping_host)
esp.send("PING DEAD" if ms is None else f"PING {ms}")
last_ping=mono
if mono-last_traffic>=TRAFFIC_UPDATE_S:
io=psutil.net_io_counters()
esp.send(f"TRAFFIC {max(0,io.bytes_sent-sent0)} {max(0,io.bytes_recv-recv0)}")
last_traffic=mono
if mono-last_socket>=SOCKET_POLL_S:
active=current_remote_ips()
current_fingerprints = set()
for bip, brec in active.items():
for bp in brec["ports"]:
current_fingerprints.add((bip, bp))
for key in (current_fingerprints - prev_fingerprints):
endpoint_history[key].append(mono)
prev_fingerprints = current_fingerprints
for ip in sorted(active):
if ip in mapped: continue
if ip in failed and mono-failed[ip]<60: continue
rec = active[ip]
ports = rec["ports"]
remote_port = min(ports) if ports else 0
conn_count = rec["conns"]
proc = process_name(rec["pids"])
d=geo(ip,cache)
if not d:
failed[ip]=mono
continue
mapped[ip]=d
cc=d["cc"]
region=region_for(cc)
region_ips[region].add(ip)
count=len(region_ips[region])
# Set status color first, then redraw the count in that state.
# Prevents a visible blue -> green flash for the newest region.
esp.send(f"LATEST {region}")
esp.send(f"REGION {region} {count}")
esp.send(f"IPS {len(mapped)}")
beacon, beacon_mean, beacon_cv = beacon_stats(endpoint_history[(ip, remote_port)])
flow = packet.snapshot(ip) if packet.available else {
"tx": 0, "rx": 0, "protocols": set(),
"odd_proto": False, "asym": False,
"out_xfer": False, "beacon": False
}
beacon = beacon or flow["beacon"]
ti = abuse_score(ip)
flags = signal_flags(
cc, remote_port, conn_count, ti,
beacon=beacon, exposed=False,
odd_proto=flow["odd_proto"],
asym_out=flow["asym"],
out_xfer=flow["out_xfer"]
)
alert = is_alert(flags)
intel_tag = ""
intel_ports = 0
intel_vulns = 0
if alert:
intel_tag, intel_ports, intel_vulns, exposed = passive_intel_summary(ip)
flags = signal_flags(
cc, remote_port, conn_count, ti,
beacon=beacon, exposed=exposed,
odd_proto=flow["odd_proto"],
asym_out=flow["asym"],
out_xfer=flow["out_xfer"]
)
alert = is_alert(flags)
if flags:
level = "A" if alert else "W"
# Red priority: don't replace a red page with amber until its dwell expires.
if level == "A" or mono >= red_priority_until:
asn = int(d.get("asn") or 0)
tx_kb = int(flow["tx"] // 1024)
rx_kb = int(flow["rx"] // 1024)
esp.send(
f"CTF {level} {region} {cc} {ip} "
f"{remote_port} {conn_count} {asn} {flags} {proc} "
f"{tx_kb} {rx_kb}"
)
if level == "A":
red_priority_until = mono + 11.0
if intel_tag:
safe_tag = re.sub(r"[^A-Z0-9_-]", "", intel_tag.upper())[:15] or "OSINT"
esp.send(f"INTEL {safe_tag} {intel_ports} {intel_vulns}")
last_alert_signature[ip] = (
flags,
remote_port,
conn_count,
int(flow["tx"] // 1024) // 256,
int(flow["rx"] // 1024) // 256,
intel_tag,
intel_ports,
intel_vulns,
)
last_alert_refresh[ip] = mono
loc=", ".join(x for x in (d.get("city"),d.get("country")) if x) or cc
org=f" | {d.get('org')}" if d.get("org") else ""
ti_txt = f" TI={ti}" if ti >= 0 else ""
btxt = f" beacon={beacon_mean:.1f}s cv={beacon_cv:.2f}" if beacon_mean else ""
flowtxt = (
f" flow30s TX={flow['tx']/1024:.1f}KB RX={flow['rx']/1024:.1f}KB"
if packet.available else ""
)
print(f"[NEW] #{len(mapped):03d} {ip} -> {cc} {loc}{org}")
print(
f" {proc} dst:{remote_port} conns:{conn_count} "
f"flags:0x{flags:X} alert:{alert}{ti_txt}{btxt}{flowtxt}"
)
# Keep WORLD alert count/node synchronized and refresh red CTF
# only when something materially changes.
active_alerts = []
for aip, arec in active.items():
d = mapped.get(aip)
if not d:
continue
ports = arec["ports"]
p = min(ports) if ports else 0
ti = abuse_score(aip)
socket_beacon, _, _ = beacon_stats(endpoint_history[(aip, p)])
aflow = packet.snapshot(aip) if packet.available else {
"tx": 0, "rx": 0, "protocols": set(),
"odd_proto": False, "asym": False,
"out_xfer": False, "beacon": False
}
beacon = socket_beacon or aflow["beacon"]
fl = signal_flags(
d["cc"], p, arec["conns"], ti,
beacon=beacon, exposed=False,
odd_proto=aflow["odd_proto"],
asym_out=aflow["asym"],
out_xfer=aflow["out_xfer"]
)
if not is_alert(fl):
continue
active_alerts.append((aip, d, fl))
# Build a compact "material state" signature.
# Flow is bucketed to 256 KB so tiny packet changes don't
# constantly re-open the CTF page.
tx_kb = int(aflow["tx"] // 1024)
rx_kb = int(aflow["rx"] // 1024)
tx_bucket = tx_kb // 256
rx_bucket = rx_kb // 256
# Passive intel is queried for red alerts only.
intel_tag, intel_ports, intel_vulns, exposed = passive_intel_summary(aip)
fl2 = signal_flags(
d["cc"], p, arec["conns"], ti,
beacon=beacon, exposed=exposed,
odd_proto=aflow["odd_proto"],
asym_out=aflow["asym"],
out_xfer=aflow["out_xfer"]
)
sig = (
fl2,
p,
arec["conns"],
tx_bucket,
rx_bucket,
intel_tag,
intel_ports,
intel_vulns,
)
changed = last_alert_signature.get(aip) != sig
cooled = (mono - last_alert_refresh[aip]) >= ALERT_REFRESH_MIN_S
if changed and cooled:
region = region_for(d["cc"])
proc = process_name(arec["pids"])
asn = int(d.get("asn") or 0)
# Re-open/refresh the red CTF page with the updated state.
esp.send(
f"CTF A {region} {d['cc']} {aip} "
f"{p} {arec['conns']} {asn} {fl2} {proc} "
f"{tx_kb} {rx_kb}"
)
safe_tag = re.sub(
r"[^A-Z0-9_-]", "", (intel_tag or "OSINT").upper()
)[:15] or "OSINT"
esp.send(f"INTEL {safe_tag} {intel_ports} {intel_vulns}")
last_alert_signature[aip] = sig
last_alert_refresh[aip] = mono
red_priority_until = mono + 7.0
# Drop stale signatures for alerts that are no longer active.
active_alert_ips = {x[0] for x in active_alerts}
for old_ip in list(last_alert_signature):
if old_ip not in active_alert_ips:
del last_alert_signature[old_ip]
esp.send(f"ALERTS {len(active_alerts)}")
if active_alerts:
_, newest_alert_geo, _ = active_alerts[-1]
esp.send(f"ALERTNODE {region_for(newest_alert_geo['cc'])}")
else:
esp.send("ALERTNODE NONE")
last_socket=mono
time.sleep(0.05)
except KeyboardInterrupt:
print("\nStopped.")
finally:
stop_event.set()
esp.close()
return 0
if __name__=="__main__":
raise SystemExit(main())
The Python side requires pyserial, psutil, requests and Scapy, with Npcap installed on Windows.
The code for this project was developed with the help of ChatGPT.