#include <tt_app.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
#include <tt_mdns.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <lwip/sockets.h>
#include <lwip/inet.h>

#define TAG "RobotArm"
#define ARM_PORT 80
#define ARM_PATH "/api/mcp"
#define NUM_JOINTS 6
#define SEQ_MAX 16

typedef struct {
    const char* name;
    const char* cute;
    uint32_t bg;
    uint32_t accent;
    int home, rmin, rmax;
    int value, last_sent;
} Joint;

static Joint joints[NUM_JOINTS] = {
    {"base","Base",0xFFD6E0,0xFF8FA8,70,0,180,70,-1},
    {"shoulder","Shldr",0xD6E8FF,0x8FB6FF,40,0,70,40,-1},
    {"elbow","Elbow",0xFFE8C5,0xFFB86A,20,0,120,20,-1},
    {"pitch","Pitch",0xD5F0D5,0x88D488,90,30,150,90,-1},
    {"roll","Roll",0xE8D5FF,0xB088FF,90,30,150,90,-1},
    {"gripper","Grip",0xFFF0B3,0xFFD060,90,0,180,90,-1},
};

typedef struct { int v[NUM_JOINTS]; } Frame;
static Frame seq[SEQ_MAX];
static int seq_len = 0, seq_idx = -1;
static bool seq_playing = false;
static int seq_play_pos = 0;

static char arm_host[64] = "";
static bool arm_connected = false;

typedef struct {
    AppHandle app;
    lv_obj_t* root;
    lv_obj_t* content; // scrollable column container
    lv_obj_t* status;
    lv_obj_t* sliders[6];
    lv_obj_t* vals[6];
    lv_obj_t* seq_label;
    lv_obj_t* play_label;
    lv_obj_t* blocks[SEQ_MAX];
    lv_obj_t* connect_box;
    lv_obj_t* main_box;
    lv_obj_t* connect_label;
    lv_obj_t* connect_spinner;
    int pend[6];
    bool has[6];
    int ticks[6];
    lv_timer_t* poll;
    lv_timer_t* seq_timer;
    lv_timer_t* connect_timer;
    int connect_attempts;
} Ctx;
static Ctx* g = NULL;

static uint16_t my_htons(uint16_t v){ return (v<<8)|(v>>8); }

static int http_post(const char* host,int port,const char* path,const char* body,char* out,size_t olen){
    uint32_t ip=ipaddr_addr(host);
    if(ip==0 || ip==0xFFFFFFFF) return -2;
    int fd=lwip_socket(AF_INET,SOCK_STREAM,0);
    if(fd<0) return -1;
    struct sockaddr_in s; memset(&s,0,sizeof(s));
    s.sin_family=AF_INET; s.sin_port=my_htons(port); s.sin_addr.s_addr=ip;
    struct timeval tv={2,0};
    lwip_setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));
    lwip_setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv));
    if(lwip_connect(fd,(struct sockaddr*)&s,sizeof(s))<0){close(fd);return -2;}
    char hdr[256]; int bl=strlen(body);
    int hl=snprintf(hdr,sizeof(hdr),"POST %s HTTP/1.1\r\nHost: %s:%d\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n",path,host,port,bl);
    if(lwip_send(fd,hdr,hl,0)<0){close(fd);return -3;}
    if(lwip_send(fd,body,bl,0)<0){close(fd);return -3;}
    int tot=0; while(tot<(int)olen-1){int r=lwip_recv(fd,out+tot,olen-1-tot,0); if(r<=0) break; tot+=r;}
    out[tot]='\0'; close(fd);
    char* bp=strstr(out,"\r\n\r\n"); if(bp){bp+=4; memmove(out,bp,strlen(bp)+1);}
    return tot>0?0:-4;
}

static bool jget(const char* js,const char* key,int* out){
    const char* p=strstr(js,key); if(!p) return false;
    p+=strlen(key); while(*p && *p!=':'){p++; if(!*p) return false;} p++;
    while(*p && (*p==' '||*p=='\t'||*p=='"'||*p=='\\')) p++;
    int sign=1; if(*p=='-'){sign=-1;p++;}
    float v=0,frac=0.1f; bool dot=false,got=false;
    while(*p){
        if(*p>='0'&&*p<='9'){got=true; if(!dot) v=v*10+(*p-'0'); else {v+=(*p-'0')*frac; frac*=0.1f;}}
        else if(*p=='.'&&!dot) dot=true; else break; p++;
    }
    if(!got) return false;
    *out=(int)(v*sign+0.5f); return true;
}
static bool parse_state(const char* r,int* b,int* s,int* e,int* p,int* ro,int* gr){
    int v; bool ok=true;
    if(jget(r,"base",&v)) *b=v; else ok=false;
    if(jget(r,"shoulder",&v)) *s=v; else ok=false;
    if(jget(r,"elbow",&v)) *e=v; else ok=false;
    if(jget(r,"pitch",&v)) *p=v; else ok=false;
    if(jget(r,"roll",&v)) *ro=v; else ok=false;
    if(jget(r,"gripper",&v)) *gr=v; else ok=false;
    return ok;
}
static bool rpc_get(int* b,int* s,int* e,int* p,int* ro,int* gr){
    const char* body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"get_arm_state\",\"arguments\":{}}}";
    char resp[2048]; memset(resp,0,sizeof(resp));
    if(http_post(arm_host,ARM_PORT,ARM_PATH,body,resp,sizeof(resp))!=0) return false;
    return parse_state(resp,b,s,e,p,ro,gr);
}
static bool rpc_move(const char* j,int a){
    char body[300]; snprintf(body,sizeof(body),
        "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"move_joint\",\"arguments\":{\"joint\":\"%s\",\"angle\":%d,\"duration\":0.5}}}",j,a);
    char r[1024]; memset(r,0,sizeof(r)); return http_post(arm_host,ARM_PORT,ARM_PATH,body,r,sizeof(r))==0;
}
static bool rpc_move_all(int b,int s,int e,int p,int ro,int gr,float dur){
    char body[420]; snprintf(body,sizeof(body),
        "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"move_all_joints\",\"arguments\":{\"base\":%d,\"shoulder\":%d,\"elbow\":%d,\"pitch\":%d,\"roll\":%d,\"gripper\":%d,\"duration\":%.1f}}}",
        b,s,e,p,ro,gr,dur);
    char r[1024]; memset(r,0,sizeof(r)); return http_post(arm_host,ARM_PORT,ARM_PATH,body,r,sizeof(r))==0;
}
static bool rpc_home(void){
    const char* b="{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"home_arm\",\"arguments\":{\"duration\":1.0}}}";
    char r[512]; memset(r,0,sizeof(r)); return http_post(arm_host,ARM_PORT,ARM_PATH,b,r,sizeof(r))==0;
}
static void set_status(const char* t){ if(g&&g->status) lv_label_set_text(g->status,t); }
static void apply_ui(void){
    if(!g) return;
    for(int i=0;i<NUM_JOINTS;i++){
        if(g->sliders[i]) lv_slider_set_value(g->sliders[i],joints[i].value,LV_ANIM_OFF);
        if(g->vals[i]){char buf[8]; snprintf(buf,sizeof(buf),"%d",joints[i].value); lv_label_set_text(g->vals[i],buf);}
    }
}
static void refresh_arm(void){
    set_status("Reading...");
    int b,s,e,p,ro,gr;
    if(rpc_get(&b,&s,&e,&p,&ro,&gr)){
        joints[0].value=b; joints[1].value=s; joints[2].value=e;
        joints[3].value=p; joints[4].value=ro; joints[5].value=gr;
        for(int i=0;i<NUM_JOINTS;i++){joints[i].last_sent=joints[i].value; if(g){g->pend[i]=joints[i].value; g->has[i]=false; g->ticks[i]=0;}}
        apply_ui();
        char buf[32]; snprintf(buf,sizeof(buf),"B%d S%d E%d",b,s,e); set_status(buf);
    } else set_status("No arm");
}
static void del_ref_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); }
static void init_cb(lv_timer_t* t){ lv_timer_delete(t); refresh_arm(); }
static void poll_cb(lv_timer_t* t){
    (void)t; if(!g || !arm_connected) return;
    for(int i=0;i<NUM_JOINTS;i++){
        if(!g->has[i]) continue;
        g->ticks[i]++; if(g->ticks[i]<3) continue;
        g->has[i]=false; g->ticks[i]=0;
        int tgt=g->pend[i]; if(joints[i].last_sent==tgt) continue;
        joints[i].last_sent=tgt; joints[i].value=tgt;
        char buf[24]; snprintf(buf,sizeof(buf),"%s %d",joints[i].cute,tgt); set_status(buf);
        bool ok=rpc_move(joints[i].name,tgt);
        snprintf(buf,sizeof(buf),"%s %d %s",joints[i].cute,tgt,ok?"o":"x"); set_status(buf);
        break;
    }
}
static void slider_cb(lv_event_t* e){
    int idx=(int)(intptr_t)lv_event_get_user_data(e);
    int v=(int)lv_slider_get_value(lv_event_get_target(e));
    joints[idx].value=v;
    if(g){
        if(g->vals[idx]){char b[8]; snprintf(b,sizeof(b),"%d",v); lv_label_set_text(g->vals[idx],b);}
        g->pend[idx]=v; g->has[idx]=true; g->ticks[idx]=0;
    }
}
static void update_seq_ui(void){
    if(!g) return;
    if(g->seq_label){
        if(seq_len==0) lv_label_set_text(g->seq_label,"empty");
        else {char b[16]; snprintf(b,sizeof(b),"%d/%d", (seq_idx>=0?seq_idx+1:seq_len), seq_len); lv_label_set_text(g->seq_label,b);}
    }
    for(int i=0;i<SEQ_MAX;i++){
        if(!g->blocks[i]) continue;
        if(i<seq_len){
            lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_COVER,0);
            if(i==seq_idx){
                lv_obj_set_style_border_width(g->blocks[i],3,0);
                lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0xFFFFFF),0);
            } else {
                lv_obj_set_style_border_width(g->blocks[i],2,0);
                lv_obj_set_style_border_color(g->blocks[i],lv_color_hex(0x3A3A3A),0);
            }
        } else {
            lv_obj_set_style_bg_opa(g->blocks[i],LV_OPA_30,0);
            lv_obj_set_style_border_width(g->blocks[i],0,0);
        }
    }
}
static void load_frame(int fidx){
    if(fidx<0||fidx>=seq_len) return;
    for(int i=0;i<NUM_JOINTS;i++){joints[i].value=seq[fidx].v[i]; joints[i].last_sent=joints[i].value; if(g){g->pend[i]=joints[i].value; g->has[i]=false;}}
    apply_ui(); seq_idx=fidx; update_seq_ui();
    char b[20]; snprintf(b,sizeof(b),"Frame %d",fidx+1); set_status(b);
}
static void seq_add_cb(void){ if(seq_len>=SEQ_MAX){set_status("Full"); return;} for(int i=0;i<NUM_JOINTS;i++) seq[seq_len].v[i]=joints[i].value; seq_len++; seq_idx=seq_len-1; update_seq_ui(); char b[16]; snprintf(b,sizeof(b),"+ %d",seq_len); set_status(b); }
static void seq_rem_cb(void){
    if(seq_len==0||seq_idx<0){set_status("Nothing"); return;}
    int rem=seq_idx; for(int f=rem;f<seq_len-1;f++) seq[f]=seq[f+1];
    seq_len--; if(seq_len==0){seq_idx=-1; update_seq_ui(); set_status("- empty"); return;}
    if(seq_idx>=seq_len) seq_idx=seq_len-1; load_frame(seq_idx);
}
static void seq_prev_cb(void){ if(seq_len==0) return; int n=(seq_idx<=0)?seq_len-1:seq_idx-1; load_frame(n); }
static void seq_next_cb(void){ if(seq_len==0) return; int n=(seq_idx>=seq_len-1)?0:seq_idx+1; load_frame(n); }
static void seq_timer_cb(lv_timer_t* t){
    (void)t; if(!seq_playing||seq_len==0) return;
    int f=seq_play_pos%seq_len; int* v=seq[f].v;
    if(!rpc_move_all(v[0],v[1],v[2],v[3],v[4],v[5],0.8f)){
        set_status("Seq fail"); seq_playing=false;
        if(g&&g->play_label) lv_label_set_text(g->play_label,"Play"); return;
    }
    for(int i=0;i<NUM_JOINTS;i++){joints[i].value=v[i]; joints[i].last_sent=v[i]; if(g){g->pend[i]=v[i]; g->has[i]=false;}}
    apply_ui(); seq_idx=f; update_seq_ui();
    char b[20]; snprintf(b,sizeof(b),"Play %d/%d",f+1,seq_len); set_status(b);
    seq_play_pos++; if(seq_play_pos>=seq_len) seq_play_pos=0;
}
static void seq_play_cb(lv_event_t* e){
    (void)e; if(seq_len==0){set_status("No frames"); return;}
    seq_playing=!seq_playing;
    if(g&&g->play_label) lv_label_set_text(g->play_label, seq_playing?"Pause":"Play");
    if(seq_playing){seq_play_pos=(seq_idx>=0?seq_idx:0); set_status("Playing");} else set_status("Paused");
}
static void seq_add_ev(lv_event_t* e){(void)e; seq_add_cb();}
static void seq_rem_ev(lv_event_t* e){(void)e; seq_rem_cb();}
static void seq_prev_ev(lv_event_t* e){(void)e; seq_prev_cb();}
static void seq_next_ev(lv_event_t* e){(void)e; seq_next_cb();}
static void block_click_cb(lv_event_t* e){int idx=(int)(intptr_t)lv_event_get_user_data(e); if(idx<0||idx>=seq_len) return; load_frame(idx);}
static void home_cb(lv_event_t* e){(void)e; set_status("Homing..."); if(rpc_home()){lv_timer_create(del_ref_cb,1200,NULL); set_status("Homed :3");} else set_status("Fail");}
static void read_cb(lv_event_t* e){(void)e; refresh_arm();}
static void open_cb(lv_event_t* e){(void)e; joints[5].value=0; apply_ui(); rpc_move("gripper",0); set_status("Open");}
static void close_cb(lv_event_t* e){(void)e; joints[5].value=180; apply_ui(); rpc_move("gripper",180); set_status("Close");}

static bool try_host(const char* host){
    char resp[1024]; memset(resp,0,sizeof(resp));
    if(http_post(host,ARM_PORT,ARM_PATH,"{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"tools/call\",\"params\":{\"name\":\"get_arm_state\",\"arguments\":{}}}",resp,sizeof(resp))==0){
        if(strstr(resp,"base")){
            strncpy(arm_host,host,sizeof(arm_host)-1);
            return true;
        }
    }
    return false;
}
static void show_main_ui(void){
    if(!g) return;
    arm_connected=true;
    if(g->connect_box) lv_obj_add_flag(g->connect_box, LV_OBJ_FLAG_HIDDEN);
    if(g->main_box) lv_obj_clear_flag(g->main_box, LV_OBJ_FLAG_HIDDEN);
    if(g->connect_timer){ lv_timer_delete(g->connect_timer); g->connect_timer=NULL; }
    if(!g->poll) g->poll=lv_timer_create(poll_cb,100,NULL);
    if(!g->seq_timer) g->seq_timer=lv_timer_create(seq_timer_cb,1300,NULL);
    lv_timer_create(init_cb,500,NULL);
    char buf[48]; snprintf(buf,sizeof(buf),"Conn %s",arm_host); set_status(buf);
}
static void connect_timer_cb(lv_timer_t* t){
    (void)t; if(!g || arm_connected) return;
    g->connect_attempts++;
    char lbl[64];
    if(g->connect_attempts==1) snprintf(lbl,sizeof(lbl),"mDNS browsing _robotarm._tcp...");
    else snprintf(lbl,sizeof(lbl),"Searching (%d)...",g->connect_attempts);
    if(g->connect_label) lv_label_set_text(g->connect_label,lbl);

    // ── 1) mDNS browse for _robotarm._tcp (preferred) ──
    TtMdnsBrowseResult res; memset(&res,0,sizeof(res));
    if(tt_mdns_browse("_robotarm","_tcp",2500,10,&res)){
        for(int i=0;i<res.count;i++){
            if(res.services[i].primaryAddress[0]){
                if(try_host(res.services[i].primaryAddress)){
                    show_main_ui(); return;
                }
            }
        }
    }
    // ── 2) mDNS resolve robotarm.local hostname ──
    char ipbuf[64]={0};
    if(tt_mdns_resolve_hostname("robotarm.local",2000,ipbuf) || tt_mdns_resolve_hostname("robotarm",2000,ipbuf)){
        if(try_host(ipbuf)){ show_main_ui(); return; }
    }
    // ── 3) fallback hardcoded (old behavior) ──
    const char* fallbacks[]={"192.168.68.148","192.168.68.103","192.168.68.102"};
    int idx = (g->connect_attempts-1) % (int)(sizeof(fallbacks)/sizeof(fallbacks[0]));
    if(try_host(fallbacks[idx])){ show_main_ui(); return; }

    if(g->connect_attempts>15){
        if(g->connect_label) lv_label_set_text(g->connect_label,"No arm found.\nMake sure robotarm\nis powered & on WiFi.\nTap Retry.");
    }
}
static void retry_cb(lv_event_t* e){(void)e; if(!g) return; g->connect_attempts=0; if(g->connect_label) lv_label_set_text(g->connect_label,"Retrying mDNS...");}

static void onShow(AppHandle app,void* data,lv_obj_t* parent){
    (void)data;
    Ctx* c=calloc(1,sizeof(Ctx)); if(!c) return;
    c->app=app; g=c;
    for(int i=0;i<NUM_JOINTS;i++){joints[i].last_sent=-1; c->pend[i]=joints[i].value; c->has[i]=false;}
    arm_host[0]='\0'; arm_connected=false; c->connect_attempts=0;

    lv_obj_t* tb=tt_lvgl_toolbar_create_for_app(parent,app); lv_obj_align(tb,LV_ALIGN_TOP_MID,0,0);

    // Root fills screen below toolbar - NOT scrollable itself, content will be
    lv_obj_t* root=lv_obj_create(parent);
    c->root=root;
    lv_obj_set_size(root,LV_PCT(100),LV_PCT(100));
    lv_obj_set_style_pad_all(root,2,0); lv_obj_set_style_pad_top(root,36,0);
    lv_obj_set_style_border_width(root,0,0);
    lv_obj_set_style_bg_color(root,lv_color_hex(0xFFF8F0),0);
    lv_obj_set_style_bg_opa(root,LV_OPA_COVER,0);
    lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE);

    // ── connecting screen (initial visible) ──
    c->connect_box=lv_obj_create(root);
    lv_obj_set_size(c->connect_box,LV_PCT(100),LV_PCT(100));
    lv_obj_set_pos(c->connect_box,0,0);
    lv_obj_set_style_border_width(c->connect_box,0,0);
    lv_obj_set_style_bg_opa(c->connect_box,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(c->connect_box,4,0);
    lv_obj_clear_flag(c->connect_box, LV_OBJ_FLAG_SCROLLABLE);

    lv_obj_t* tl=lv_label_create(c->connect_box);
    lv_label_set_text(tl,"Finding robotarm...");
    lv_obj_set_style_text_font(tl,lvgl_get_text_font(FONT_SIZE_LARGE),0);
    lv_obj_set_style_text_color(tl,lv_color_hex(0x3D2B5A),0);
    lv_obj_set_pos(tl,4,4);

    c->connect_label=lv_label_create(c->connect_box);
    lv_label_set_text(c->connect_label,"mDNS: _robotarm._tcp / robotarm.local\nFallback: 192.168.68.148\n\nSearching...");
    lv_obj_set_style_text_font(c->connect_label,lvgl_get_text_font(FONT_SIZE_SMALL),0);
    lv_obj_set_style_text_color(c->connect_label,lv_color_hex(0x6A5A7A),0);
    lv_obj_set_pos(c->connect_label,4,28); lv_obj_set_width(c->connect_label,260);

    c->connect_spinner=lv_spinner_create(c->connect_box);
    lv_obj_set_size(c->connect_spinner,40,40);
    lv_obj_set_pos(c->connect_spinner,120,110);

    lv_obj_t* rb=lv_btn_create(c->connect_box);
    lv_obj_set_size(rb,80,28); lv_obj_set_pos(rb,80,170);
    lv_obj_set_style_bg_color(rb,lv_color_hex(0xC5F5C5),0); lv_obj_set_style_radius(rb,8,0);
    lv_obj_t* rbl=lv_label_create(rb); lv_label_set_text(rbl,"Retry");
    lv_obj_set_style_text_font(rbl,lvgl_get_text_font(FONT_SIZE_SMALL),0);
    lv_obj_set_style_text_color(rbl,lv_color_hex(0x2A2A5A),0); lv_obj_center(rbl);
    lv_obj_add_event_cb(rb,retry_cb,LV_EVENT_CLICKED,NULL);

    // ── main scrollable content (hidden until connected) ──
    // content is the ONLY scrollable container - FIX overall app scroller lost
    c->content=lv_obj_create(root);
    lv_obj_set_size(c->content,LV_PCT(100),LV_PCT(100));
    lv_obj_set_pos(c->content,0,0);
    lv_obj_set_style_border_width(c->content,0,0);
    lv_obj_set_style_bg_opa(c->content,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(c->content,0,0);
    lv_obj_set_flex_flow(c->content, LV_FLEX_FLOW_COLUMN);
    lv_obj_add_flag(c->content, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_scrollbar_mode(c->content, LV_SCROLLBAR_MODE_AUTO);

    c->main_box=lv_obj_create(c->content);
    lv_obj_set_size(c->main_box,LV_PCT(100),LV_SIZE_CONTENT);
    lv_obj_set_style_border_width(c->main_box,0,0);
    lv_obj_set_style_bg_opa(c->main_box,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(c->main_box,0,0);
    lv_obj_clear_flag(c->main_box, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_add_flag(c->main_box, LV_OBJ_FLAG_HIDDEN);

    lv_obj_t* hdr=lv_obj_create(c->main_box); lv_obj_set_size(hdr,LV_PCT(100),16);
    lv_obj_set_style_border_width(hdr,0,0); lv_obj_set_style_bg_opa(hdr,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(hdr,0,0);
    lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_t* title=lv_label_create(hdr); lv_label_set_text(title,"Robot Arm :3");
    lv_obj_set_style_text_font(title,lvgl_get_text_font(FONT_SIZE_SMALL),0);
    lv_obj_set_style_text_color(title,lv_color_hex(0x3D2B5A),0);
    lv_obj_set_pos(title,2,0);
    c->status=lv_label_create(hdr); lv_label_set_text(c->status,"Conn...");
    lv_obj_set_style_text_font(c->status,lvgl_get_text_font(FONT_SIZE_SMALL),0);
    lv_obj_set_style_text_color(c->status,lv_color_hex(0xA08090),0);
    lv_obj_set_pos(c->status,90,0);

    lv_obj_t* brow=lv_obj_create(c->main_box); lv_obj_set_size(brow,LV_PCT(100),20);
    lv_obj_set_style_border_width(brow,0,0); lv_obj_set_style_bg_opa(brow,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(brow,0,0);
    lv_obj_clear_flag(brow, LV_OBJ_FLAG_SCROLLABLE);
    struct { const char* t; uint32_t col; lv_event_cb_t cb; } btns[]={
        {"Home",0xFFD6E0,home_cb},{"Read",0xD6E8FF,read_cb},{"Open",0xD5F0D5,open_cb},{"Close",0xFFE8C5,close_cb},};
    for(int i=0;i<4;i++){
        lv_obj_t* b=lv_btn_create(brow); lv_obj_set_size(b,56,18);
        lv_obj_set_style_bg_color(b,lv_color_hex(btns[i].col),0); lv_obj_set_style_radius(b,8,0);
        lv_obj_set_pos(b,i*62+2,0);
        lv_obj_t* l=lv_label_create(b); lv_label_set_text(l,btns[i].t);
        lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0);
        lv_obj_set_style_text_color(l,lv_color_hex(0x4A356A),0); lv_obj_center(l);
        lv_obj_add_event_cb(b,btns[i].cb,LV_EVENT_CLICKED,NULL);
    }

    lv_obj_t* grid=lv_obj_create(c->main_box);
    lv_obj_set_size(grid,LV_PCT(100),196);
    lv_obj_set_style_border_width(grid,0,0); lv_obj_set_style_bg_opa(grid,LV_OPA_TRANSP,0);
    lv_obj_set_style_pad_all(grid,2,0);
    lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_scrollbar_mode(grid, LV_SCROLLBAR_MODE_OFF);

    for(int i=0;i<NUM_JOINTS;i++){
        int col=i%3, row=i/3;
        lv_obj_t* card=lv_obj_create(grid);
        lv_obj_set_size(card,102,96);
        lv_obj_set_pos(card,col*104+2,row*98);
        lv_obj_set_style_bg_color(card,lv_color_hex(joints[i].bg),0);
        lv_obj_set_style_bg_opa(card,LV_OPA_90,0);
        lv_obj_set_style_radius(card,12,0);
        lv_obj_set_style_border_width(card,0,0);
        lv_obj_set_style_pad_all(card,3,0);
        lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
        lv_obj_set_scrollbar_mode(card, LV_SCROLLBAR_MODE_OFF);

        lv_obj_t* name=lv_label_create(card);
        lv_label_set_text(name,joints[i].cute);
        lv_obj_set_style_text_font(name,lvgl_get_text_font(FONT_SIZE_SMALL),0);
        lv_obj_set_style_text_color(name,lv_color_hex(0x4A356A),0); lv_obj_set_pos(name,2,0);

        c->vals[i]=lv_label_create(card);
        char vb[8]; snprintf(vb,sizeof(vb),"%d",joints[i].value);
        lv_label_set_text(c->vals[i],vb);
        lv_obj_set_style_text_font(c->vals[i],lvgl_get_text_font(FONT_SIZE_SMALL),0);
        lv_obj_set_style_text_color(c->vals[i],lv_color_hex(0xC04060),0); lv_obj_set_pos(c->vals[i],40,0);

        lv_obj_t* rlbl=lv_label_create(card);
        char rb[12]; snprintf(rb,sizeof(rb),"%d-%d",joints[i].rmin,joints[i].rmax);
        lv_label_set_text(rlbl,rb);
        lv_obj_set_style_text_font(rlbl,lvgl_get_text_font(FONT_SIZE_SMALL),0);
        lv_obj_set_style_text_color(rlbl,lv_color_hex(0xA090A0),0); lv_obj_set_pos(rlbl,58,0);

        lv_obj_t* sl=lv_slider_create(card);
        c->sliders[i]=sl;
        lv_obj_set_size(sl,22,72);
        lv_obj_set_pos(sl,40,20);
        lv_slider_set_range(sl,joints[i].rmin,joints[i].rmax);
        lv_slider_set_value(sl,joints[i].value,LV_ANIM_OFF);
        lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_MAIN);
        lv_obj_set_style_bg_opa(sl,LV_OPA_80,LV_PART_MAIN);
        lv_obj_set_style_radius(sl,10,LV_PART_MAIN);
        lv_obj_set_style_bg_color(sl,lv_color_hex(joints[i].accent),LV_PART_INDICATOR);
        lv_obj_set_style_radius(sl,10,LV_PART_INDICATOR);
        lv_obj_set_style_bg_color(sl,lv_color_hex(0xFFFFFF),LV_PART_KNOB);
        lv_obj_set_style_border_color(sl,lv_color_hex(joints[i].accent),LV_PART_KNOB);
        lv_obj_set_style_border_width(sl,2,LV_PART_KNOB);
        lv_obj_set_style_radius(sl,12,LV_PART_KNOB);
        lv_obj_set_style_pad_all(sl,3,LV_PART_KNOB);
        lv_obj_add_event_cb(sl,slider_cb,LV_EVENT_VALUE_CHANGED,(void*)(intptr_t)i);
    }

    lv_obj_t* seqbar=lv_obj_create(c->main_box);
    lv_obj_set_size(seqbar,316,96);
    lv_obj_set_style_bg_color(seqbar,lv_color_hex(0xF0E8FF),0);
    lv_obj_set_style_bg_opa(seqbar,LV_OPA_90,0);
    lv_obj_set_style_radius(seqbar,12,0);
    lv_obj_set_style_border_width(seqbar,1,0);
    lv_obj_set_style_border_color(seqbar,lv_color_hex(0xD0C0E0),0);
    lv_obj_set_style_pad_all(seqbar,4,0);
    lv_obj_clear_flag(seqbar, LV_OBJ_FLAG_SCROLLABLE);

    lv_obj_t* seq_t=lv_label_create(seqbar); lv_label_set_text(seq_t,"Seq");
    lv_obj_set_style_text_font(seq_t,lvgl_get_text_font(FONT_SIZE_SMALL),0);
    lv_obj_set_style_text_color(seq_t,lv_color_hex(0x3D2B5A),0); lv_obj_set_pos(seq_t,2,0);
    c->seq_label=lv_label_create(seqbar); lv_label_set_text(c->seq_label,"empty");
    lv_obj_set_style_text_font(c->seq_label,lvgl_get_text_font(FONT_SIZE_SMALL),0);
    lv_obj_set_style_text_color(c->seq_label,lv_color_hex(0x6A5A7A),0); lv_obj_set_pos(c->seq_label,30,0);
    struct { const char* t; uint32_t col; lv_event_cb_t cb; int play; } sbtns[]={
        {"-",0xFFB7B7,seq_rem_ev,0},{"<",0xD6E8FF,seq_prev_ev,0},
        {"Play",0xC5F5C5,seq_play_cb,1},{">",0xD6E8FF,seq_next_ev,0},{"+",0xFFE8A0,seq_add_ev,0},};
    for(int i=0;i<5;i++){
        lv_obj_t* b=lv_btn_create(seqbar);
        lv_obj_set_size(b,(i==2)?56:38,32);
        lv_obj_set_style_bg_color(b,lv_color_hex(sbtns[i].col),0);
        lv_obj_set_style_radius(b,8,0);
        int xs[5]={64,110,158,220,260};
        lv_obj_set_pos(b,xs[i],0);
        lv_obj_t* l=lv_label_create(b); if(sbtns[i].play) c->play_label=l;
        lv_label_set_text(l,sbtns[i].t);
        lv_obj_set_style_text_font(l,lvgl_get_text_font(FONT_SIZE_SMALL),0);
        lv_obj_set_style_text_color(l,lv_color_hex(0x2A2A5A),0); lv_obj_center(l);
        lv_obj_add_event_cb(b,sbtns[i].cb,LV_EVENT_CLICKED,NULL);
    }
    uint32_t blk_cols[16]={
        0xFF8FA8,0x8FB6FF,0xFFB86A,0x88D488,0xB088FF,0xFFD060,0xFF7AA2,0x7AC8FF,
        0xFDBA74,0x86EFAC,0xA78BFA,0xFDE68A,0xFCA5A5,0x93C5FD,0xBEF264,0xFDA4AF
    };
    for(int i=0;i<SEQ_MAX;i++){
        lv_obj_t* blk=lv_btn_create(seqbar);
        lv_obj_set_size(blk,22,22);
        lv_obj_set_pos(blk,(i%8)*24+2,(i/8)*24+38);
        lv_obj_set_style_bg_color(blk,lv_color_hex(blk_cols[i]),0);
        lv_obj_set_style_radius(blk,6,0);
        lv_obj_set_style_border_width(blk,0,0);
        lv_obj_set_style_bg_opa(blk,LV_OPA_30,0);
        c->blocks[i]=blk;
        lv_obj_add_event_cb(blk,block_click_cb,LV_EVENT_CLICKED,(void*)(intptr_t)i);
    }
    update_seq_ui();

    c->connect_timer=lv_timer_create(connect_timer_cb, 1000, NULL);
    connect_timer_cb(NULL);
}

static void onHide(AppHandle app,void* data){
    (void)app;(void)data;
    if(g){
        if(g->poll) lv_timer_delete(g->poll);
        if(g->seq_timer) lv_timer_delete(g->seq_timer);
        if(g->connect_timer) lv_timer_delete(g->connect_timer);
        free(g); g=NULL;
    }
    seq_playing=false; arm_connected=false;
}
int main(int argc,char* argv[]){(void)argc;(void)argv; tt_app_register((AppRegistration){.onShow=onShow,.onHide=onHide}); return 0;}
