Hardware components | ||||||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
| |||||
It is a real-time packet intelligence terminal built on the M5Cardputer that's connects to the global APRS-IS network, pulls in live traffic, and does something no existing APRS tool does, it runs every packet through a threat engine before displaying it.
Instead of showing raw packets, APRS HAWK translates them into intelligence.
It connects to the global APRS-IS network, pulls in live traffic, and does something no existing APRS tool does: it runs every packet through a threat engine before displaying it.
Eight screens. One pocket device.
- Screen 1 - Dashboard Live threat score, status, packet counts, last heard all
- Screen 2 - Threat Feed (Timestamped alert log with callsign and detail)
- Screen 3 - RF Health (RF vs internet ratio, top digipeater paths, noise score)
- Screen 4 - Top Stations (Ranked leaderboard with anomaly flags)
- Screen 5 - Packet Analytics (Position, message, weather, telemetry)
- Screen 6 - Satellite Activity (Live satellite pass detection with active/last-heard status)
- Screen 7 - Security Alerts (Full threat breakdown with score bar)
- Screen 8 - Intel Summary (RF health, uptime, most active callsign)
APRS Hawk
C/C++1)Flash this code onto an M5StickCardputer Adv via Arduino IDE.
2) Enter your WiFi credentials in the lines below:
const char* ssid = "Your Wfif SSID";
const char* password = "Your WIF Password;
3) Power on the device.
4) The badge connects to WiFi and opens a live TCP stream to rotate.aprs2.net and instantly begins analysing every incoming packet for threats, anomalies, and satellite activity in real time.
This badge is designed for:
- Educational demos
- Space enthusiasts
- Ham radio operators
2) Enter your WiFi credentials in the lines below:
const char* ssid = "Your Wfif SSID";
const char* password = "Your WIF Password;
3) Power on the device.
4) The badge connects to WiFi and opens a live TCP stream to rotate.aprs2.net and instantly begins analysing every incoming packet for threats, anomalies, and satellite activity in real time.
This badge is designed for:
- Educational demos
- Space enthusiasts
- Ham radio operators
#include <M5Cardputer.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <SD.h>
#include <SPI.h>
#include <time.h>
// WIFI Details
#define WIFI_SSID "" //Your WIFI SSID
#define WIFI_PASS "" //Your WIF Password
#define MY_CALL "N0CALL"
#define APRS_FILTER "filter t/p"
#define APRS_HOST "rotate.aprs2.net"
#define APRS_PORT 14580
// SD Card
#define SD_CS 12
#define SD_SCK 40
#define SD_MOSI 14
#define SD_MISO 39
SPIClass sdSPI(HSPI);
bool sdOK = false;
// Display
M5Canvas canvA(&M5Cardputer.Display);
M5Canvas canvB(&M5Cardputer.Display);
M5Canvas* draw = &canvA;
M5Canvas* shown = &canvB;
// Network
WiFiClient aprsClient;
bool wifiOK = false, aprsOK = false;
String inputBuf = "";
uint32_t lastKA = 0;
uint32_t bootTime = 0;
enum WiFiSM { WF_IDLE,WF_WAIT,WF_DONE,WF_FAIL };
WiFiSM wfState = WF_IDLE;
uint32_t wfTimer = 0;
bool aprsAttempted = false;
// Color Define
#define CB TFT_BLACK
#define CGRN 0x07E0
#define CCYN 0x07FF
#define CYEL 0xFFE0
#define CRED 0xF800
#define CORN 0xFD20
#define CWHT 0xFFFF
#define CGRY 0x8410
#define CDKG 0x2104
#define CPUR 0x781F
#define CLIM 0x7FE0
#define CDKGRN 0x0320
#define CDKRED 0x4000
#define CDKBLU 0x000F
#define CDKCYN 0x0210
// Display
#define W 240
#define H 135
#define HDR 14
#define FTR 10
#define BODY_Y (HDR+1)
#define BODY_H (H - HDR - FTR - 1)
// ============================================================
// STATIONS
// ============================================================
#define MAX_ST 150
struct Station {
char call[14];
float lat,lon,prevLat,prevLon;
uint16_t pktCount;
uint32_t firstSeen,lastSeen,prevPosTm,lastPktTm;
uint32_t avgInterval;
uint8_t hopCount,intervalSamples;
bool seenRF,seenINET;
uint16_t pktWindow;
uint32_t windowStart;
bool flagFlood,flagJump,flagDualSrc,flagPath,flagBeacon;
};
Station st[MAX_ST];
int stCount=0;
uint32_t cntPos=0,cntMsg=0,cntStat=0,cntWX=0,cntObj=0,cntTel=0,cntOther=0;
uint32_t totalPkts=0,rfPkts=0,inetPkts=0,newStations=0;
uint32_t disappearedSt=0;
String lastCall="-",mostActive="-";
uint16_t mostActiveCnt=0;
// Path stats
#define MAX_PATHS 16
struct PathStat { char path[20]; uint16_t count; };
PathStat pathStats[MAX_PATHS];
int pathCount=0;
// Alerts
#define MAX_ALERTS 30
struct Alert {
char type[14];
char call[14];
char detail[24];
uint32_t ts;
};
Alert alerts[MAX_ALERTS];
int alertHead=0,totalAlerts=0;
uint16_t threatScore=0;
uint32_t lastDecay=0;
uint8_t threatLevel(){ if(threatScore>=71)return 3; if(threatScore>=41)return 2; if(threatScore>=21)return 1; return 0; }
const char* TLBL[]={" LOW ","MEDIUM "," HIGH ","CRITICAL"};
uint16_t TCOL[]={CGRN,CYEL,CORN,CRED};
uint16_t TBGCOL[]={CDKGRN,0x4200,0x4100,CDKRED};
// Satellites
const char* SAT_TOK[]={"ARISS","RS0ISS","ISS","NO-84","NO84","PSAT","PSAT2","TEVEL","AISAT","PO-101","UISS"};
#define SAT_N 11
#define MAX_SAT 10
struct SatSt { char call[14]; uint16_t pktCount; uint32_t lastSeen; bool active; };
SatSt satSt[MAX_SAT];
int satCount=0;
uint32_t totalSatPkts=0;
char lastSatCall[14]="-";
// UI
uint8_t screen=0;
uint32_t lastDraw=0;
uint32_t lastIntelUpd=0;
#define REFRESH_MS 500
// Backlight Dim Function
#define BACKLIGHT_TIMEOUT 30000
#define BACKLIGHT_DIM 10
#define BACKLIGHT_FULL 200
bool backlightOn = true;
uint32_t lastActivity = 0;
// Battery Percentage
#define BATT_SAMPLES 8
uint8_t battBuf[BATT_SAMPLES] = {0};
uint8_t battIdx = 0;
bool battFilled = false;
uint8_t battSmooth = 0;
void updateBattery(){
battBuf[battIdx] = M5.Power.getBatteryLevel();
battIdx = (battIdx + 1) % BATT_SAMPLES;
if(battIdx == 0) battFilled = true;
int n = battFilled ? BATT_SAMPLES : battIdx;
int sum = 0;
for(int i=0;i<n;i++) sum += battBuf[i];
battSmooth = (uint8_t)(sum / n);
}
// SD LOGGING
void logPacket(const String& line){
if(!sdOK) return;
if(SD.exists("/hawk.log")){
File c=SD.open("/hawk.log",FILE_READ);
if(c){ size_t sz=c.size(); c.close(); if(sz>2UL*1024*1024){ SD.remove("/hawk.old"); SD.rename("/hawk.log","/hawk.old"); } }
}
File f=SD.open("/hawk.log",FILE_APPEND);
if(f){ f.println(line); f.close(); }
}
// ALERT ENGINE
void raiseAlert(const char* type,const char* call,const char* detail,uint8_t score){
int s=alertHead%MAX_ALERTS;
strncpy(alerts[s].type,type,14);
strncpy(alerts[s].call,call,14);
strncpy(alerts[s].detail,detail,24);
alerts[s].ts=millis();
alertHead++; totalAlerts++;
threatScore=min((int)threatScore+score,100);
}
void decayThreat(){ if(millis()-lastDecay>60000){ lastDecay=millis(); if(threatScore>=5)threatScore-=5;else threatScore=0; } }
// HELPERS
bool hasSatToken(const String& s){ for(int i=0;i<SAT_N;i++) if(s.indexOf(SAT_TOK[i])>=0) return true; return false; }
void recordSat(const char* call){
totalSatPkts++; strncpy(lastSatCall,call,14);
for(int i=0;i<satCount;i++){ if(strcmp(satSt[i].call,call)==0){ satSt[i].pktCount++; satSt[i].lastSeen=millis(); satSt[i].active=true; return; } }
if(satCount<MAX_SAT){ strncpy(satSt[satCount].call,call,14); satSt[satCount].pktCount=1; satSt[satCount].lastSeen=millis(); satSt[satCount].active=true; satCount++; }
}
void recordPath(const String& ps){
int c=ps.indexOf(','); String f=c>0?ps.substring(0,c):ps; f.trim();
if(f.length()<2||f.length()>18) return;
for(int i=0;i<pathCount;i++){ if(f==String(pathStats[i].path)){pathStats[i].count++;return;} }
if(pathCount<MAX_PATHS){ f.toCharArray(pathStats[pathCount].path,20); pathStats[pathCount].count=1; pathCount++; }
}
float nmeaDeg(const String& r,char h){ if(r.length()<4)return 0; int d=r.indexOf('.'); if(d<2)return 0; float deg=r.substring(0,d-2).toFloat()+r.substring(d-2).toFloat()/60.0; if(h=='S'||h=='W')deg=-deg; return deg; }
bool parsePos(const String& line,float& lat,float& lon){ int col=line.indexOf(':'); if(col<0)return false; String info=line.substring(col+1); for(int i=1;i<(int)info.length()-18;i++){ char ns=info[i+7],ew=info[i+17]; if((ns=='N'||ns=='S')&&(ew=='E'||ew=='W')){ lat=nmeaDeg(info.substring(i,i+7),ns); lon=nmeaDeg(info.substring(i+9,i+17),ew); return(lat!=0||lon!=0); } } return false; }
float haversineKm(float a,float b,float c,float d){ const float R=6371; float dl=(c-a)*PI/180,dn=(d-b)*PI/180,x=sin(dl/2)*sin(dl/2)+cos(a*PI/180)*cos(c*PI/180)*sin(dn/2)*sin(dn/2); return R*2*atan2(sqrt(x),sqrt(1-x)); }
void classifyPkt(const String& l){ int c=l.indexOf(':'); if(c<0){cntOther++;return;} char x=l[c+1]; if(x=='!'||x=='='||x=='@'||x=='/')cntPos++; else if(x==':')cntMsg++; else if(x=='>')cntStat++; else if(x=='_')cntWX++; else if(x==';')cntObj++; else if(x=='T')cntTel++; else cntOther++; }
void uptimeStr(char* buf,int len){ if(!bootTime){strncpy(buf,"--:--",len);return;} uint32_t s=(millis()-bootTime)/1000; snprintf(buf,len,"%02lu:%02lu:%02lu",s/3600,(s%3600)/60,s%60); }
// PROCESS PACKET
void processPacket(const String& raw){
if(raw.length()<5||raw.startsWith("#")||raw.startsWith("user")) return;
int gt=raw.indexOf('>'); if(gt<=0) return;
char call[14]="",ssid[4]="";
String fc_s=raw.substring(0,gt);
int dash=fc_s.indexOf('-');
if(dash>0){fc_s.substring(0,dash).toCharArray(call,14);fc_s.substring(dash+1).toCharArray(ssid,4);}
else fc_s.toCharArray(call,14);
if(strlen(call)<2) return;
char fc[18]="";
if(ssid[0])snprintf(fc,18,"%s-%s",call,ssid);else strncpy(fc,call,18);
int col=raw.indexOf(':');
bool isRF=true; String pathStr="";
if(col>gt){ pathStr=raw.substring(gt+1,col); isRF=(pathStr.indexOf("TCPIP")<0&&pathStr.indexOf("TCPXX")<0); recordPath(pathStr); }
if(hasSatToken(String(call))||hasSatToken(pathStr)) recordSat(call);
classifyPkt(raw);
totalPkts++; if(isRF)rfPkts++;else inetPkts++;
lastCall=String(fc);
int idx=-1;
for(int i=0;i<stCount;i++) if(strcmp(st[i].call,fc)==0){idx=i;break;}
if(idx<0&&stCount<MAX_ST){
idx=stCount++;
memset(&st[idx],0,sizeof(Station));
strncpy(st[idx].call,fc,14);
st[idx].firstSeen=millis(); st[idx].windowStart=millis();
newStations++;
}
if(idx<0) return;
uint32_t now=millis();
// beacon interval anomaly
if(st[idx].lastPktTm>0){
uint32_t iv=now-st[idx].lastPktTm;
if(st[idx].intervalSamples==0){st[idx].avgInterval=iv;st[idx].intervalSamples=1;}
else{
st[idx].avgInterval=(uint32_t)(st[idx].avgInterval*0.8+iv*0.2);
if(st[idx].intervalSamples<255)st[idx].intervalSamples++;
if(!st[idx].flagBeacon&&st[idx].intervalSamples>=5&&st[idx].avgInterval>30000&&iv<st[idx].avgInterval/8){
st[idx].flagBeacon=true;
char det[24]; snprintf(det,24,"%.0fs avg->%.0fs",st[idx].avgInterval/1000.0,iv/1000.0);
raiseAlert("BeaconAnom",fc,det,10);
}
}
}
st[idx].lastPktTm=now;
st[idx].pktCount++; st[idx].lastSeen=now;
if(isRF)st[idx].seenRF=true;else st[idx].seenINET=true;
if(!st[idx].flagDualSrc&&st[idx].seenRF&&st[idx].seenINET){
st[idx].flagDualSrc=true; raiseAlert("DualSrc",fc,"RF+INET seen",10);
}
if(now-st[idx].windowStart>90000){st[idx].pktWindow=0;st[idx].windowStart=now;}
st[idx].pktWindow++;
if(!st[idx].flagFlood&&st[idx].pktWindow>15){
st[idx].flagFlood=true;
char det[24]; snprintf(det,24,"%d pkts/90s",st[idx].pktWindow);
raiseAlert("Flood",fc,det,25);
}
float lat=0,lon=0;
if(parsePos(raw,lat,lon)&&lat!=0){
if(st[idx].prevLat!=0&&st[idx].prevPosTm!=0){
float dist=haversineKm(st[idx].prevLat,st[idx].prevLon,lat,lon);
uint32_t dt=(now-st[idx].prevPosTm)/1000;
if(dt>0&&dist>200&&(dist/dt)*3600>800){
if(!st[idx].flagJump){
st[idx].flagJump=true;
char det[24]; snprintf(det,24,"%.0fkm in %lus",dist,dt);
raiseAlert("PosJump",fc,det,20);
}
}
}
st[idx].prevLat=st[idx].lat=lat; st[idx].prevLon=st[idx].lon=lon; st[idx].prevPosTm=now;
}
uint8_t hops=0; for(int i=0;i<(int)pathStr.length();i++) if(pathStr[i]==',')hops++;
st[idx].hopCount=hops;
if(!st[idx].flagPath&&hops>3&&st[idx].pktCount>5){
st[idx].flagPath=true;
char det[24]; snprintf(det,24,"%d hops",hops);
raiseAlert("PathManip",fc,det,15);
}
if(st[idx].pktCount>mostActiveCnt){mostActiveCnt=st[idx].pktCount;mostActive=String(fc);}
logPacket(raw);
}
// INTEL UPDATE
void updateIntel(){
uint32_t now=millis(); disappearedSt=0;
for(int i=0;i<stCount;i++) if(now-st[i].lastSeen>600000)disappearedSt++;
for(int i=0;i<satCount;i++) satSt[i].active=(now-satSt[i].lastSeen<600000);
}
// DRAW PRIMITIVES
// Header bar: title left, battery right, dots middle-right
void hdr(const char* title, uint8_t scr){
draw->fillRect(0,0,W,HDR,CDKG);
draw->setTextSize(1); draw->setTextColor(CGRN);
draw->setCursor(3,3); draw->print(title);
// screen dots
for(int i=0;i<8;i++) draw->fillCircle(150+i*5,6,2,i==scr?CGRN:0x4208);
// battery
draw->setTextColor(CWHT); draw->setCursor(208,3);
draw->printf("B%d%%",battSmooth);
draw->drawFastHLine(0,HDR,W,CDKG);
}
// Footer bar
void ftr(const char* hint){
draw->fillRect(0,H-FTR,W,FTR,CDKG);
draw->setTextColor(CCYN); draw->setTextSize(1);
draw->setCursor(3,H-FTR+1); draw->print(hint);
}
// Horizontal divider
void hdiv(int y){ draw->drawFastHLine(0,y,W,CDKG); }
// Table row
void tRow(int y,const char* lbl,const char* val,uint16_t lc=CGRY,uint16_t vc=CWHT){
draw->setTextSize(1);
draw->setTextColor(lc); draw->setCursor(4,y); draw->printf("%-11s",lbl);
draw->setTextColor(vc); draw->setCursor(90,y); draw->print(val);
}
// Two-column table row
void tRow2(int y,const char* l1,const char* v1,const char* l2,const char* v2,
uint16_t vc1=CWHT,uint16_t vc2=CWHT){
draw->setTextSize(1);
draw->setTextColor(CGRY); draw->setCursor(4,y); draw->printf("%-7s",l1);
draw->setTextColor(vc1); draw->setCursor(52,y); draw->printf("%-10s",v1);
draw->drawFastVLine(122,y-1,9,CDKG);
draw->setTextColor(CGRY); draw->setCursor(126,y); draw->printf("%-7s",l2);
draw->setTextColor(vc2); draw->setCursor(174,y); draw->print(v2);
}
// Mini horizontal bar
void miniBar(int x,int y,int w,int h2,float pct,uint16_t col){
draw->fillRect(x,y,w,h2,CDKG);
int fw=(int)(w*pct/100); if(fw>0) draw->fillRect(x,y,fw,h2,col);
}
// SCREEN 0 — SOC DASHBOARD
void drawSOC(){
draw->fillScreen(CB);
hdr("APRS HAWK",0);
uint8_t tl=threatLevel();
draw->fillRect(0,HDR,W,16,TBGCOL[tl]);
draw->setTextSize(1); draw->setTextColor(TCOL[tl]);
draw->setCursor(4,HDR+4);
draw->printf("THREAT: %s",TLBL[tl]);
draw->setCursor(160,HDR+4);
draw->printf("SCORE: %3d",threatScore);
hdiv(HDR+16);
int y=HDR+20;
char b1[20],b2[20];
// row: WIFI | APRS
snprintf(b1,20,"%s",wifiOK?"ONLINE":"OFFLINE");
snprintf(b2,20,"%s",aprsOK?"STREAM":"OFFLINE");
tRow2(y,"WIFI",b1,"APRS",b2,wifiOK?CGRN:CRED,aprsOK?CCYN:CORN); y+=11;
hdiv(y); y+=3;
// row: PKTS | RF
snprintf(b1,20,"%lu",totalPkts); snprintf(b2,20,"%lu",rfPkts);
tRow2(y,"PKTS",b1,"RF",b2,CWHT,CLIM); y+=11;
// row: STATIONS | NEW
snprintf(b1,20,"%d",stCount); snprintf(b2,20,"%lu",newStations);
tRow2(y,"STNS",b1,"NEW",b2); y+=11;
hdiv(y); y+=3;
// row: THREATS | ALERTS
int thr=0; for(int i=0;i<stCount;i++) if(st[i].flagFlood||st[i].flagJump||st[i].flagDualSrc||st[i].flagPath||st[i].flagBeacon)thr++;
snprintf(b1,20,"%d",thr); snprintf(b2,20,"%d",totalAlerts);
tRow2(y,"THREATS",b1,"ALERTS",b2,thr?CRED:CGRY,totalAlerts?CYEL:CGRY); y+=11;
// row: LAST HEARD (full width)
hdiv(y); y+=3;
draw->setTextColor(CGRY); draw->setCursor(4,y); draw->print("LAST ");
draw->setTextColor(CYEL); draw->setCursor(52,y);
draw->print(lastCall.substring(0,22)); y+=11;
// row: UPTIME | SAT
hdiv(y); y+=3;
char ut[12]; uptimeStr(ut,12);
snprintf(b2,20,"%lu",totalSatPkts);
tRow2(y,"UPTIME",ut,"SAT PKT",b2,CGRY,totalSatPkts?CCYN:CGRY);
ftr("1-8 screens R=reconnect S=save SD");
}
// SCREEN 1 — THREAT FEED
void drawThreatFeed(){
draw->fillScreen(CB);
hdr("THREAT FEED",1);
int y=HDR+2;
draw->setTextSize(1); draw->setTextColor(CDKG);
draw->fillRect(0,y,W,9,0x18C3);
draw->setTextColor(CCYN); draw->setCursor(4,y+1);
draw->print("AGO TYPE STATION DETAIL");
hdiv(y+9); y+=11;
if(totalAlerts==0){
draw->setTextColor(CGRY); draw->setCursor(4,y+20);
draw->print("No threats detected yet.");
ftr("Monitoring live APRS stream...");
return;
}
int shown=0;
for(int i=alertHead-1; i>=max(0,alertHead-MAX_ALERTS) && shown<8; i--){
int slot=((i%MAX_ALERTS)+MAX_ALERTS)%MAX_ALERTS;
uint32_t ago=(millis()-alerts[slot].ts)/1000;
char abuf[8];
if(ago<60) snprintf(abuf,8,"%lus",ago);
else if(ago<3600) snprintf(abuf,8,"%lum",ago/60);
else snprintf(abuf,8,"%luh",ago/3600);
uint16_t tc=CYEL;
if(strcmp(alerts[slot].type,"Flood")==0) tc=CRED;
if(strcmp(alerts[slot].type,"PosJump")==0) tc=CORN;
if(strcmp(alerts[slot].type,"DualSrc")==0) tc=CYEL;
if(strcmp(alerts[slot].type,"PathManip")==0) tc=CCYN;
if(strcmp(alerts[slot].type,"BeaconAnom")==0)tc=CPUR;
if(shown%2==0) draw->fillRect(0,y,W,10,0x0821);
draw->setTextSize(1);
draw->setTextColor(CGRY); draw->setCursor(4,y+1); draw->printf("%-7s",abuf);
draw->setTextColor(tc); draw->setCursor(56,y+1); draw->printf("%-13s",alerts[slot].type);
draw->setTextColor(CWHT); draw->setCursor(130,y+1); draw->printf("%-10s",alerts[slot].call);
draw->setTextColor(0x6B4D);draw->setCursor(194,y+1); draw->printf("%-10s",alerts[slot].detail);
y+=10; shown++;
}
ftr("Newest first · colour=severity");
}
// SCREEN 2 — RF HEALTH TABLE
void drawRFHealth(){
draw->fillScreen(CB);
hdr("RF HEALTH",2);
float rfR=totalPkts>0?(float)rfPkts/totalPkts*100:0;
uint32_t now=millis(); int act5=0;
for(int i=0;i<stCount;i++) if(now-st[i].lastSeen<300000)act5++;
const char* noiseL="LOW"; uint16_t noiseC=CGRN;
if(rfR<30){noiseL="HIGH";noiseC=CRED;}else if(rfR<60){noiseL="MED";noiseC=CYEL;}
int y=HDR+3;
draw->setTextColor(CGRY); draw->setCursor(4,y); draw->printf("RF Ratio %.0f%%",rfR);
miniBar(130,y,106,7,rfR,CGRN); y+=11;
hdiv(y); y+=3;
char b1[20],b2[20];
snprintf(b1,20,"%lu",rfPkts); snprintf(b2,20,"%lu",inetPkts);
tRow2(y,"RF pkts",b1,"INET",b2,CLIM,CGRY); y+=11;
snprintf(b1,20,"%d",act5);
snprintf(b2,20,"%s",noiseL);
tRow2(y,"Active5m",b1,"Noise",b2,CWHT,noiseC); y+=11;
hdiv(y); y+=3;
char topP[20]="-"; uint16_t topPC=0;
for(int i=0;i<pathCount;i++) if(pathStats[i].count>topPC){topPC=pathStats[i].count;strncpy(topP,pathStats[i].path,20);}
tRow(y,"Top Path",topP,CGRY,CCYN); y+=11;
hdiv(y); y+=3;
draw->setTextColor(CCYN); draw->setCursor(4,y);
draw->fillRect(0,y,W,9,0x18C3);
draw->print("PATH COUNT %TOTAL");
y+=10;
bool used[MAX_PATHS]={};
for(int s=0;s<4;s++){
int best=-1; uint16_t bv=0;
for(int i=0;i<pathCount;i++) if(!used[i]&&pathStats[i].count>bv){bv=pathStats[i].count;best=i;}
if(best<0) break; used[best]=true;
if(s%2==0) draw->fillRect(0,y,W,10,0x0821);
float pct=totalPkts?pathStats[best].count*100.0/totalPkts:0;
draw->setTextColor(CWHT); draw->setCursor(4,y+1);
draw->printf("%-18s %5d %4.1f%%",pathStats[best].path,pathStats[best].count,pct);
y+=10;
}
ftr("RF=green INET=gray Noise=ratio");
}
// SCREEN 3 — TOP STATIONS TABLE
void drawTopStations(){
draw->fillScreen(CB);
hdr("TOP STATIONS",3);
int y=HDR+2;
draw->fillRect(0,y,W,9,0x18C3);
draw->setTextSize(1); draw->setTextColor(CCYN);
draw->setCursor(4,y+1);
draw->print("CALLSIGN PKTS SRC HOPS FLAG");
hdiv(y+9); y+=11;
if(stCount==0){ draw->setTextColor(CGRY); draw->setCursor(4,y+10); draw->print("No stations yet..."); ftr("Waiting for packets"); return; }
static Station sorted[MAX_ST];
memcpy(sorted,st,sizeof(Station)*stCount);
for(int i=0;i<stCount-1;i++)
for(int j=i+1;j<stCount;j++)
if(sorted[j].pktCount>sorted[i].pktCount)
{Station tmp=sorted[i];sorted[i]=sorted[j];sorted[j]=tmp;}
int show=min(stCount,9);
for(int i=0;i<show;i++){
if(i%2==0) draw->fillRect(0,y,W,10,0x0821);
uint16_t cc=CWHT;
char flag[5]=" -";
if(sorted[i].flagFlood) {cc=CRED; strncpy(flag,"FLD",4);}
else if(sorted[i].flagJump) {cc=CORN; strncpy(flag,"JMP",4);}
else if(sorted[i].flagDualSrc){cc=CYEL; strncpy(flag,"DUP",4);}
else if(sorted[i].flagBeacon){cc=CPUR; strncpy(flag,"BCN",4);}
else if(sorted[i].flagPath) {cc=CCYN; strncpy(flag,"PTH",4);}
char src[5]="";
if(sorted[i].seenRF&&sorted[i].seenINET) strncpy(src,"BOTH",5);
else if(sorted[i].seenRF) strncpy(src,"RF",5);
else strncpy(src,"INET",5);
draw->setTextSize(1); draw->setTextColor(cc);
draw->setCursor(4,y+1);
draw->printf("%-11s %4d %-4s %4d %s",
sorted[i].call, sorted[i].pktCount,
src, sorted[i].hopCount, flag);
y+=10;
}
ftr("FLD=flood JMP=jump DUP=dual BCN=bcn");
}
// SCREEN 4 — PACKET ANALYTICS
void drawAnalytics(){
draw->fillScreen(CB);
hdr("PKT ANALYTICS",4);
struct { const char* n; uint32_t* c; uint16_t col; } rows[]={
{"POSITION",&cntPos,CGRN},{"MESSAGE",&cntMsg,CCYN},
{"STATUS",&cntStat,CWHT},{"WEATHER",&cntWX,CYEL},
{"OBJECT",&cntObj,CORN},{"TELEMETRY",&cntTel,CPUR},
{"OTHER",&cntOther,CGRY},
};
int y=HDR+2;
draw->fillRect(0,y,W,9,0x18C3);
draw->setTextSize(1); draw->setTextColor(CCYN);
draw->setCursor(4,y+1); draw->print("TYPE COUNT PCT BAR");
hdiv(y+9); y+=11;
uint32_t grand=max(totalPkts,(uint32_t)1);
for(int i=0;i<7;i++){
if(i%2==0) draw->fillRect(0,y,W,10,0x0821);
float pct=*rows[i].c*100.0/grand;
draw->setTextColor(rows[i].col); draw->setCursor(4,y+1);
draw->printf("%-12s %5lu %5.1f%%",rows[i].n,*rows[i].c,pct);
miniBar(200,y+2,36,6,pct,rows[i].col);
y+=10;
}
ftr("Packet type breakdown");
}
// SCREEN 5 — SATELLITE
void drawSatellite(){
draw->fillScreen(CB);
hdr("SAT APRS",5);
int y=HDR+3;
char b1[20],b2[20];
snprintf(b1,20,"%lu",totalSatPkts); snprintf(b2,20,"%s",lastSatCall);
tRow2(y,"Sat Pkts",b1,"Last",b2,totalSatPkts?CCYN:CGRY,CYEL); y+=11;
hdiv(y); y+=3;
if(satCount==0){
draw->setTextColor(CGRY); draw->setCursor(4,y);
draw->print("No satellite activity yet.");
draw->setCursor(4,y+11);
draw->print("Watching callsign + path for:");
draw->setTextColor(CDKG+0x0210); draw->setCursor(4,y+22);
draw->print("ARISS RS0ISS ISS NO-84 PSAT");
draw->setCursor(4,y+33);
draw->print("PSAT2 TEVEL AISAT PO-101 UISS");
ftr("Satellite pass detection active");
return;
}
// table header
draw->fillRect(0,y,W,9,0x18C3);
draw->setTextSize(1); draw->setTextColor(CCYN);
draw->setCursor(4,y+1); draw->print(" SAT PKTS STATUS AGO");
hdiv(y+9); y+=11;
uint32_t now=millis();
for(int i=0;i<satCount&&i<7;i++){
if(i%2==0) draw->fillRect(0,y,W,10,0x0821);
bool actv=satSt[i].active;
uint32_t ago=(now-satSt[i].lastSeen)/1000;
char abuf[10];
if(ago<60)snprintf(abuf,10,"%lus",ago);
else if(ago<3600)snprintf(abuf,10,"%lum",ago/60);
else snprintf(abuf,10,"%luh",ago/3600);
draw->setTextColor(actv?CGRN:CGRY);
draw->setCursor(4,y+1);
draw->printf("%s %-12s %4d %-6s %s",
actv?"●":"○", satSt[i].call, satSt[i].pktCount,
actv?"ACTIVE":"LAST", abuf);
y+=10;
}
ftr("● active now ○ last heard");
}
// SCREEN 6 — SECURITY ALERTS
void drawSecAlerts(){
draw->fillScreen(CB);
hdr("SECURITY",6);
int floods=0,jumps=0,duals=0,paths=0,beacons=0;
for(int i=0;i<stCount;i++){
if(st[i].flagFlood) floods++;
if(st[i].flagJump) jumps++;
if(st[i].flagDualSrc) duals++;
if(st[i].flagPath) paths++;
if(st[i].flagBeacon) beacons++;
}
int y=HDR+3;
// threat score bar
draw->setTextColor(CGRY); draw->setCursor(4,y);
uint8_t tl=threatLevel();
draw->printf("Threat Score: %d/100 [%s]",threatScore,TLBL[tl]);
y+=9;
miniBar(4,y,232,5,(float)threatScore,TCOL[tl]); y+=9;
hdiv(y); y+=3;
// detection summary table
draw->fillRect(0,y,W,9,0x18C3);
draw->setTextSize(1); draw->setTextColor(CCYN);
draw->setCursor(4,y+1); draw->print("TYPE COUNT SCORE");
hdiv(y+9); y+=11;
struct { const char* i; const char* n; int cnt; uint16_t c; int sc; } rows[]={
{"!","Flood", floods, CRED, 25},
{"^","PosJump", jumps, CORN, 20},
{"#","PathManip", paths, CCYN, 15},
{"~","DualSrc", duals, CYEL, 10},
{"*","BeaconAnom",beacons,CPUR, 10},
};
for(int i=0;i<5;i++){
if(i%2==0) draw->fillRect(0,y,W,10,0x0821);
draw->setTextColor(rows[i].cnt?rows[i].c:CDKG);
draw->setCursor(4,y+1);
draw->printf("%s %-16s %3d +%d",rows[i].i,rows[i].n,rows[i].cnt,rows[i].sc);
y+=10;
}
hdiv(y); y+=3;
draw->setTextColor(CGRY); draw->setCursor(4,y); draw->printf("Total alerts: %d",totalAlerts);
ftr("Score decays 5pts/min without alerts");
}
// SCREEN 7 — INTEL SUMMARY
void drawIntel(){
draw->fillScreen(CB);
hdr("APRS INTEL",7);
uint32_t now=millis(); int act5=0,act10=0;
for(int i=0;i<stCount;i++){
if(now-st[i].lastSeen<300000)act5++;
if(now-st[i].lastSeen<600000)act10++;
}
float rfR=totalPkts>0?(float)rfPkts/totalPkts*100:0;
const char* netH="NOR"; uint16_t nhC=CGRN;
if(threatScore>=41){netH="DEG";nhC=CRED;}
else if(threatScore>=21){netH="CAU";nhC=CYEL;}
char ut[12]; uptimeStr(ut,12);
char b1[20],b2[20];
int y=HDR+3;
// table
draw->fillRect(0,y,W,9,0x18C3);
draw->setTextSize(1); draw->setTextColor(CCYN);
draw->setCursor(4,y+1); draw->print("METRIC VALUE | METRIC VALUE");
hdiv(y+9); y+=11;
auto iRow=[&](const char* l1,const char* v1,const char* l2,const char* v2,uint16_t c1=CWHT,uint16_t c2=CWHT){
if( ((y-HDR-12)/11) %2==0) draw->fillRect(0,y,W,10,0x0821);
draw->drawFastVLine(122,y,10,CDKG);
draw->setTextColor(CGRY); draw->setCursor(4,y+1); draw->printf("%-12s",l1);
draw->setTextColor(c1); draw->setCursor(96,y+1); draw->printf("%-5s",v1);
draw->setTextColor(CGRY); draw->setCursor(126,y+1);draw->printf("%-12s",l2);
draw->setTextColor(c2); draw->setCursor(208,y+1);draw->print(v2);
y+=10;
};
snprintf(b1,20,"%d",stCount); snprintf(b2,20,"%d",act5);
iRow("Total stns",b1,"Active 5m",b2);
snprintf(b1,20,"%lu",newStations); snprintf(b2,20,"%lu",disappearedSt);
iRow("New",b1,"Gone>10m",b2,CGRN,CGRY);
snprintf(b1,20,"%.0f%%",rfR);
iRow("RF ratio",b1,"Net status",netH,rfR>50?CGRN:CYEL,nhC);
snprintf(b1,20,"%lu",totalSatPkts); snprintf(b2,20,"%d",satCount);
iRow("Sat pkts",b1,"Sat heard",b2,totalSatPkts?CCYN:CGRY);
snprintf(b1,20,"%d",totalAlerts); snprintf(b2,20,"%d",threatScore);
iRow("Alerts",b1,"Thr score",b2,totalAlerts?CYEL:CGRY,TCOL[threatLevel()]);
if(y < H-22){
hdiv(y); y+=2;
draw->setTextColor(CGRY); draw->setCursor(4,y+1);
draw->print("Most Active");
draw->setTextColor(CYEL); draw->setCursor(90,y+1);
draw->print(mostActive.substring(0,18));
y+=10;
}
if(y < H-12){
draw->setTextColor(CGRY); draw->setCursor(4,y+1);
draw->print("Uptime");
draw->setCursor(90,y+1);
draw->print(ut);
}
ftr("Intel refreshes every 60s");
}
// WIFI
void tickWiFi(){
switch(wfState){
case WF_IDLE: WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID,WIFI_PASS); wfState=WF_WAIT; wfTimer=millis(); break;
case WF_WAIT:
if(WiFi.status()==WL_CONNECTED){wifiOK=true;wfState=WF_DONE;configTime(0,0,"pool.ntp.org");}
else if(millis()-wfTimer>15000)wfState=WF_FAIL;
break;
default: break;
}
}
// APRS CONNECT
void connectAPRS(){
aprsOK=false;
if(!aprsClient.connect(APRS_HOST,APRS_PORT)) return;
String login="user "; login+=MY_CALL;
login+=" pass -1 vers APRSHawk 1.0 ";
login+=APRS_FILTER; login+="\r\n";
aprsClient.print(login);
aprsOK=true;
if(!bootTime) bootTime=millis();
lastKA=millis();
}
// SPLASH
void splash(const char* msg,uint8_t pct){
M5Cardputer.Display.fillScreen(CB);
M5Cardputer.Display.setTextColor(CGRN); M5Cardputer.Display.setTextSize(2);
M5Cardputer.Display.setCursor(4,15); M5Cardputer.Display.print("APRS");
M5Cardputer.Display.setTextColor(CCYN); M5Cardputer.Display.print(" HAWK");
M5Cardputer.Display.setTextSize(1); M5Cardputer.Display.setTextColor(CGRY);
M5Cardputer.Display.setCursor(4,45); M5Cardputer.Display.print("Packet Heuristic Analysis & Watch Kit");
M5Cardputer.Display.drawRect(4,62,232,8,CDKG);
if(pct) M5Cardputer.Display.fillRect(4,62,(232*pct)/100,8,CGRN);
M5Cardputer.Display.setTextColor(CWHT); M5Cardputer.Display.setCursor(4,76); M5Cardputer.Display.print(msg);
M5Cardputer.Display.setTextColor(CGRY);
M5Cardputer.Display.setCursor(4,92);
M5Cardputer.Display.printf("Battery: %d%%", battSmooth);
}
// KEYBOARD
void handleKeys(){
if(!M5Cardputer.Keyboard.isChange()||!M5Cardputer.Keyboard.isPressed()) return;
// backlight wakeup
lastActivity = millis();
if(!backlightOn){
M5Cardputer.Display.setBrightness(BACKLIGHT_FULL);
backlightOn = true;
return;
}
Keyboard_Class::KeysState ks=M5Cardputer.Keyboard.keysState();
char ch=0; for(auto k:ks.word) ch=k;
if(ch>='1'&&ch<='8') screen=ch-'1';
if(ch=='r'||ch=='R'){aprsClient.stop();aprsOK=false;aprsAttempted=false;}
if(ch=='s'||ch=='S'){
if(sdOK){
File f=SD.open("/stations.csv",FILE_WRITE);
if(f){
f.println("call,pkts,rf,inet,flood,jump,dualsrc,path,beacon");
for(int i=0;i<stCount;i++)
f.printf("%s,%d,%s,%s,%s,%s,%s,%s,%s\n",st[i].call,st[i].pktCount,
st[i].seenRF?"Y":"N",st[i].seenINET?"Y":"N",
st[i].flagFlood?"Y":"N",st[i].flagJump?"Y":"N",
st[i].flagDualSrc?"Y":"N",st[i].flagPath?"Y":"N",st[i].flagBeacon?"Y":"N");
f.close();
}
draw->fillScreen(CGRN);
draw->setTextColor(CB); draw->setTextSize(2); draw->setCursor(60,55);
draw->print("SAVED!"); draw->pushSprite(0,0); delay(300);
}
}
}
// SETUP
void setup(){
auto cfg=M5.config();
M5Cardputer.begin(cfg,true);
M5Cardputer.Display.setRotation(1);
M5Cardputer.Display.fillScreen(CB);
canvA.createSprite(W,H); canvA.setTextWrap(false);
canvB.createSprite(W,H); canvB.setTextWrap(false);
draw=&canvA; shown=&canvB;
// backlight init
M5Cardputer.Display.setBrightness(BACKLIGHT_FULL);
backlightOn = true;
lastActivity = millis();
// seed battery
uint8_t firstBatt = (uint8_t)M5.Power.getBatteryLevel();
for(int i=0;i<BATT_SAMPLES;i++) battBuf[i] = firstBatt;
battSmooth = firstBatt;
splash("Initialising...",5); delay(200);
sdSPI.begin(SD_SCK,SD_MISO,SD_MOSI,SD_CS);
sdOK=SD.begin(SD_CS,sdSPI);
splash(sdOK?"SD card ready":"No SD — logging disabled",25); delay(200);
splash("Starting WiFi...",45);
tickWiFi();
}
// LOOP
void loop(){
M5Cardputer.update();
handleKeys();
decayThreat();
// WiFi process
if(wfState==WF_WAIT){
tickWiFi();
static uint32_t st2=0;
if(millis()-st2>400){
st2=millis();
uint8_t p=45+min((uint32_t)35,(millis()-wfTimer)/400);
splash("Connecting to WiFi...",p);
}
return;
}
if(wfState==WF_FAIL&&!aprsAttempted){
splash("WiFi FAILED — check WIFI_SSID/PASS",0); delay(3000);
wfState=WF_IDLE; return;
}
if(wifiOK&&!aprsAttempted){
aprsAttempted=true;
splash("Connecting APRS-IS...",88);
connectAPRS();
splash(aprsOK?"Stream live — APRS HAWK online":"APRS connect failed",100);
delay(500);
}
// watchdogs
if(WiFi.status()!=WL_CONNECTED){wifiOK=false;wfState=WF_IDLE;tickWiFi();}
if(aprsAttempted&&!aprsClient.connected()){
aprsOK=false;
static uint32_t retryAt=0;
if(!retryAt)retryAt=millis()+5000;
if(millis()>retryAt){retryAt=0;connectAPRS();}
}
if(aprsOK&&millis()-lastKA>30000){aprsClient.print("#keepalive\r\n");lastKA=millis();}
// drain APRS stream
while(aprsClient.available()){
char c=aprsClient.read();
if(c=='\n'){
inputBuf.trim();
if(inputBuf.length()>5) processPacket(inputBuf);
inputBuf="";
} else if(c!='\r'&&inputBuf.length()<512){
inputBuf+=c;
}
}
if(millis()-lastIntelUpd>60000){updateIntel();lastIntelUpd=millis();}
// battery: poll every 10s
static uint32_t lastBattPoll=0;
if(millis()-lastBattPoll>10000){ updateBattery(); lastBattPoll=millis(); }
// backlight timeout
if(backlightOn && millis()-lastActivity > BACKLIGHT_TIMEOUT){
M5Cardputer.Display.setBrightness(BACKLIGHT_DIM);
backlightOn = false;
}
if(millis()-lastDraw>REFRESH_MS){
draw->setTextWrap(false);
draw->fillScreen(CB);
switch(screen){
case 0: drawSOC(); break;
case 1: drawThreatFeed(); break;
case 2: drawRFHealth(); break;
case 3: drawTopStations(); break;
case 4: drawAnalytics(); break;
case 5: drawSatellite(); break;
case 6: drawSecAlerts(); break;
case 7: drawIntel(); break;
}
draw->pushSprite(0,0);
M5Canvas* tmp=draw; draw=shown; shown=tmp;
lastDraw=millis();
}
}








Comments