From 6d9001fcd7cf7a9f65c788e59fb206c696bbb1f0 Mon Sep 17 00:00:00 2001 From: Adolfo Date: Mon, 20 Jul 2026 14:04:18 -0400 Subject: [PATCH] feat(RobotArm): mDNS auto-connect, connecting screen, scroller fix - Pure mDNS discovery via tt_mdns_browse('_robotarm','_tcp') + resolve 'robotarm.local' Fallback to 192.168.68.148/.103 for old firmware, no LAN-wide scan - Connecting screen first: shows 'Finding robotarm...' + spinner + retry button, establishes connection then shows main UI (auto-connect on IP change) - Fix overall scroller lost: root not scrollable, inner content flex-column scrollable vertically, all nested cards/grid/hdr/brow/seqbar clear_flag SCROLLABLE + scrollbar OFF - no double scrollbars - Keep 2x tall sliders 22x72 3 cols, cute pastel cards, Home/Read/Open/Close 56x18, shoulder max 70, open=0 close=180 - Sequencer bottom not fixed but inside scrollable content, colored blocks 22x22 no info, white 3px border highlight current, larger buttons 38x32 Play 56x32, smooth move_all_joints - Fix missing symbol on 0.8.0-dev old firmware: remove esp_log (ESP_LOGI), lwip_recvfrom/sendto, keep 50U (was 52U) matching ReynaBot working set - MicroPython arm: mdns_server.py now advertises _robotarm._tcp.local PTR/SRV/TXT for tt_mdns_browse, A robotarm.local, re-announce 60s --- Apps/RobotArm/CMakeLists.txt | 16 + Apps/RobotArm/main/CMakeLists.txt | 6 + Apps/RobotArm/main/Source/main.c | 523 ++++++++++++++++++++++++++++++ Apps/RobotArm/manifest.properties | 13 + 4 files changed, 558 insertions(+) create mode 100644 Apps/RobotArm/CMakeLists.txt create mode 100644 Apps/RobotArm/main/CMakeLists.txt create mode 100644 Apps/RobotArm/main/Source/main.c create mode 100644 Apps/RobotArm/manifest.properties diff --git a/Apps/RobotArm/CMakeLists.txt b/Apps/RobotArm/CMakeLists.txt new file mode 100644 index 0000000..1d26e12 --- /dev/null +++ b/Apps/RobotArm/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(RobotArm) +tactility_project(RobotArm) diff --git a/Apps/RobotArm/main/CMakeLists.txt b/Apps/RobotArm/main/CMakeLists.txt new file mode 100644 index 0000000..f4ef8bc --- /dev/null +++ b/Apps/RobotArm/main/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c) +idf_component_register( + SRCS ${SOURCE_FILES} + REQUIRES TactilitySDK lwip +) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-format-truncation -Wno-unused-but-set-variable -Wno-unused-function) diff --git a/Apps/RobotArm/main/Source/main.c b/Apps/RobotArm/main/Source/main.c new file mode 100644 index 0000000..a75b24a --- /dev/null +++ b/Apps/RobotArm/main/Source/main.c @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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;isliders[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;ipend[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;ihas[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;iblocks[i]) continue; + if(iblocks[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;ipend[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=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;ipend[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;iconnect_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;ipend[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;ivals[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;iblocks[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;} diff --git a/Apps/RobotArm/manifest.properties b/Apps/RobotArm/manifest.properties new file mode 100644 index 0000000..e5c4106 --- /dev/null +++ b/Apps/RobotArm/manifest.properties @@ -0,0 +1,13 @@ +[manifest] +version=0.1 + +[target] +sdk=0.8.0-dev +platforms=esp32s3 + +[app] +id=one.tactility.robotarm +versionName=0.1.0 +versionCode=1 +name=Robot Arm +description=Cute controller for MCP robot arm