feat(RobotArm): mDNS auto-connect, connecting screen, scroller fix
Main / Build (BookPlayer) (push) Has been cancelled
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
Main / Build (BookPlayer) (push) Has been cancelled
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
- 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
This commit is contained in:
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
#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;}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user