This project turns an ESP32 with a display into a smartphone-like emergency communication device. It provides local control buttons for sending SOS alerts in different categories (Medical, Fire, Accident, Other) and periodic check-in reminders, all communicating via USB to an approved gateway without storing network credentials or operational data on the device itself.
The guide includes a wiring diagram for the display connections, a complete parts list, the Arduino firmware with LVGL interface code, and step-by-step assembly instructions. After wiring and deploying the firmware, users will test the local controls and then connect an approved gateway to enable operational use.
Wiring diagram
Assemble it in 4 steps
1. Keep the supplied display connections
Use the built-in screen and touch panel without modifying their cables. No external components or wiring are required.
2. Connect USB and deploy
Connect the board programming USB-C port to your computer with a data cable. Press Deploy in Schematik. Keep USB power connected during use.
- This prototype is not Army-approved or suitable as a sole emergency communication device.
3. Test the local controls
Select Medical, Fire, Accident or Other. Hold SOS continuously for three seconds. Releasing early cancels. Without a gateway receipt the screen must not claim delivery. The check-in button resets a local 15-minute reminder; it does not notify a contact.
- There is no GPS, audible alarm, radio link, or automatic rescue dispatch. Logs and pending alerts are lost on restart.
4. Connect an approved gateway before operational use
Have your authorized service administrator implement the USB serial protocol in include/gateway_protocol.h using the board programming connection at 115200 baud. No real service or credentials are configured. Test with a non-emergency recipient and confirm receipt behavior before relying on the integration.
- Use established emergency channels first. Service acceptance is not confirmation that help was dispatched.
- Clearing an alert stops local retries only. It does not retract an alert already received by the service.
Deploy the firmware
#include <Arduino.h>
#include <lvgl.h>
#include "schematik_display.h"
// USB gateway protocol; no network credentials or operational data are stored.
static lv_obj_t *linkLabel, *feedLabel, *sosLabel, *checkLabel, *logLabel, *category;
static const char *categories[] = {"Medical", "Fire", "Accident", "Other"};
static uint32_t heartbeat=0, holdStart=0, checkStart=0, sentAt=0, lastRetry=0;
static uint32_t sequence=0, activeId=0;
static bool seenGateway=false, holding=false, pending=false, checkDue=false;
static int selected=0, lastLink=-1;
static String logText, input;
static uint32_t feedAt=0;
static bool hasFeed=false, staleShown=false;
static void logEvent(const String &s) {
logText=String(millis()/1000)+"s: "+s+"\n"+logText;
if(logText.length()>650) logText.remove(650);
lv_label_set_text(logLabel,logText.c_str());
}
static bool online() { return seenGateway && uint32_t(millis()-heartbeat)<15000; }
static void emitSos() {
Serial.printf("SOS|%lu|%s\n",(unsigned long)activeId,categories[selected]);
lastRetry=millis();
}
static void action(lv_event_t *e) {
intptr_t id=(intptr_t)lv_event_get_user_data(e);
if(id==1) {
if(pending || holding) return;
selected=(selected+1)%4;
lv_label_set_text_fmt(category,"Category: %s",categories[selected]);
} else if(id==2) {
checkStart=millis(); checkDue=false;
lv_label_set_text(checkLabel,"Check-in: next reminder in 15 minutes");
Serial.println("CHECKIN|LOCAL");
logEvent("Local check-in (not a delivery receipt)");
} else if(id==3 && pending) {
Serial.printf("CANCEL|%lu\n",(unsigned long)activeId);
pending=false; holding=false;
lv_label_set_text(sosLabel,"Local alert cleared; remote cancellation NOT confirmed.");
logEvent("Stopped retries; contact recipient to cancel");
}
}
static void hold(lv_event_t *e) {
lv_event_code_t code=lv_event_get_code(e);
if(code==LV_EVENT_PRESSED && !pending) { holding=true; holdStart=millis(); lv_label_set_text(sosLabel,"Keep holding for 3 seconds..."); }
if((code==LV_EVENT_RELEASED || code==LV_EVENT_PRESS_LOST) && holding) {
holding=false; lv_label_set_text(sosLabel,"Hold cancelled. No SOS sent.");
}
}
static lv_obj_t *label(lv_obj_t *parent,const char *text,int width) {
lv_obj_t *o=lv_label_create(parent); lv_label_set_text(o,text);
lv_obj_set_width(o,width); lv_label_set_long_mode(o,LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_color(o,lv_color_hex(0xEEF3EF),0); return o;
}
static lv_obj_t *button(lv_obj_t *parent,const char *text,uint32_t color,intptr_t id) {
lv_obj_t *o=lv_btn_create(parent); lv_obj_set_size(o,420,48);
lv_obj_set_style_bg_color(o,lv_color_hex(color),0);
lv_obj_t *t=label(o,text,390); lv_obj_set_style_text_align(t,LV_TEXT_ALIGN_CENTER,0); lv_obj_center(t);
if(id) lv_obj_add_event_cb(o,action,LV_EVENT_CLICKED,(void*)id);
return o;
}
static void build_interface() {
lv_obj_t *s=lv_scr_act();
lv_obj_set_style_bg_color(s,lv_color_hex(0x101E19),0);
lv_obj_set_style_bg_opa(s,LV_OPA_COVER,0);
lv_obj_set_style_text_font(s,&lv_font_montserrat_16,0);
lv_obj_set_flex_flow(s,LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(s,LV_FLEX_ALIGN_START,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(s,16,0); lv_obj_set_style_pad_row(s,10,0);
label(s,"SAFETY DASHBOARD | PROTOTYPE",430);
label(s,"Not Army-approved. Not a rescue service.\nUse established emergency channels first.",430);
linkLabel=label(s,"Gateway offline | no confirmed delivery",430);
feedLabel=label(s,"UPDATES: No verified feed received.\nNo live news or operational information configured.",430);
category=label(s,"Category: Medical",430);
button(s,"Change emergency category",0x365747,1);
lv_obj_t *sos=button(s,"HOLD 3 SECONDS FOR SOS",0xA32632,0);
lv_obj_add_event_cb(sos,hold,LV_EVENT_ALL,nullptr);
sosLabel=label(s,"SOS idle. USB gateway required for delivery.",430);
button(s,"Stop retries / clear local alert",0x555B61,3);
checkLabel=label(s,"Check-in: next reminder in 15 minutes",430);
button(s,"I am safe - local check-in",0x28604B,2);
label(s,"Recent events (volatile; cleared on restart)",430);
logLabel=label(s,"Ready. No location tracking. No audio alarm.",430);
checkStart=millis();
}
static void receiveLine(const String &line) {
if(line=="HEARTBEAT") {heartbeat=millis(); seenGateway=true;}
else if(line.startsWith("UPDATE|")) {
if(!online()) return;
String text=line.substring(7); text.trim();
if(text.length()) {lv_label_set_text_fmt(feedLabel,"GATEWAY UPDATE (not independently verified):\n%s",text.c_str()); feedAt=millis(); hasFeed=true; staleShown=false; logEvent("Update received");}
} else if(line.startsWith("ACK|")) {
// Exact receipt ID only; this means service acceptance, NOT rescue dispatch.
if(pending && online() && line==String("ACK|")+String(activeId)) {
pending=false;
lv_label_set_text(sosLabel,"Service receipt received. NOT proof of rescue dispatch.");
logEvent("Service acknowledged SOS #"+String(activeId));
}
}
}
void setup() {
Serial.begin(115200);
sequence=esp_random();
schematikDisplayBegin();
if(schematikDisplayLock()) {build_interface(); schematikDisplayUnlock();}
}
void loop() {
if(schematikDisplayLock()) {
unsigned count=0;
while(Serial.available() && count++<128) {
char c=Serial.read();
if(c=='\n') {receiveLine(input); input="";}
else if(c!='\r' && c>=32 && c<=126) {
if(input.length()<240) input+=c;
else { input=""; while(Serial.available()) Serial.read(); break; }
}
}
bool connected=online();
if(lastLink!=(int)connected) {lastLink=connected; lv_label_set_text(linkLabel,connected?"Gateway connected | receipt needed for each SOS":"Gateway offline | no confirmed delivery");}
if(holding && uint32_t(millis()-holdStart)>=3000) {
holding=false; pending=true; activeId=++sequence; sentAt=millis();
lv_label_set_text(sosLabel,"SOS pending. NOT delivered until service receipt.\nUse established emergency channels now.");
logEvent("SOS #"+String(activeId)+" pending: "+categories[selected]);
emitSos();
}
if(pending && uint32_t(millis()-lastRetry)>=10000 && uint32_t(millis()-sentAt)<120000) emitSos();
if(pending && uint32_t(millis()-sentAt)>=120000) {
pending=false; lv_label_set_text(sosLabel,"NO RECEIPT after 2 minutes. Delivery unknown.\nUse established emergency channels immediately."); logEvent("SOS receipt timeout");
}
if(!checkDue && uint32_t(millis()-checkStart)>=900000) {
checkDue=true; lv_label_set_text(checkLabel,"CHECK-IN OVERDUE: tap I am safe. Local reminder only."); logEvent("Check-in overdue (no automatic SOS)");
}
if(hasFeed && !staleShown && uint32_t(millis()-feedAt)>=300000) {
staleShown=true; lv_label_set_text(feedLabel,"UPDATE STALE: no new update in 5 minutes.\nConsult your approved information channel.");
}
schematikDisplayUnlock();
}
delay(10);
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




