6 Commits

Author SHA1 Message Date
Adolfo 2795dddc12 feat(RobotArm): cute MCP arm controller with 2x tall sliders, sequencer colored blocks, smooth move_all_joints
- 6 joints vertical sliders 22x72 (2x tall), 3 cols, pastel cute cards, home-centered ranges shoulder max 70
- open/close inverted fix (0=open 180=close)
- sequencer bottom not fixed, colored blocks 22x22 no info, current highlight white 3px, larger buttons 38x32 Play 56x32
- smooth concurrent moves via move_all_joints tool (tools/list discovered) not one-by-one
- scroll fixes: clearflag SCROLLABLE on cards/grid/hdr/brow/seqbar, root LV_DIR_VER only (no horiz scroll)
- safe for device .129: custom my_htons avoids missing lwip_htons, 52U 0 missing symbols, 14K ELF
- live reading from 192.168.68.103/api/mcp get_arm_state + move_joint
- enjoyed by son :3
2026-07-17 11:19:07 -04:00
Ken Van Hoeylandt 8721a653e7 Tool 4.1.0 2026-07-03 23:17:09 +02:00
Ken Van Hoeylandt 694b68de1a Fixes for app publishing (#35) 2026-07-03 22:30:14 +02:00
Ken Van Hoeylandt 71bf2631f4 Implement CDN uploading (#34) 2026-07-03 21:41:18 +02:00
Ken Van Hoeylandt 4ab2377970 Manifest format update (#33) 2026-07-03 00:04:29 +02:00
Ken Van Hoeylandt 8a0f9ef4e4 SDK 0.7.0 & bump app versions (#32) 2026-07-02 19:31:34 +02:00
27 changed files with 791 additions and 256 deletions
+21
View File
@@ -0,0 +1,21 @@
name: Publish Apps
inputs:
sdk_version:
description: The SDK version that determines the path on the CDN
required: true
runs:
using: 'composite'
steps:
- name: 'Download cdn-files'
uses: actions/download-artifact@v4
with:
name: 'cdn-files'
path: cdn_files
- name: 'Install boto3'
shell: bash
run: pip install boto3
- name: 'Upload files'
shell: bash
run: python Buildscripts/CDN/upload-app-files.py cdn_files ${{ inputs.sdk_version }} ${{ env.CDN_ID }} ${{ env.CDN_TOKEN_NAME }} ${{ env.CDN_TOKEN_VALUE }}
+9 -1
View File
@@ -1,5 +1,10 @@
name: Release Apps name: Release Apps
outputs:
sdk_version:
description: 'Common SDK version shared by all bundled apps'
value: ${{ steps.release.outputs.sdk_version }}
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
@@ -11,8 +16,11 @@ runs:
run: rsync -av downloaded_apps/*/*.app cdn_files/ run: rsync -av downloaded_apps/*/*.app cdn_files/
shell: bash shell: bash
- name: 'Create CDN release files' - name: 'Create CDN release files'
run: python release.py cdn_files/ id: release
shell: bash shell: bash
run: |
python release.py cdn_files/
echo "sdk_version=$(cat sdk_version.txt)" >> "$GITHUB_OUTPUT"
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
+21
View File
@@ -16,6 +16,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Build" - name: "Build"
uses: ./.github/actions/build-app uses: ./.github/actions/build-app
with: with:
@@ -23,7 +25,26 @@ jobs:
Bundle: Bundle:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [Build] needs: [Build]
outputs:
sdk_version: ${{ steps.release.outputs.sdk_version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Build" - name: "Build"
id: release
uses: ./.github/actions/release-apps uses: ./.github/actions/release-apps
PublishApps:
runs-on: ubuntu-latest
needs: [Bundle]
if: (github.event_name == 'push' && github.ref == 'refs/heads/main')
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: "Publish Apps"
env:
CDN_ID: ${{ secrets.CDN_ID }}
CDN_TOKEN_NAME: ${{ secrets.CDN_TOKEN_NAME }}
CDN_TOKEN_VALUE: ${{ secrets.CDN_TOKEN_VALUE }}
uses: ./.github/actions/publish-apps
with:
sdk_version: ${{ needs.Bundle.outputs.sdk_version }}
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.brainfuck
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.brainfuck app.name=Brainfuck interpreter
versionName=0.2.0 app.description=Brainfuck esoteric language interpreter
versionCode=2
name=Brainfuck interpreter
description=Brainfuck esoteric language interpreter
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.breakout
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.breakout app.name=Breakout
versionName=0.2.0 app.description=Classic brick-breaking arcade game
versionCode=2
name=Breakout
description=Classic brick-breaking arcade game
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.calculator
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.calculator app.name=Calculator
versionName=0.3.0
versionCode=3
name=Calculator
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.diceware
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.diceware app.name=Diceware
versionName=0.3.0
versionCode=3
name=Diceware
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32s3,esp32p4
sdk=0.7.0-dev app.id=one.tactility.epubreader
platforms=esp32s3,esp32p4 app.version.name=0.4.0
[app] app.version.code=4
id=one.tactility.epubreader app.name=Epub Reader
versionName=0.1.0 app.description=Epub and text file reader. Requires PSRAM!
versionCode=1
name=Epub Reader
description=Epub and text file reader. Requires PSRAM!
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.gpio
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.gpio app.name=GPIO
versionName=0.4.0
versionCode=4
name=GPIO
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.graphicsdemo
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.graphicsdemo app.name=Graphics Demo
versionName=0.3.0
versionCode=3
name=Graphics Demo
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.helloworld
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.helloworld app.name=Hello World
versionName=0.3.0
versionCode=3
name=Hello World
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32s3,esp32p4
sdk=0.7.0-dev app.id=one.tactility.m5unittest
platforms=esp32s3,esp32p4 app.version.name=0.4.0
[app] app.version.code=4
id=one.tactility.m5unittest app.name=M5 Unit Test
versionName=0.1.0
versionCode=1
name=M5 Unit Test
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.magic8ball
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.magic8ball app.name=Magic 8-Ball
versionName=0.2.0
versionCode=2
name=Magic 8-Ball
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32s3,esp32p4
sdk=0.7.0-dev app.id=one.tactility.mediakeys
platforms=esp32s3,esp32p4 app.version.name=0.4.0
[app] app.version.code=4
id=one.tactility.mediakeys app.name=Media Keys
versionName=0.1.0 app.description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
versionCode=1
name=Media Keys
description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus.
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.mystifydemo
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.6.0
[app] app.version.code=6
id=one.tactility.mystifydemo app.name=Mystify Demo
versionName=0.3.0
versionCode=3
name=Mystify Demo
+16
View File
@@ -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)
+6
View File
@@ -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)
+429
View File
@@ -0,0 +1,429 @@
#include <tt_app.h>
#include <tt_lvgl_toolbar.h>
#include <tactility/lvgl_fonts.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <lwip/sockets.h>
#include <lwip/inet.h>
#include "esp_log.h"
#define TAG "RobotArm"
#define ARM_HOST "192.168.68.103"
#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;
typedef struct {
AppHandle app;
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];
int pend[6];
bool has[6];
int ticks[6];
lv_timer_t* poll;
lv_timer_t* seq_timer;
} 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){
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=ipaddr_addr(host);
struct timeval tv={5,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));
int rc=http_post(ARM_HOST,ARM_PORT,ARM_PATH,body,r,sizeof(r));
ESP_LOGI(TAG,"move_all %d %d %d rc=%d",b,s,e,rc);
return rc==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 .103...");
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 .103");
}
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) 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[20]; 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?"ok":"fail"); 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;
}
}
/* ── sequencer ── */
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("Seq 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 void onShow(AppHandle app,void* data,lv_obj_t* parent){
(void)data;
Ctx* c=(Ctx*)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;}
lv_obj_t* tb=tt_lvgl_toolbar_create_for_app(parent,app); lv_obj_align(tb,LV_ALIGN_TOP_MID,0,0);
lv_obj_t* root=lv_obj_create(parent);
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,34,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_set_scroll_dir(root, LV_DIR_VER);
lv_obj_t* hdr=lv_obj_create(root); 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_set_pos(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 .103...");
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(root); 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_set_pos(brow,0,16);
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);
}
/* ── large nice vertical sliders: 3 cols, 2x height 22x72 per request ── */
lv_obj_t* grid=lv_obj_create(root);
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_set_pos(grid,0,36);
lv_obj_clear_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
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_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);
}
/* ── sequencer at bottom, NOT fixed — scrollable with content ──
visual: colored blocks, no info, current highlighted, larger buttons */
lv_obj_t* seqbar=lv_obj_create(root);
lv_obj_set_size(seqbar,316,96);
lv_obj_set_pos(seqbar,2,232);
lv_obj_clear_flag(seqbar, LV_OBJ_FLAG_SCROLLABLE);
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_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->poll=lv_timer_create(poll_cb,100,NULL);
c->seq_timer=lv_timer_create(seq_timer_cb,1300,NULL);
lv_timer_create(init_cb,600,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);
free(g); g=NULL;
}
seq_playing=false;
}
int main(int argc,char* argv[]){(void)argc;(void)argv; tt_app_register((AppRegistration){.onShow=onShow,.onHide=onHide}); return 0;}
+13
View File
@@ -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
+7 -10
View File
@@ -1,10 +1,7 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.serialconsole
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.serialconsole app.name=Serial Console
versionName=0.4.0
versionCode=4
name=Serial Console
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.snake
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.8.0
[app] app.version.code=8
id=one.tactility.snake app.name=Snake
versionName=0.5.0 app.description=Classic Snake game
versionCode=5
name=Snake
description=Classic Snake game
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.tamatac
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.4.0
[app] app.version.code=4
id=one.tactility.tamatac app.name=TamaTac
versionName=0.1.0 app.description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
versionCode=1
name=TamaTac
description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.todolist
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.5.0
[app] app.version.code=5
id=one.tactility.todolist app.name=Todo List
versionName=0.2.0 app.description=Simple task list manager
versionCode=2
name=Todo List
description=Simple task list manager
+8 -11
View File
@@ -1,11 +1,8 @@
[manifest] manifest.version=0.2
version=0.1 target.sdk=0.8.0-dev
[target] target.platforms=esp32,esp32s3,esp32c6,esp32p4
sdk=0.7.0-dev app.id=one.tactility.twoeleven
platforms=esp32,esp32s3,esp32c6,esp32p4 app.version.name=0.7.0
[app] app.version.code=7
id=one.tactility.twoeleven app.name=2048
versionName=0.4.0 app.description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert).
versionCode=4
name=2048
description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert).
+70
View File
@@ -0,0 +1,70 @@
import os
import sys
import boto3
SHELL_COLOR_RED = "\033[91m"
SHELL_COLOR_ORANGE = "\033[93m"
SHELL_COLOR_RESET = "\033[m"
def print_warning(message):
print(f"{SHELL_COLOR_ORANGE}WARNING: {message}{SHELL_COLOR_RESET}")
def print_error(message):
print(f"{SHELL_COLOR_RED}ERROR: {message}{SHELL_COLOR_RESET}")
def print_help():
print("Usage: python upload-app-files.py [path] [sdkVersion] [cloudflareAccountId] [cloudflareTokenName] [cloudflareTokenValue]")
print("")
print("Options:")
print(" --index-only Upload only apps.json")
def exit_with_error(message):
print_error(message)
sys.exit(1)
def main(path: str, sdk_version: str, cloudflare_account_id, cloudflare_token_name: str, cloudflare_token_value: str, index_only: bool):
if not os.path.exists(path):
exit_with_error(f"Path not found: {path}")
s3 = boto3.client(
service_name="s3",
endpoint_url=f"https://{cloudflare_account_id}.r2.cloudflarestorage.com",
aws_access_key_id=cloudflare_token_name,
aws_secret_access_key=cloudflare_token_value,
region_name="auto"
)
files_to_upload = os.listdir(path)
if index_only:
files_to_upload = [f for f in files_to_upload if f == 'apps.json']
else:
# Ensure apps.json is uploaded last so it never references files that
# haven't finished uploading yet.
files_to_upload.sort(key=lambda f: f == 'apps.json')
counter = 1
total = len(files_to_upload)
for file_name in files_to_upload:
object_path = f"apps/{sdk_version}/{file_name}"
print(f"[{counter}/{total}] Uploading {file_name} to {object_path}")
file_path = os.path.join(path, file_name)
try:
s3.upload_file(file_path, "tactility", object_path)
except Exception as e:
exit_with_error(f"Failed to upload {file_name}: {str(e)}")
counter += 1
if __name__ == "__main__":
print("Tactility CDN Apps Uploader")
if "--help" in sys.argv:
print_help()
sys.exit()
# Argument validation
if len(sys.argv) < 6:
print_help()
sys.exit(1)
main(
path=sys.argv[1],
sdk_version=sys.argv[2],
cloudflare_account_id=sys.argv[3],
cloudflare_token_name=sys.argv[4],
cloudflare_token_value=sys.argv[5],
index_only="--index-only" in sys.argv
)
+56 -41
View File
@@ -1,14 +1,23 @@
import json import json
import subprocess
import tarfile import tarfile
import os import os
import tempfile import tempfile
import configparser
import sys import sys
from datetime import datetime, UTC
def read_properties_file(path): def read_properties_file(path):
config = configparser.RawConfigParser() properties = {}
config.read(path) with open(path, "r") as file:
return config for line in file:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
key, sep, value = line.partition("=")
if not sep:
continue
properties[key.strip()] = value.strip()
return properties
def get_manifest(appPath): def get_manifest(appPath):
"""Extract only the file named 'manifest.properties' from the given tar/tar.gz """Extract only the file named 'manifest.properties' from the given tar/tar.gz
@@ -62,51 +71,49 @@ def get_manifest(appPath):
return None return None
def get_versioned_file_name(manifest): def get_versioned_file_name(manifest):
app_id = manifest["app"]["id"] app_id = manifest["app.id"]
version_code = manifest["app"]["versionCode"] version_code = manifest["app.version.code"]
return f"{app_id}-{version_code}.app" return f"{app_id}-{version_code}.app"
def get_os_version(manifest): def get_os_version(manifest):
sdk = manifest["target"]["sdk"] sdk = manifest["target.sdk"]
# Remove trailing hyphen suffix if present # Remove trailing hyphen suffix if present
if "-" in sdk: if "-" in sdk:
return sdk.rsplit("-", 1)[0].strip() return sdk.rsplit("-", 1)[0].strip()
else: else:
return sdk return sdk
def manifest_config_to_flat_json(manifest): def check_and_get_sdk_version(manifest_map):
"""Convert a ConfigParser manifest into a flat JSON-like dict. """Ensure all apps target the same (simplified) SDK version and return it."""
versions = {get_os_version(manifest) for manifest in manifest_map.values()}
if len(versions) != 1:
print(f"ERROR: Apps target multiple SDK versions: {sorted(versions)}. All apps must target the same SDK version.")
sys.exit(1)
return next(iter(versions))
Expected sections/keys (case-insensitive for keys): def get_git_commit_hash():
- [app] return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
id -> appId
versionName -> appVersionName def manifest_config_to_flat_json(manifest):
versionCode -> appVersionCode (int) """Convert a flat (V2) manifest dict into a flat JSON-like dict.
name -> appName
description -> appDescription (optional; default "") Expected keys:
- [target] app.id -> appId
sdk -> targetSdk app.version.name -> appVersionName
platforms -> targetPlatforms (comma-separated list) app.version.code -> appVersionCode (int)
app.name -> appName
app.description -> appDescription (optional; default "")
target.sdk -> targetSdk
target.platforms -> targetPlatforms (comma-separated list)
Unknown/missing values fall back to sensible defaults per requirements. Unknown/missing values fall back to sensible defaults per requirements.
""" """
def get_opt(section, option, default=None):
if not manifest.has_section(section):
return default
# try exact option then lowercase (RawConfigParser lowercases by default)
if manifest.has_option(section, option):
return manifest.get(section, option)
low = option.lower()
if manifest.has_option(section, low):
return manifest.get(section, low)
return default
# Map values # Map values
app_id = get_opt("app", "id", "") app_id = manifest.get("app.id", "")
app_version_name = get_opt("app", "versionName", "") app_version_name = manifest.get("app.version.name", "")
app_version_code_raw = get_opt("app", "versionCode", "0") app_version_code_raw = manifest.get("app.version.code", "0")
app_name = get_opt("app", "name", "") app_name = manifest.get("app.name", "")
app_description = get_opt("app", "description", "") or "" app_description = manifest.get("app.description", "") or ""
# Coerce version code to int safely # Coerce version code to int safely
try: try:
@@ -114,8 +121,8 @@ def manifest_config_to_flat_json(manifest):
except Exception: except Exception:
app_version_code = 0 app_version_code = 0
target_sdk = get_opt("target", "sdk", "") target_sdk = manifest.get("target.sdk", "")
platforms_raw = get_opt("target", "platforms", "") platforms_raw = manifest.get("target.platforms", "")
target_platforms = [p.strip() for p in str(platforms_raw).split(",") if p.strip()] if platforms_raw is not None else [] target_platforms = [p.strip() for p in str(platforms_raw).split(",") if p.strip()] if platforms_raw is not None else []
filename = get_versioned_file_name(manifest) filename = get_versioned_file_name(manifest)
@@ -142,15 +149,23 @@ if __name__ == "__main__":
sys.exit() sys.exit()
app_directory = sys.argv[1] app_directory = sys.argv[1]
manifest_map = {} manifest_map = {}
output_json = {
"apps": []
}
any_manifest = None any_manifest = None
if os.path.exists(app_directory): if os.path.exists(app_directory):
for file in os.listdir(app_directory): for file in os.listdir(app_directory):
if file.endswith(".app"): if file.endswith(".app"):
file_path = os.path.join(app_directory, file) file_path = os.path.join(app_directory, file)
manifest_map[file_path] = get_manifest(file_path) manifest_map[file_path] = get_manifest(file_path)
# All bundled apps must target the same SDK version; this becomes the CDN path segment
sdk_version = check_and_get_sdk_version(manifest_map)
with open("sdk_version.txt", "w") as f:
f.write(sdk_version)
print(f"SDK version: {sdk_version}")
output_json = {
"sdkVersion": sdk_version,
"created": datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'),
"gitCommit": get_git_commit_hash(),
"apps": []
}
# Rename files and collect manifest data into output json object # Rename files and collect manifest data into output json object
for file_path in manifest_map.keys(): for file_path in manifest_map.keys():
print(f"Processing {file_path}: {manifest_map[file_path]}") print(f"Processing {file_path}: {manifest_map[file_path]}")
@@ -164,4 +179,4 @@ if __name__ == "__main__":
any_manifest = manifest any_manifest = manifest
# Write JSON # Write JSON
output_json_path = os.path.join(app_directory, "apps.json") output_json_path = os.path.join(app_directory, "apps.json")
write_json(output_json_path, output_json) write_json(output_json_path, output_json)
+23 -36
View File
@@ -1,4 +1,3 @@
import configparser
import json import json
import os import os
import re import re
@@ -13,7 +12,7 @@ import tarfile
from urllib.parse import urlparse from urllib.parse import urlparse
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "3.5.1" ttbuild_version = "4.1.0"
ttbuild_cdn = "https://cdn.tactilityproject.org" ttbuild_cdn = "https://cdn.tactilityproject.org"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@@ -106,9 +105,17 @@ def get_url(ip, path):
return f"http://{ip}:{ttport}{path}" return f"http://{ip}:{ttport}{path}"
def read_properties_file(path): def read_properties_file(path):
config = configparser.RawConfigParser() properties = {}
config.read(path) with open(path, "r") as file:
return config for line in file:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
key, sep, value = line.partition("=")
if not sep:
continue
properties[key.strip()] = value.strip()
return properties
#endregion Core #endregion Core
@@ -185,7 +192,7 @@ def fetch_sdkconfig_files(platform_targets):
for platform in platform_targets: for platform in platform_targets:
sdkconfig_filename = f"sdkconfig.app.{platform}" sdkconfig_filename = f"sdkconfig.app.{platform}"
target_path = os.path.join(ttbuild_path, sdkconfig_filename) target_path = os.path.join(ttbuild_path, sdkconfig_filename)
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): if not download_file(f"{ttbuild_cdn}/sdk/{sdkconfig_filename}", target_path):
exit_with_error(f"Failed to download sdkconfig file for {platform}") exit_with_error(f"Failed to download sdkconfig file for {platform}")
#endregion SDK helpers #endregion SDK helpers
@@ -231,32 +238,12 @@ def read_manifest():
return read_properties_file("manifest.properties") return read_properties_file("manifest.properties")
def validate_manifest(manifest): def validate_manifest(manifest):
# [manifest] for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"):
if not "manifest" in manifest: if key not in manifest:
exit_with_error("Invalid manifest format: [manifest] not found") exit_with_error(f"Invalid manifest format: {key} not found")
if not "version" in manifest["manifest"]:
exit_with_error("Invalid manifest format: [manifest] version not found")
# [target]
if not "target" in manifest:
exit_with_error("Invalid manifest format: [target] not found")
if not "sdk" in manifest["target"]:
exit_with_error("Invalid manifest format: [target] sdk not found")
if not "platforms" in manifest["target"]:
exit_with_error("Invalid manifest format: [target] platforms not found")
# [app]
if not "app" in manifest:
exit_with_error("Invalid manifest format: [app] not found")
if not "id" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] id not found")
if not "versionName" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] versionName not found")
if not "versionCode" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] versionCode not found")
if not "name" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] name not found")
def is_valid_manifest_platform(manifest, platform): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target"]["platforms"].split(",") manifest_platforms = manifest["target.platforms"].split(",")
return platform in manifest_platforms return platform in manifest_platforms
def validate_manifest_platform(manifest, platform): def validate_manifest_platform(manifest, platform):
@@ -265,7 +252,7 @@ def validate_manifest_platform(manifest, platform):
def get_manifest_target_platforms(manifest, requested_platform): def get_manifest_target_platforms(manifest, requested_platform):
if requested_platform == "" or requested_platform is None: if requested_platform == "" or requested_platform is None:
return manifest["target"]["platforms"].split(",") return manifest["target.platforms"].split(",")
else: else:
validate_manifest_platform(manifest, requested_platform) validate_manifest_platform(manifest, requested_platform)
return [requested_platform] return [requested_platform]
@@ -512,7 +499,7 @@ def build_action(manifest, platform_arg, skip_build):
if use_local_sdk: if use_local_sdk:
global local_base_path global local_base_path
local_base_path = os.environ.get("TACTILITY_SDK_PATH") local_base_path = os.environ.get("TACTILITY_SDK_PATH")
validate_local_sdks(platforms_to_build, manifest["target"]["sdk"]) validate_local_sdks(platforms_to_build, manifest["target.sdk"])
if should_fetch_sdkconfig_files(platforms_to_build): if should_fetch_sdkconfig_files(platforms_to_build):
fetch_sdkconfig_files(platforms_to_build) fetch_sdkconfig_files(platforms_to_build)
@@ -521,7 +508,7 @@ def build_action(manifest, platform_arg, skip_build):
sdk_json = read_sdk_json() sdk_json = read_sdk_json()
validate_self(sdk_json) validate_self(sdk_json)
# Build # Build
sdk_version = manifest["target"]["sdk"] sdk_version = manifest["target.sdk"]
if not use_local_sdk: if not use_local_sdk:
if not sdk_download_all(sdk_version, platforms_to_build): if not sdk_download_all(sdk_version, platforms_to_build):
exit_with_error("Failed to download one or more SDKs") exit_with_error("Failed to download one or more SDKs")
@@ -570,7 +557,7 @@ def get_device_info(ip):
print_status_error(f"Device info request failed: {e}") print_status_error(f"Device info request failed: {e}")
def run_action(manifest, ip): def run_action(manifest, ip):
app_id = manifest["app"]["id"] app_id = manifest["app.id"]
print_status_busy("Running") print_status_busy("Running")
url = get_url(ip, "/app/run") url = get_url(ip, "/app/run")
params = {'id': app_id} params = {'id': app_id}
@@ -614,7 +601,7 @@ def install_action(ip, platforms):
return False return False
def uninstall_action(manifest, ip): def uninstall_action(manifest, ip):
app_id = manifest["app"]["id"] app_id = manifest["app.id"]
print_status_busy("Uninstalling") print_status_busy("Uninstalling")
url = get_url(ip, "/app/uninstall") url = get_url(ip, "/app/uninstall")
params = {'id': app_id} params = {'id': app_id}
@@ -670,7 +657,7 @@ if __name__ == "__main__":
exit_with_error("manifest.properties not found") exit_with_error("manifest.properties not found")
manifest = read_manifest() manifest = read_manifest()
validate_manifest(manifest) validate_manifest(manifest)
all_platform_targets = manifest["target"]["platforms"].split(",") all_platform_targets = manifest["target.platforms"].split(",")
# Update SDK cache (tool.json) # Update SDK cache (tool.json)
if not use_local_sdk and should_update_tool_json() and not update_tool_json(): if not use_local_sdk and should_update_tool_json() and not update_tool_json():
exit_with_error("Failed to retrieve SDK info") exit_with_error("Failed to retrieve SDK info")