2 Commits

Author SHA1 Message Date
Adolfo Reyna 3c6acf47a5 Add touch support to Snake game by dividing the screen into Up/Down/Left/Right zones
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
2026-06-23 12:16:33 -04:00
Adolfo Reyna 23e969c1b6 Configure HelloWorld manifest target platforms for esp32s3
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
2026-06-23 11:59:51 -04:00
29 changed files with 292 additions and 791 deletions
-21
View File
@@ -1,21 +0,0 @@
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 }}
+1 -9
View File
@@ -1,10 +1,5 @@
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:
@@ -16,11 +11,8 @@ 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'
id: release run: python release.py cdn_files/
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,8 +16,6 @@ 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:
@@ -25,26 +23,7 @@ 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 }}
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.brainfuck sdk=0.7.0-dev
app.version.name=0.5.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=5 [app]
app.name=Brainfuck interpreter id=one.tactility.brainfuck
app.description=Brainfuck esoteric language interpreter versionName=0.2.0
versionCode=2
name=Brainfuck interpreter
description=Brainfuck esoteric language interpreter
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.breakout sdk=0.7.0-dev
app.version.name=0.5.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=5 [app]
app.name=Breakout id=one.tactility.breakout
app.description=Classic brick-breaking arcade game versionName=0.2.0
versionCode=2
name=Breakout
description=Classic brick-breaking arcade game
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.calculator sdk=0.7.0-dev
app.version.name=0.6.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=6 [app]
app.name=Calculator id=one.tactility.calculator
versionName=0.3.0
versionCode=3
name=Calculator
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.diceware sdk=0.7.0-dev
app.version.name=0.6.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=6 [app]
app.name=Diceware id=one.tactility.diceware
versionName=0.3.0
versionCode=3
name=Diceware
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32s3,esp32p4 [target]
app.id=one.tactility.epubreader sdk=0.7.0-dev
app.version.name=0.4.0 platforms=esp32s3,esp32p4
app.version.code=4 [app]
app.name=Epub Reader id=one.tactility.epubreader
app.description=Epub and text file reader. Requires PSRAM! versionName=0.1.0
versionCode=1
name=Epub Reader
description=Epub and text file reader. Requires PSRAM!
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.gpio sdk=0.7.0-dev
app.version.name=0.7.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=7 [app]
app.name=GPIO id=one.tactility.gpio
versionName=0.4.0
versionCode=4
name=GPIO
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.graphicsdemo sdk=0.7.0-dev
app.version.name=0.6.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=6 [app]
app.name=Graphics Demo id=one.tactility.graphicsdemo
versionName=0.3.0
versionCode=3
name=Graphics Demo
+1 -1
View File
@@ -10,7 +10,7 @@ static void onShowApp(AppHandle app, void* data, lv_obj_t* parent) {
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent); lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, "Hello, world!"); lv_label_set_text(label, "Hello, world!\n(Loaded via LAN!)");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
} }
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.helloworld sdk=0.7.0-dev
app.version.name=0.6.0 platforms=esp32s3
app.version.code=6 [app]
app.name=Hello World id=one.tactility.helloworld
versionName=0.3.0
versionCode=3
name=Hello World
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32s3,esp32p4 [target]
app.id=one.tactility.m5unittest sdk=0.7.0-dev
app.version.name=0.4.0 platforms=esp32s3,esp32p4
app.version.code=4 [app]
app.name=M5 Unit Test id=one.tactility.m5unittest
versionName=0.1.0
versionCode=1
name=M5 Unit Test
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.magic8ball sdk=0.7.0-dev
app.version.name=0.5.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=5 [app]
app.name=Magic 8-Ball id=one.tactility.magic8ball
versionName=0.2.0
versionCode=2
name=Magic 8-Ball
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32s3,esp32p4 [target]
app.id=one.tactility.mediakeys sdk=0.7.0-dev
app.version.name=0.4.0 platforms=esp32s3,esp32p4
app.version.code=4 [app]
app.name=Media Keys id=one.tactility.mediakeys
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. versionName=0.1.0
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.
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.mystifydemo sdk=0.7.0-dev
app.version.name=0.6.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=6 [app]
app.name=Mystify Demo id=one.tactility.mystifydemo
versionName=0.3.0
versionCode=3
name=Mystify Demo
-16
View File
@@ -1,16 +0,0 @@
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
@@ -1,6 +0,0 @@
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
@@ -1,429 +0,0 @@
#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
@@ -1,13 +0,0 @@
[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
+10 -7
View File
@@ -1,7 +1,10 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.serialconsole sdk=0.7.0-dev
app.version.name=0.7.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=7 [app]
app.name=Serial Console id=one.tactility.serialconsole
versionName=0.4.0
versionCode=4
name=Serial Console
+36
View File
@@ -245,6 +245,41 @@ static void game_play_event(lv_event_t* e) {
default: default:
break; break;
} }
} else if (code == LV_EVENT_CLICKED) {
lv_indev_t* indev = lv_indev_active();
if (indev) {
lv_point_t point;
lv_indev_get_point(indev, &point);
lv_area_t coords;
lv_obj_get_coords(game->container, &coords);
lv_coord_t w = coords.x2 - coords.x1 + 1;
lv_coord_t h = coords.y2 - coords.y1 + 1;
lv_coord_t cx = coords.x1 + w / 2;
lv_coord_t cy = coords.y1 + h / 2;
lv_coord_t rx = point.x - cx;
lv_coord_t ry = point.y - cy;
if (rx != 0 || ry != 0) {
if (abs(rx) * h > abs(ry) * w) {
// Horizontal touch
if (rx > 0) {
snake_set_direction(game, SNAKE_DIR_RIGHT);
} else {
snake_set_direction(game, SNAKE_DIR_LEFT);
}
} else {
// Vertical touch
if (ry > 0) {
snake_set_direction(game, SNAKE_DIR_DOWN);
} else {
snake_set_direction(game, SNAKE_DIR_UP);
}
}
}
}
} else if (code == LV_EVENT_KEY) { } else if (code == LV_EVENT_KEY) {
uint32_t key = lv_event_get_key(e); uint32_t key = lv_event_get_key(e);
// Arrow keys, WASD, and punctuation keys for cardputer // Arrow keys, WASD, and punctuation keys for cardputer
@@ -394,6 +429,7 @@ lv_obj_t* snake_create(lv_obj_t* parent, uint16_t cell_size, bool wall_collision
// Add event callbacks for touch gestures and keyboard // Add event callbacks for touch gestures and keyboard
lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_GESTURE, obj); lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_GESTURE, obj);
lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_KEY, obj); lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_KEY, obj);
lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_CLICKED, obj);
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL); lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
// Set up keyboard focus if available // Set up keyboard focus if available
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.snake sdk=0.7.0-dev
app.version.name=0.8.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=8 [app]
app.name=Snake id=one.tactility.snake
app.description=Classic Snake game versionName=0.5.0
versionCode=5
name=Snake
description=Classic Snake game
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.tamatac sdk=0.7.0-dev
app.version.name=0.4.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=4 [app]
app.name=TamaTac id=one.tactility.tamatac
app.description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM. versionName=0.1.0
versionCode=1
name=TamaTac
description=Virtual pet inspired by Tamagotchi. Only runs on devices with PSRAM.
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.todolist sdk=0.7.0-dev
app.version.name=0.5.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=5 [app]
app.name=Todo List id=one.tactility.todolist
app.description=Simple task list manager versionName=0.2.0
versionCode=2
name=Todo List
description=Simple task list manager
+11 -8
View File
@@ -1,8 +1,11 @@
manifest.version=0.2 [manifest]
target.sdk=0.8.0-dev version=0.1
target.platforms=esp32,esp32s3,esp32c6,esp32p4 [target]
app.id=one.tactility.twoeleven sdk=0.7.0-dev
app.version.name=0.7.0 platforms=esp32,esp32s3,esp32c6,esp32p4
app.version.code=7 [app]
app.name=2048 id=one.tactility.twoeleven
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). versionName=0.4.0
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
@@ -1,70 +0,0 @@
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
)
+39 -54
View File
@@ -1,23 +1,14 @@
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):
properties = {} config = configparser.RawConfigParser()
with open(path, "r") as file: config.read(path)
for line in file: return config
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
@@ -71,49 +62,51 @@ 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.version.code"] version_code = manifest["app"]["versionCode"]
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 check_and_get_sdk_version(manifest_map):
"""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))
def get_git_commit_hash():
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
def manifest_config_to_flat_json(manifest): def manifest_config_to_flat_json(manifest):
"""Convert a flat (V2) manifest dict into a flat JSON-like dict. """Convert a ConfigParser manifest into a flat JSON-like dict.
Expected keys: Expected sections/keys (case-insensitive for keys):
app.id -> appId - [app]
app.version.name -> appVersionName id -> appId
app.version.code -> appVersionCode (int) versionName -> appVersionName
app.name -> appName versionCode -> appVersionCode (int)
app.description -> appDescription (optional; default "") name -> appName
target.sdk -> targetSdk description -> appDescription (optional; default "")
target.platforms -> targetPlatforms (comma-separated list) - [target]
sdk -> targetSdk
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 = manifest.get("app.id", "") app_id = get_opt("app", "id", "")
app_version_name = manifest.get("app.version.name", "") app_version_name = get_opt("app", "versionName", "")
app_version_code_raw = manifest.get("app.version.code", "0") app_version_code_raw = get_opt("app", "versionCode", "0")
app_name = manifest.get("app.name", "") app_name = get_opt("app", "name", "")
app_description = manifest.get("app.description", "") or "" app_description = get_opt("app", "description", "") or ""
# Coerce version code to int safely # Coerce version code to int safely
try: try:
@@ -121,8 +114,8 @@ def manifest_config_to_flat_json(manifest):
except Exception: except Exception:
app_version_code = 0 app_version_code = 0
target_sdk = manifest.get("target.sdk", "") target_sdk = get_opt("target", "sdk", "")
platforms_raw = manifest.get("target.platforms", "") platforms_raw = get_opt("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)
@@ -149,23 +142,15 @@ 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]}")
+36 -23
View File
@@ -1,3 +1,4 @@
import configparser
import json import json
import os import os
import re import re
@@ -12,7 +13,7 @@ import tarfile
from urllib.parse import urlparse from urllib.parse import urlparse
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "4.1.0" ttbuild_version = "3.5.1"
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
@@ -105,17 +106,9 @@ 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):
properties = {} config = configparser.RawConfigParser()
with open(path, "r") as file: config.read(path)
for line in file: return config
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
@@ -192,7 +185,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}/sdk/{sdkconfig_filename}", target_path): if not download_file(f"{ttbuild_cdn}/{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
@@ -238,12 +231,32 @@ def read_manifest():
return read_properties_file("manifest.properties") return read_properties_file("manifest.properties")
def validate_manifest(manifest): def validate_manifest(manifest):
for key in ("manifest.version", "target.sdk", "target.platforms", "app.id", "app.version.name", "app.version.code", "app.name"): # [manifest]
if key not in manifest: if not "manifest" in manifest:
exit_with_error(f"Invalid manifest format: {key} not found") exit_with_error("Invalid manifest format: [manifest] 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):
@@ -252,7 +265,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]
@@ -499,7 +512,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)
@@ -508,7 +521,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")
@@ -557,7 +570,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}
@@ -601,7 +614,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}
@@ -657,7 +670,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")