/* SPDX-License-Identifier: GPL-3.0-or-later * Copyright (C) 2026 L8OLOOE and contributors. See CREDITS.md and NOTICE.md. */ #include "frame_overlay.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static json_object* field(json_object* object,const char* key){json_object* value=nullptr;if(object)json_object_object_get_ex(object,key,&value);return value;} static bool flag(json_object* object,const char* key){auto* value=field(object,key);return value&&json_object_get_type(value)==json_type_boolean&&json_object_get_boolean(value);} static std::string stringField(json_object* object,const char* key){auto* value=field(object,key);return value&&json_object_get_type(value)==json_type_string?json_object_get_string(value):"Unknown";} static volatile sig_atomic_t stop=0, show=0; static void signalHandler(int s){if(s==SIGUSR1)show=1;else stop=1;} static size_t receive(char* data,size_t a,size_t b,void* output){static_cast(output)->append(data,a*b);return a*b;} static std::string api(const std::string& route){ if(route=="/service/restart"){ int code=std::system("systemctl --user restart frame-pico-api.service"); if(code!=0)return "API restart failed. Check API service status."; return "API service restarted"; } CURL* c=curl_easy_init(); if(!c)return "API unavailable"; std::string response; curl_easy_setopt(c,CURLOPT_URL,("http://127.0.0.1:18741"+route).c_str()); curl_easy_setopt(c,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(c,CURLOPT_WRITEDATA,&response); curl_easy_setopt(c,CURLOPT_TIMEOUT,12L);curl_easy_setopt(c,CURLOPT_NOSIGNAL,1L); if(route!="/status")curl_easy_setopt(c,CURLOPT_POSTFIELDS,""); auto error=curl_easy_perform(c);long code=0;curl_easy_getinfo(c,CURLINFO_RESPONSE_CODE,&code);curl_easy_cleanup(c); if(error!=CURLE_OK)return std::string("Connection error: ")+curl_easy_strerror(error); if(code!=200&&code!=202)return "API error "+std::to_string(code)+": "+response; if(route=="/radio/start")return "Tracking station start requested. Check live connection status."; return response; } static void text(cairo_t* c,double x,double y,double size,const std::string& value){ cairo_set_font_size(c,size);cairo_move_to(c,x,y);cairo_show_text(c,value.c_str()); } int main(int argc,char** argv){ if(argc>1&&std::string(argv[1])=="--probe")return VrOverlay::probe(); int lock=open("/home/steamos/pico-ram-work/frame-pico-ui.lock",O_CREAT|O_RDWR,0600); if(lock<0)return 1; if(flock(lock,LOCK_EX|LOCK_NB)!=0){FILE* f=fopen("/home/steamos/pico-ram-work/frame-pico-ui.pid","r");int pid=0;if(f){if(fscanf(f,"%d",&pid)==1&&pid>1)kill(pid,SIGUSR1);fclose(f);}close(lock);return 0;} FILE* pf=fopen("/home/steamos/pico-ram-work/frame-pico-ui.pid","w");if(pf){fprintf(pf,"%d\n",getpid());fclose(pf);} signal(SIGINT,signalHandler);signal(SIGTERM,signalHandler);signal(SIGUSR1,signalHandler); curl_global_init(CURL_GLOBAL_DEFAULT); VrOverlay overlay;std::string error; if(overlay.connect(1200,700,error)!=VrOverlay::ConnectResult::Ok){fprintf(stderr,"%s\n",error.c_str());return 1;} std::vector rgba(1200*700*4),thumb(128*128*4,255); if(!overlay.submitThumbnail(thumb.data(),128)){fprintf(stderr,"Could not submit dashboard thumbnail\n");overlay.shutdown();curl_global_cleanup();close(lock);return 1;} overlay.showPanel(); auto* surface=cairo_image_surface_create(CAIRO_FORMAT_ARGB32,1200,700);auto* c=cairo_create(surface); std::string state="Connecting to diagnostic agent...",result=""; std::future pending=std::async(std::launch::async,api,std::string("/status")); bool recordRequest=false;auto refresh=std::chrono::steady_clock::now(); bool savedPreview=false,uiFailed=false; double mouseX=-1,mouseY=-1; std::string queuedRoute; std::vector updateRoutes; bool gameAvailable=false,serviceAvailable=false; auto hovered=[&](double x,double y,double w,double h){return mouseX>=x&&mouseX<=x+w&&mouseY>=y&&mouseY<=y+h;}; auto button=[&](double x,double y,double w,double h,bool close=false){ bool hover=hovered(x,y,w,h); cairo_set_source_rgb(c,close?(hover?.55:.30):(hover?.18:.10),close?.15:(hover?.62:.42),close?.17:(hover?.82:.60)); cairo_rectangle(c,x,y,w,h);cairo_fill(c); if(hover){cairo_set_source_rgb(c,.75,.95,1);cairo_set_line_width(c,2);cairo_rectangle(c,x+1,y+1,w-2,h-2);cairo_stroke(c);} }; while(!stop&&overlay.steamVrAlive()){ auto events=overlay.pollEvents();if(events.quit||events.closeRequested)break; if(show){show=0;overlay.showPanel();} if(pending.valid()&&pending.wait_for(std::chrono::milliseconds(0))==std::future_status::ready){ auto value=pending.get();if(recordRequest){result=value;recordRequest=false;}else state=value; } for(auto p:events.pointer){ if(p.type==PointerInput::Type::Leave){mouseX=mouseY=-1;continue;} mouseX=p.x;mouseY=p.y; if(p.type!=PointerInput::Type::Up)continue; if(hovered(40,180,270,80)&&gameAvailable)queuedRoute="/profiles/game"; if(hovered(330,180,270,80)&&serviceAvailable)queuedRoute="/profiles/service"; if(hovered(650,180,240,80))queuedRoute="/service/restart"; for(size_t i=0;istd::chrono::seconds(2)){pending=std::async(std::launch::async,api,std::string("/status"));refresh=now;} cairo_set_source_rgb(c,.04,.07,.10);cairo_paint(c);cairo_select_font_face(c,"Sans",CAIRO_FONT_SLANT_NORMAL,CAIRO_FONT_WEIGHT_NORMAL); cairo_set_source_rgb(c,.9,.95,1);text(c,40,65,42,"FramePicoTracker"); text(c,40,110,24,"Connected trackers"); auto* appStatus=json_tokener_parse(state.c_str()); auto* app=field(appStatus,"application");auto* profiles=field(app,"profiles"); gameAvailable=flag(field(profiles,"game"),"available");serviceAvailable=flag(field(profiles,"service"),"available"); button(40,180,270,80); cairo_set_source_rgb(c,gameAvailable?1:.5,gameAvailable?1:.5,gameAvailable?1:.5);text(c,60,230,23,"Game radio / mode 1"); button(330,180,270,80); cairo_set_source_rgb(c,serviceAvailable?1:.5,serviceAvailable?1:.5,serviceAvailable?1:.5);text(c,348,230,23,"Service / mode 0"); button(650,180,240,80); cairo_set_source_rgb(c,1,1,1);text(c,667,230,22,"Restart API"); auto* trackerRows=field(app,"trackers");updateRoutes.clear();int visible=0; if(trackerRows&&json_object_get_type(trackerRows)==json_type_array){ for(size_t i=0;i