Druk
Published © GPL3+

APRS Hawk

APRS HAWK turns raw ham radio packets into a live security intel dashboard that detects floods, position spoofing, and satellite passes.

AdvancedFull instructions provided1 hour18
APRS Hawk

Things used in this project

Hardware components

Cardputer-Adv
M5Stack Cardputer-Adv
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

sch_m5cardputer_sch_01_mcdlot3lhv_lNRuPVWc9z.png

Code

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
#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();
  }
}

Credits

Druk
5 projects • 1 follower

Comments