From 15f4fbfdc6d357486f8ced6e565e0331e1c763a0 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 2 Aug 2025 12:28:28 +0200 Subject: [PATCH] Merge Develop into Main (#300) - Made an external app from internal Calculator app - Update tactility.py to v1.2.0 (fix bug with selfupdate) - Added warning to Development service UI - Add context to `SECURITY.md` - Split `ObjectFileReader` and `ObjectFileWriter` into separate cpp files - Fix related to GPS config read errors --- ExternalApps/Calculator/.gitignore | 2 + ExternalApps/Calculator/CMakeLists.txt | 16 + ExternalApps/Calculator/main/CMakeLists.txt | 6 + .../Calculator/main/Source/Calculator.cpp | 204 ++++++ .../Calculator/main/Source/Calculator.h | 27 + ExternalApps/Calculator/main/Source/Dequeue.h | 95 +++ ExternalApps/Calculator/main/Source/Stack.h | 21 + ExternalApps/Calculator/main/Source/Str.cpp | 2 + ExternalApps/Calculator/main/Source/Str.h | 618 ++++++++++++++++++ ExternalApps/Calculator/main/Source/main.cpp | 30 + ExternalApps/Calculator/tactility.properties | 2 + ExternalApps/Calculator/tactility.py | 478 ++++++++++++++ ExternalApps/HelloWorld/tactility.py | 92 ++- SECURITY.md | 19 +- .../Source/app/development/Development.cpp | 9 +- .../Source/service/gps/GpsConfiguration.cpp | 8 + TactilityC/Source/tt_init.cpp | 13 + TactilityCore/CMakeLists.txt | 5 + .../Tactility/file/ObjectFilePrivate.h | 19 + .../Source/file/ObjectFileReader.cpp | 78 +++ .../{ObjectFile.cpp => ObjectFileWriter.cpp} | 100 +-- 21 files changed, 1736 insertions(+), 108 deletions(-) create mode 100644 ExternalApps/Calculator/.gitignore create mode 100644 ExternalApps/Calculator/CMakeLists.txt create mode 100644 ExternalApps/Calculator/main/CMakeLists.txt create mode 100644 ExternalApps/Calculator/main/Source/Calculator.cpp create mode 100644 ExternalApps/Calculator/main/Source/Calculator.h create mode 100644 ExternalApps/Calculator/main/Source/Dequeue.h create mode 100644 ExternalApps/Calculator/main/Source/Stack.h create mode 100644 ExternalApps/Calculator/main/Source/Str.cpp create mode 100644 ExternalApps/Calculator/main/Source/Str.h create mode 100644 ExternalApps/Calculator/main/Source/main.cpp create mode 100644 ExternalApps/Calculator/tactility.properties create mode 100644 ExternalApps/Calculator/tactility.py create mode 100644 TactilityCore/Private/Tactility/file/ObjectFilePrivate.h create mode 100644 TactilityCore/Source/file/ObjectFileReader.cpp rename TactilityCore/Source/file/{ObjectFile.cpp => ObjectFileWriter.cpp} (56%) diff --git a/ExternalApps/Calculator/.gitignore b/ExternalApps/Calculator/.gitignore new file mode 100644 index 00000000..89baa26e --- /dev/null +++ b/ExternalApps/Calculator/.gitignore @@ -0,0 +1,2 @@ +build*/ +.tactility/ diff --git a/ExternalApps/Calculator/CMakeLists.txt b/ExternalApps/Calculator/CMakeLists.txt new file mode 100644 index 00000000..01bfb4b5 --- /dev/null +++ b/ExternalApps/Calculator/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if (DEFINED ENV{TACTILITY_SDK_PATH}) + set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH}) +else() + set(TACTILITY_SDK_PATH "../../release/TactilitySDK") + message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}") +endif() + +include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") +set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH}) + +project(Calculator) +tactility_project(Calculator) diff --git a/ExternalApps/Calculator/main/CMakeLists.txt b/ExternalApps/Calculator/main/CMakeLists.txt new file mode 100644 index 00000000..62c36897 --- /dev/null +++ b/ExternalApps/Calculator/main/CMakeLists.txt @@ -0,0 +1,6 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + REQUIRES TactilitySDK +) diff --git a/ExternalApps/Calculator/main/Source/Calculator.cpp b/ExternalApps/Calculator/main/Source/Calculator.cpp new file mode 100644 index 00000000..b6d0ad67 --- /dev/null +++ b/ExternalApps/Calculator/main/Source/Calculator.cpp @@ -0,0 +1,204 @@ +#include "Calculator.h" +#include "Stack.h" + +#include +#include +#include + +constexpr const char* TAG = "Calculator"; + +static int precedence(char op) { + if (op == '+' || op == '-') return 1; + if (op == '*' || op == '/') return 2; + return 0; +} + +void Calculator::button_event_cb(lv_event_t* e) { + Calculator* self = static_cast(lv_event_get_user_data(e)); + lv_obj_t* buttonmatrix = lv_event_get_current_target_obj(e); + lv_event_code_t event_code = lv_event_get_code(e); + uint32_t button_id = lv_buttonmatrix_get_selected_button(buttonmatrix); + const char* button_text = lv_buttonmatrix_get_button_text(buttonmatrix, button_id); + if (event_code == LV_EVENT_VALUE_CHANGED) { + self->handleInput(button_text); + } +} + +void Calculator::handleInput(const char* txt) { + if (strcmp(txt, "C") == 0) { + resetCalculator(); + return; + } + + if (strcmp(txt, "=") == 0) { + evaluateExpression(); + return; + } + + if (strlen(formulaBuffer) + strlen(txt) < sizeof(formulaBuffer) - 1) { + if (newInput) { + memset(formulaBuffer, 0, sizeof(formulaBuffer)); + newInput = false; + } + strcat(formulaBuffer, txt); + lv_label_set_text(displayLabel, formulaBuffer); + } +} + +Dequeue Calculator::infixToRPN(const Str& infix) { + Stack opStack; + Dequeue output; + Str token; + size_t i = 0; + + while (i < infix.length()) { + char ch = infix[i]; + + if (isdigit(ch)) { + token.clear(); + while (i < infix.length() && (isdigit(infix[i]) || infix[i] == '.')) { token.append(infix[i++]); } + output.pushBack(token); + continue; + } + + if (ch == '(') { opStack.push(ch); } else if (ch == ')') { + while (!opStack.empty() && opStack.top() != '(') { + output.pushBack(Str(1, opStack.top())); + opStack.pop(); + } + opStack.pop(); + } else if (strchr("+-*/", ch)) { + while (!opStack.empty() && precedence(opStack.top()) >= precedence(ch)) { + output.pushBack(Str(1, opStack.top())); + opStack.pop(); + } + opStack.push(ch); + } + + i++; + } + + while (!opStack.empty()) { + output.pushBack(Str(1, opStack.top())); + opStack.pop(); + } + + return output; +} + +double Calculator::evaluateRPN(Dequeue rpnQueue) { + Stack values; + + while (!rpnQueue.empty()) { + Str token = rpnQueue.front(); + rpnQueue.popFront(); + + if (isdigit(token[0])) { + double d; + sscanf(token.c_str(), "%lf", &d); + values.push(d); + } else if (strchr("+-*/", token[0])) { + if (values.size() < 2) return 0; + + double b = values.top(); + values.pop(); + double a = values.top(); + values.pop(); + + if (token[0] == '+') values.push(a + b); + else if (token[0] == '-') values.push(a - b); + else if (token[0] == '*') values.push(a * b); + else if (token[0] == '/' && b != 0) values.push(a / b); + } + } + + return values.empty() ? 0 : values.top(); +} +void Calculator::evaluateExpression() { + double result = computeFormula(); + + size_t formulaLen = strlen(formulaBuffer); + size_t maxAvailable = sizeof(formulaBuffer) - formulaLen - 1; + + if (maxAvailable > 10) { + char resultBuffer[32]; + snprintf(resultBuffer, sizeof(resultBuffer), " = %.8g", result); + strncat(formulaBuffer, resultBuffer, maxAvailable); + } else { snprintf(formulaBuffer, sizeof(formulaBuffer), "%.8g", result); } + + lv_label_set_text(displayLabel, "0"); + lv_label_set_text(resultLabel, formulaBuffer); + newInput = true; +} + +double Calculator::computeFormula() { + return evaluateRPN(infixToRPN(Str(formulaBuffer))); +} + +void Calculator::resetCalculator() { + memset(formulaBuffer, 0, sizeof(formulaBuffer)); + lv_label_set_text(displayLabel, "0"); + lv_label_set_text(resultLabel, ""); + newInput = true; +} + +void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) { + lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_height(wrapper, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(wrapper, 0); + lv_obj_set_style_pad_top(wrapper, 4, LV_PART_MAIN); + lv_obj_set_style_pad_bottom(wrapper, 4, LV_PART_MAIN); + lv_obj_set_style_pad_left(wrapper, 10, LV_PART_MAIN); + lv_obj_set_style_pad_right(wrapper, 10, LV_PART_MAIN); + lv_obj_set_style_pad_column(wrapper, 40, LV_PART_MAIN); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE); + + displayLabel = lv_label_create(wrapper); + lv_label_set_text(displayLabel, "0"); + lv_obj_set_width(displayLabel, LV_SIZE_CONTENT); + lv_obj_set_align(displayLabel, LV_ALIGN_LEFT_MID); + + resultLabel = lv_label_create(wrapper); + lv_label_set_text(resultLabel, ""); + lv_obj_set_width(resultLabel, LV_SIZE_CONTENT); + lv_obj_set_align(resultLabel, LV_ALIGN_RIGHT_MID); + + static const char* btn_map[] = { + "(", ")", "C", "/", "\n", + "7", "8", "9", "*", "\n", + "4", "5", "6", "-", "\n", + "1", "2", "3", "+", "\n", + "0", "=", "", "", "" + }; + + lv_obj_t* buttonmatrix = lv_buttonmatrix_create(parent); + lv_buttonmatrix_set_map(buttonmatrix, btn_map); + + lv_obj_set_style_pad_all(buttonmatrix, 5, LV_PART_MAIN); + lv_obj_set_style_pad_row(buttonmatrix, 10, LV_PART_MAIN); + lv_obj_set_style_pad_column(buttonmatrix, 5, LV_PART_MAIN); + lv_obj_set_style_border_width(buttonmatrix, 0, LV_PART_MAIN); + lv_obj_set_style_bg_color(buttonmatrix, lv_palette_main(LV_PALETTE_BLUE), LV_PART_ITEMS); + + lv_obj_set_style_border_width(buttonmatrix, 0, LV_PART_MAIN); + if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { + //small screens + lv_obj_set_size(buttonmatrix, lv_pct(100), lv_pct(60)); + } else { + //large screens + lv_obj_set_size(buttonmatrix, lv_pct(100), lv_pct(80)); + } + lv_obj_align(buttonmatrix, LV_ALIGN_BOTTOM_MID, 0, -5); + + lv_obj_add_event_cb(buttonmatrix, button_event_cb, LV_EVENT_VALUE_CHANGED, this); +} \ No newline at end of file diff --git a/ExternalApps/Calculator/main/Source/Calculator.h b/ExternalApps/Calculator/main/Source/Calculator.h new file mode 100644 index 00000000..9149d912 --- /dev/null +++ b/ExternalApps/Calculator/main/Source/Calculator.h @@ -0,0 +1,27 @@ +#pragma once + +#include "tt_app.h" + +#include +#include "Str.h" +#include "Dequeue.h" + +class Calculator { + + lv_obj_t* displayLabel; + lv_obj_t* resultLabel; + char formulaBuffer[128] = {0}; // Stores the full input expression + bool newInput = true; + + static void button_event_cb(lv_event_t* e); + void handleInput(const char* txt); + void evaluateExpression(); + double computeFormula(); + static Dequeue infixToRPN(const Str& infix); + static double evaluateRPN(Dequeue rpnQueue); + void resetCalculator(); + +public: + + void onShow(AppHandle context, lv_obj_t* parent); +}; \ No newline at end of file diff --git a/ExternalApps/Calculator/main/Source/Dequeue.h b/ExternalApps/Calculator/main/Source/Dequeue.h new file mode 100644 index 00000000..9d3f2d53 --- /dev/null +++ b/ExternalApps/Calculator/main/Source/Dequeue.h @@ -0,0 +1,95 @@ +#pragma once + +template +class Dequeue { + + struct Node { + DataType data; + Node* next; + Node* previous; + + Node(DataType data, Node* next, Node* previous): + data(data), + next(next), + previous(previous) + {} + }; + + int count = 0; + Node* head = nullptr; + Node* tail = nullptr; + +public: + + void pushFront(DataType data) { + auto* new_node = new Node(data, head, nullptr); + + if (head != nullptr) { + head->previous = new_node; + } + + if (tail == nullptr) { + tail = new_node; + } + + head = new_node; + count++; + } + + void pushBack(DataType data) { + auto* new_node = new Node(data, nullptr, tail); + + if (head == nullptr) { + head = new_node; + } + + if (tail != nullptr) { + tail->next = new_node; + } + + tail = new_node; + count++; + } + + void popFront() { + if (head != nullptr) { + bool is_last_node = (head == tail); + Node* node_to_delete = head; + head = node_to_delete->next; + if (is_last_node) { + tail = nullptr; + } + delete node_to_delete; + count--; + } + } + + void popBack() { + if (tail != nullptr) { + bool is_last_node = (head == tail); + Node* node_to_delete = tail; + tail = node_to_delete->previous; + if (is_last_node) { + head = nullptr; + } + delete node_to_delete; + count--; + } + } + + DataType back() const { + assert(tail != nullptr); + return tail->data; + } + + DataType front() const { + assert(head != nullptr); + return head->data; + } + + bool empty() const { + return head == nullptr; + } + + int size() const { return count; } +}; \ No newline at end of file diff --git a/ExternalApps/Calculator/main/Source/Stack.h b/ExternalApps/Calculator/main/Source/Stack.h new file mode 100644 index 00000000..0382b4ec --- /dev/null +++ b/ExternalApps/Calculator/main/Source/Stack.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Dequeue.h" + +template +class Stack { + + Dequeue dequeue; + +public: + + void push(DataType data) { dequeue.pushFront(data); } + + void pop() { dequeue.popFront(); } + + DataType top() const { return dequeue.front(); } + + bool empty() const { return dequeue.empty(); } + + int size() const { return dequeue.size(); } +}; diff --git a/ExternalApps/Calculator/main/Source/Str.cpp b/ExternalApps/Calculator/main/Source/Str.cpp new file mode 100644 index 00000000..78bcabea --- /dev/null +++ b/ExternalApps/Calculator/main/Source/Str.cpp @@ -0,0 +1,2 @@ +#define STR_IMPLEMENTATION +#include "Str.h" diff --git a/ExternalApps/Calculator/main/Source/Str.h b/ExternalApps/Calculator/main/Source/Str.h new file mode 100644 index 00000000..c5c022ae --- /dev/null +++ b/ExternalApps/Calculator/main/Source/Str.h @@ -0,0 +1,618 @@ +// Str v0.33 +// Simple C++ string type with an optional local buffer, by Omar Cornut +// https://github.com/ocornut/str + +// LICENSE +// This software is in the public domain. Where that dedication is not +// recognized, you are granted a perpetual, irrevocable license to copy, +// distribute, and modify this file as you see fit. + +// USAGE +// Include this file in whatever places need to refer to it. +// In ONE .cpp file, write '#define STR_IMPLEMENTATION' before the #include of this file. +// This expands out the actual implementation into that C/C++ file. + + +/* +- This isn't a fully featured string class. +- It is a simple, bearable replacement to std::string that isn't heap abusive nor bloated (can actually be debugged by humans). +- String are mutable. We don't maintain size so length() is not-constant time. +- Maximum string size currently limited to 2 MB (we allocate 21 bits to hold capacity). +- Local buffer size is currently limited to 1023 bytes (we allocate 10 bits to hold local buffer size). +- In "non-owned" mode for literals/reference we don't do any tracking/counting of references. +- Overhead is 8-bytes in 32-bits, 16-bytes in 64-bits (12 + alignment). +- This code hasn't been tested very much. it is probably incomplete or broken. Made it for my own use. + +The idea is that you can provide an arbitrary sized local buffer if you expect string to fit +most of the time, and then you avoid using costly heap. + +No local buffer, always use heap, sizeof()==8~16 (depends if your pointers are 32-bits or 64-bits) + + Str s = "hey"; + +With a local buffer of 16 bytes, sizeof() == 8~16 + 16 bytes. + + Str16 s = "filename.h"; // copy into local buffer + Str16 s = "long_filename_not_very_long_but_longer_than_expected.h"; // use heap + +With a local buffer of 256 bytes, sizeof() == 8~16 + 256 bytes. + + Str256 s = "long_filename_not_very_long_but_longer_than_expected.h"; // copy into local buffer + +Common sizes are defined at the bottom of Str.h, you may define your own. + +Functions: + + Str256 s; + s.set("hello sailor"); // set (copy) + s.setf("%s/%s.tmp", folder, filename); // set (w/format) + s.append("hello"); // append. cost a length() calculation! + s.appendf("hello %d", 42); // append (w/format). cost a length() calculation! + s.set_ref("Hey!"); // set (literal/reference, just copy pointer, no tracking) + +Constructor helper for format string: add a trailing 'f' to the type. Underlying type is the same. + + Str256f filename("%s/%s.tmp", folder, filename); // construct (w/format) + fopen(Str256f("%s/%s.tmp, folder, filename).c_str(), "rb"); // construct (w/format), use as function param, destruct + +Constructor helper for reference/literal: + + StrRef ref("literal"); // copy pointer, no allocation, no string copy + StrRef ref2(GetDebugName()); // copy pointer. no tracking of anything whatsoever, know what you are doing! + +All StrXXX types derives from Str and instance hold the local buffer capacity. So you can pass e.g. Str256* to a function taking base type Str* and it will be functional. + + void MyFunc(Str& s) { s = "Hello"; } // will use local buffer if available in Str instance + +(Using a template e.g. Str we could remove the LocalBufSize storage but it would make passing typed Str<> to functions tricky. + Instead we don't use template so you can pass them around as the base type Str*. Also, templates are ugly.) +*/ + +/* + CHANGELOG + 0.33 - fixed capacity() return value to match standard. e.g. a Str256's capacity() now returns 255, not 256. + 0.32 - added owned() accessor. + 0.31 - fixed various warnings. + 0.30 - turned into a single header file, removed Str.cpp. + 0.29 - fixed bug when calling reserve on non-owned strings (ie. when using StrRef or set_ref), and fixed include. + 0.28 - breaking change: replaced Str32 by Str30 to avoid collision with Str32 from MacTypes.h . + 0.27 - added STR_API and basic .natvis file. + 0.26 - fixed set(cont char* src, const char* src_end) writing null terminator to the wrong position. + 0.25 - allow set(const char* NULL) or operator= NULL to clear the string. note that set() from range or other types are not allowed. + 0.24 - allow set_ref(const char* NULL) to clear the string. include fixes for linux. + 0.23 - added append(char). added append_from(int idx, XXX) functions. fixed some compilers warnings. + 0.22 - documentation improvements, comments. fixes for some compilers. + 0.21 - added StrXXXf() constructor to construct directly from a format string. +*/ + +/* +TODO +- Since we lose 4-bytes of padding on 64-bits architecture, perhaps just spread the header to 8-bytes and lift size limits? +- More functions/helpers. +*/ + +#ifndef STR_INCLUDED +#define STR_INCLUDED + +//------------------------------------------------------------------------- +// CONFIGURATION +//------------------------------------------------------------------------- + +#ifndef STR_MEMALLOC +#define STR_MEMALLOC malloc +#include +#endif +#ifndef STR_MEMFREE +#define STR_MEMFREE free +#include +#endif +#ifndef STR_ASSERT +#define STR_ASSERT assert +#include +#endif +#ifndef STR_API +#define STR_API +#endif +#include // for va_list +#include // for strlen, strcmp, memcpy, etc. + +// Configuration: #define STR_DEFINE_STR32 1 to keep defining Str32/Str32f, but be warned: on macOS/iOS, MacTypes.h also defines a type named Str32. +#ifndef STR_DEFINE_STR32 +#define STR_DEFINE_STR32 0 +#endif + +//------------------------------------------------------------------------- +// HEADERS +//------------------------------------------------------------------------- + +// This is the base class that you can pass around +// Footprint is 8-bytes (32-bits arch) or 16-bytes (64-bits arch) +class STR_API Str +{ + char* Data; // Point to LocalBuf() or heap allocated + int Capacity : 21; // Max 2 MB. Exclude zero terminator. + int LocalBufSize : 10; // Max 1023 bytes + unsigned int Owned : 1; // Set when we have ownership of the pointed data (most common, unless using set_ref() method or StrRef constructor) + +public: + inline char* c_str() { return Data; } + inline const char* c_str() const { return Data; } + inline bool empty() const { return Data[0] == 0; } + inline int length() const { return (int)strlen(Data); } // by design, allow user to write into the buffer at any time + inline int capacity() const { return Capacity; } + inline bool owned() const { return Owned ? true : false; } + + inline void set_ref(const char* src); + int setf(const char* fmt, ...); + int setfv(const char* fmt, va_list args); + int setf_nogrow(const char* fmt, ...); + int setfv_nogrow(const char* fmt, va_list args); + int append(char c); + int append(const char* s, const char* s_end = NULL); + int appendf(const char* fmt, ...); + int appendfv(const char* fmt, va_list args); + int append_from(int idx, char c); + int append_from(int idx, const char* s, const char* s_end = NULL); // If you know the string length or want to append from a certain point + int appendf_from(int idx, const char* fmt, ...); + int appendfv_from(int idx, const char* fmt, va_list args); + + void clear(); + void reserve(int cap); + void reserve_discard(int cap); + void shrink_to_fit(); + + inline char& operator[](size_t i) { return Data[i]; } + inline char operator[](size_t i) const { return Data[i]; } + //explicit operator const char*() const{ return Data; } + + inline Str(); + inline Str(const char* rhs); + inline void set(const char* src); + inline void set(const char* src, const char* src_end); + inline Str& operator=(const char* rhs) { set(rhs); return *this; } + inline bool operator==(const char* rhs) const { return strcmp(c_str(), rhs) == 0; } + + inline Str(const Str& rhs); + inline void set(const Str& src); + inline void set(int count, char character); + inline Str& operator=(const Str& rhs) { set(rhs); return *this; } + inline bool operator==(const Str& rhs) const { return strcmp(c_str(), rhs.c_str()) == 0; } + + inline Str(int amount, char character); + + // Destructor for all variants + inline ~Str() + { + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + } + + static char* EmptyBuffer; + +protected: + inline char* local_buf() { return (char*)this + sizeof(Str); } + inline const char* local_buf() const { return (char*)this + sizeof(Str); } + inline bool is_using_local_buf() const { return Data == local_buf() && LocalBufSize != 0; } + + // Constructor for StrXXX variants with local buffer + Str(unsigned short local_buf_size) + { + STR_ASSERT(local_buf_size < 1024); + Data = local_buf(); + Data[0] = '\0'; + Capacity = local_buf_size ? local_buf_size - 1 : 0; + LocalBufSize = local_buf_size; + Owned = 1; + } +}; + +void Str::set(const char* src) +{ + // We allow set(NULL) or via = operator to clear the string. + if (src == NULL) + { + clear(); + return; + } + int buf_len = (int)strlen(src); + if (Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src, (size_t)(buf_len + 1)); + Owned = 1; +} + +void Str::set(const char* src, const char* src_end) +{ + STR_ASSERT(src != NULL && src_end >= src); + int buf_len = (int)(src_end - src); + if ((int)Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src, (size_t)buf_len); + Data[buf_len] = 0; + Owned = 1; +} + +void Str::set(const Str& src) +{ + int buf_len = (int)strlen(src.c_str()); + if ((int)Capacity < buf_len) + reserve_discard(buf_len); + memcpy(Data, src.c_str(), (size_t)(buf_len + 1)); + Owned = 1; +} + +void Str::set(int count, char character) { + int buf_len = count + 1; + if ((int)Capacity < buf_len) + reserve_discard(buf_len); + memset(Data, character, count); + Data[count] = 0; + Owned = 1; +} + +inline void Str::set_ref(const char* src) +{ + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + Data = src ? (char*)src : EmptyBuffer; + Capacity = 0; + Owned = 0; +} + +Str::Str() +{ + Data = EmptyBuffer; // Shared READ-ONLY initial buffer for 0 capacity + Capacity = 0; + LocalBufSize = 0; + Owned = 0; +} + +Str::Str(const Str& rhs) : Str() +{ + set(rhs); +} + +Str::Str(const char* rhs) : Str() +{ + set(rhs); +} + +Str::Str(int amount, char character) : Str() { + set(amount, character); +} + +// Literal/reference string +class StrRef : public Str +{ +public: + StrRef(const char* s) : Str() { set_ref(s); } +}; + +#define STR_DEFINETYPE(TYPENAME, LOCALBUFSIZE) \ +class TYPENAME : public Str \ +{ \ + char local_buf[LOCALBUFSIZE]; \ +public: \ + TYPENAME() : Str(LOCALBUFSIZE) {} \ + TYPENAME(const Str& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const char* rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME(const TYPENAME& rhs) : Str(LOCALBUFSIZE) { set(rhs); } \ + TYPENAME& operator=(const char* rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const Str& rhs) { set(rhs); return *this; } \ + TYPENAME& operator=(const TYPENAME& rhs) { set(rhs); return *this; } \ +}; + +// Disable PVS-Studio warning V730: Not all members of a class are initialized inside the constructor (local_buf is not initialized and that is fine) +// -V:STR_DEFINETYPE:730 + +// Helper to define StrXXXf constructors +#define STR_DEFINETYPE_F(TYPENAME, TYPENAME_F) \ +class TYPENAME_F : public TYPENAME \ +{ \ +public: \ + TYPENAME_F(const char* fmt, ...) : TYPENAME() { va_list args; va_start(args, fmt); setfv(fmt, args); va_end(args); } \ +}; + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-private-field" // warning : private field 'local_buf' is not used +#endif + +// Declaring types for common sizes here +STR_DEFINETYPE(Str16, 16) +STR_DEFINETYPE(Str30, 30) +STR_DEFINETYPE(Str64, 64) +STR_DEFINETYPE(Str128, 128) +STR_DEFINETYPE(Str256, 256) +STR_DEFINETYPE(Str512, 512) + +// Declaring helper constructors to pass in format strings in one statement +STR_DEFINETYPE_F(Str16, Str16f) +STR_DEFINETYPE_F(Str30, Str30f) +STR_DEFINETYPE_F(Str64, Str64f) +STR_DEFINETYPE_F(Str128, Str128f) +STR_DEFINETYPE_F(Str256, Str256f) +STR_DEFINETYPE_F(Str512, Str512f) + +#if STR_DEFINE_STR32 +STR_DEFINETYPE(Str32, 32) +STR_DEFINETYPE_F(Str32, Str32f) +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // #ifndef STR_INCLUDED + +//------------------------------------------------------------------------- +// IMPLEMENTATION +//------------------------------------------------------------------------- + +#ifdef STR_IMPLEMENTATION + +#include // for vsnprintf + +// On some platform vsnprintf() takes va_list by reference and modifies it. +// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. +#ifndef va_copy +#define va_copy(dest, src) (dest = src) +#endif + +// Static empty buffer we can point to for empty strings +// Pointing to a literal increases the like-hood of getting a crash if someone attempts to write in the empty string buffer. +char* Str::EmptyBuffer = (char*)"\0NULL"; + +// Clear +void Str::clear() +{ + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + if (LocalBufSize) + { + Data = local_buf(); + Data[0] = '\0'; + Capacity = LocalBufSize - 1; + Owned = 1; + } + else + { + Data = EmptyBuffer; + Capacity = 0; + Owned = 0; + } +} + +// Reserve memory, preserving the current of the buffer +// Capacity doesn't include the zero terminator, so reserve(5) is enough to store "hello". +void Str::reserve(int new_capacity) +{ + if (new_capacity <= Capacity) + return; + + char* new_data; + if (new_capacity <= LocalBufSize - 1) + { + // Disowned -> LocalBuf + new_data = local_buf(); + new_capacity = LocalBufSize - 1; + } + else + { + // Disowned or LocalBuf -> Heap + new_data = (char*)STR_MEMALLOC((size_t)(new_capacity + 1) * sizeof(char)); + } + + // string in Data might be longer than new_capacity if it wasn't owned, don't copy too much +#ifdef _MSC_VER + strncpy_s(new_data, (size_t)new_capacity + 1, Data, (size_t)new_capacity); +#else + strncpy(new_data, Data, (size_t)new_capacity); +#endif + new_data[new_capacity] = 0; + + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + + Data = new_data; + Capacity = new_capacity; + Owned = 1; +} + +// Reserve memory, discarding the current of the buffer (if we expect to be fully rewritten) +void Str::reserve_discard(int new_capacity) +{ + if (new_capacity <= Capacity) + return; + + if (Owned && !is_using_local_buf()) + STR_MEMFREE(Data); + + if (new_capacity <= LocalBufSize - 1) + { + // Disowned -> LocalBuf + Data = local_buf(); + Capacity = LocalBufSize - 1; + } + else + { + // Disowned or LocalBuf -> Heap + Data = (char*)STR_MEMALLOC((size_t)(new_capacity + 1) * sizeof(char)); + Capacity = new_capacity; + } + Owned = 1; +} + +void Str::shrink_to_fit() +{ + if (!Owned || is_using_local_buf()) + return; + int new_capacity = length(); + if (Capacity <= new_capacity) + return; + + char* new_data = (char*)STR_MEMALLOC((size_t)(new_capacity + 1) * sizeof(char)); + memcpy(new_data, Data, (size_t)(new_capacity + 1)); + STR_MEMFREE(Data); + Data = new_data; + Capacity = new_capacity; +} + +// FIXME: merge setfv() and appendfv()? +int Str::setfv(const char* fmt, va_list args) +{ + // Needed for portability on platforms where va_list are passed by reference and modified by functions + va_list args2; + va_copy(args2, args); + + // MSVC returns -1 on overflow when writing, which forces us to do two passes + // FIXME-OPT: Find a way around that. +#ifdef _MSC_VER + int len = vsnprintf(NULL, 0, fmt, args); + STR_ASSERT(len >= 0); + + if (Capacity < len) + reserve_discard(len); + len = vsnprintf(Data, (size_t)len + 1, fmt, args2); +#else + // First try + int len = vsnprintf(Owned ? Data : NULL, Owned ? (size_t)(Capacity + 1): 0, fmt, args); + STR_ASSERT(len >= 0); + + if (Capacity < len) + { + reserve_discard(len); + len = vsnprintf(Data, (size_t)len + 1, fmt, args2); + } +#endif + + STR_ASSERT(Owned); + return len; +} + +int Str::setf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = setfv(fmt, args); + va_end(args); + return len; +} + +int Str::setfv_nogrow(const char* fmt, va_list args) +{ + STR_ASSERT(Owned); + + if (Capacity == 0) + return 0; + + int w = vsnprintf(Data, (size_t)(Capacity + 1), fmt, args); + Data[Capacity] = 0; + Owned = 1; + return (w == -1) ? Capacity : w; +} + +int Str::setf_nogrow(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = setfv_nogrow(fmt, args); + va_end(args); + return len; +} + +int Str::append_from(int idx, char c) +{ + int add_len = 1; + if (Capacity < idx + add_len) + reserve(idx + add_len); + Data[idx] = c; + Data[idx + add_len] = 0; + STR_ASSERT(Owned); + return add_len; +} + +int Str::append_from(int idx, const char* s, const char* s_end) +{ + if (!s_end) + s_end = s + strlen(s); + int add_len = (int)(s_end - s); + if (Capacity < idx + add_len) + reserve(idx + add_len); + memcpy(Data + idx, (const void*)s, (size_t)add_len); + Data[idx + add_len] = 0; // Our source data isn't necessarily zero terminated + STR_ASSERT(Owned); + return add_len; +} + +// FIXME: merge setfv() and appendfv()? +int Str::appendfv_from(int idx, const char* fmt, va_list args) +{ + // Needed for portability on platforms where va_list are passed by reference and modified by functions + va_list args2; + va_copy(args2, args); + + // MSVC returns -1 on overflow when writing, which forces us to do two passes + // FIXME-OPT: Find a way around that. +#ifdef _MSC_VER + int add_len = vsnprintf(NULL, 0, fmt, args); + STR_ASSERT(add_len >= 0); + + if (Capacity < idx + add_len) + reserve(idx + add_len); + add_len = vsnprintf(Data + idx, add_len + 1, fmt, args2); +#else + // First try + int add_len = vsnprintf(Owned ? Data + idx : NULL, Owned ? (size_t)(Capacity + 1 - idx) : 0, fmt, args); + STR_ASSERT(add_len >= 0); + + if (Capacity < idx + add_len) + { + reserve(idx + add_len); + add_len = vsnprintf(Data + idx, (size_t)add_len + 1, fmt, args2); + } +#endif + + STR_ASSERT(Owned); + return add_len; +} + +int Str::appendf_from(int idx, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = appendfv_from(idx, fmt, args); + va_end(args); + return len; +} + +int Str::append(char c) +{ + int cur_len = length(); + return append_from(cur_len, c); +} + +int Str::append(const char* s, const char* s_end) +{ + int cur_len = length(); + return append_from(cur_len, s, s_end); +} + +int Str::appendfv(const char* fmt, va_list args) +{ + int cur_len = length(); + return appendfv_from(cur_len, fmt, args); +} + +int Str::appendf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int len = appendfv(fmt, args); + va_end(args); + return len; +} + +#endif // #define STR_IMPLEMENTATION + +//------------------------------------------------------------------------- diff --git a/ExternalApps/Calculator/main/Source/main.cpp b/ExternalApps/Calculator/main/Source/main.cpp new file mode 100644 index 00000000..dc56360e --- /dev/null +++ b/ExternalApps/Calculator/main/Source/main.cpp @@ -0,0 +1,30 @@ +#include +#include "Calculator.h" + +static void onShow(AppHandle appHandle, void* data, lv_obj_t* parent) { + static_cast(data)->onShow(appHandle, parent); +} + +static void* createApp() { + return new Calculator(); +} + +static void destroyApp(void* app) { + delete static_cast(app); +} + +ExternalAppManifest manifest = { + .name = "Hello World", + .createData = createApp, + .destroyData = destroyApp, + .onShow = onShow, +}; + +extern "C" { + +int main(int argc, char* argv[]) { + tt_app_register(&manifest); + return 0; +} + +} diff --git a/ExternalApps/Calculator/tactility.properties b/ExternalApps/Calculator/tactility.properties new file mode 100644 index 00000000..aa5aa4a2 --- /dev/null +++ b/ExternalApps/Calculator/tactility.properties @@ -0,0 +1,2 @@ +[sdk] +version = 0.4.0 diff --git a/ExternalApps/Calculator/tactility.py b/ExternalApps/Calculator/tactility.py new file mode 100644 index 00000000..e5ec6c3a --- /dev/null +++ b/ExternalApps/Calculator/tactility.py @@ -0,0 +1,478 @@ +import configparser +import json +import os +import re +import shutil +import sys +import subprocess +import time +import urllib.request +import zipfile + +import requests + +# Targetable platforms that represent a specific hardware target +platform_targets = ["esp32", "esp32s3"] +# All valid platform commandline arguments +platform_arguments = platform_targets.copy() +platform_arguments.append("all") +ttbuild_path = ".tactility" +ttbuild_version = "1.2.0" +ttbuild_properties_file = "tactility.properties" +ttbuild_cdn = "https://cdn.tactility.one" +ttbuild_sdk_json_validity = 3600 # seconds +ttport = 6666 +verbose = False +use_local_sdk = False + +spinner_pattern = [ + "⠋", + "⠙", + "⠹", + "⠸", + "⠼", + "⠴", + "⠦", + "⠧", + "⠇", + "⠏" +] + +if sys.platform == "win32": + shell_color_red = "" + shell_color_orange = "" + shell_color_green = "" + shell_color_purple = "" + shell_color_cyan = "" + shell_color_reset = "" +else: + shell_color_red = "\033[91m" + shell_color_orange = "\033[93m" + shell_color_green = "\033[32m" + shell_color_purple = "\033[35m" + shell_color_cyan = "\033[36m" + shell_color_reset = "\033[m" + +def print_help(): + print("Usage: python tactility.py [action] [options]") + print("") + print("Actions:") + print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") + print(" esp32: ESP32") + print(" esp32s3: ESP32 S3") + print(" all: all supported ESP platforms") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print(" run [ip] [app id] Run an application") + print(" install [ip] [esp32,esp32s3] Install an application") + print("") + print("Options:") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") + +def download_file(url, filepath): + global verbose + if verbose: + print(f"Downloading from {url} to {filepath}") + request = urllib.request.Request( + url, + data=None, + headers={ + "User-Agent": f"Tactility Build Tool {ttbuild_version}" + } + ) + try: + response = urllib.request.urlopen(request) + file = open(filepath, mode="wb") + file.write(response.read()) + file.close() + return True + except OSError as error: + if verbose: + print_error(f"Failed to fetch URL {url}\n{error}") + return False + +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 exit_with_error(message): + print_error(message) + sys.exit(1) + +def get_url(ip, path): + return f"http://{ip}:{ttport}{path}" + +def is_valid_platform_name(name): + global platform_arguments + return name in platform_arguments + +def validate_environment(): + global ttbuild_properties_file, use_local_sdk + if os.environ.get("IDF_PATH") is None: + exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh") + if not os.path.exists(ttbuild_properties_file): + exit_with_error(f"{ttbuild_properties_file} file not found") + if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None: + print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.") + print_warning("If you want to use it, use the 'build local' parameters.") + elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None: + exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.") + +def setup_environment(): + global ttbuild_path + os.makedirs(ttbuild_path, exist_ok=True) + +def get_sdk_dir(version, platform): + global use_local_sdk + if use_local_sdk: + return os.environ.get("TACTILITY_SDK_PATH") + else: + global ttbuild_cdn + return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK") + +def get_sdk_version(): + global ttbuild_properties_file + parser = configparser.RawConfigParser() + parser.read(ttbuild_properties_file) + sdk_dict = dict(parser.items("sdk")) + if not "version" in sdk_dict: + exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}") + return sdk_dict["version"] + +def get_sdk_root_dir(version, platform): + global ttbuild_cdn + return os.path.join(ttbuild_path, f"{version}-{platform}") + +def get_sdk_url(version, platform): + global ttbuild_cdn + return f"{ttbuild_cdn}/TactilitySDK-{version}-{platform}.zip" + +def sdk_exists(version, platform): + sdk_dir = get_sdk_dir(version, platform) + return os.path.isdir(sdk_dir) + +def should_update_sdk_json(): + global ttbuild_cdn + json_filepath = os.path.join(ttbuild_path, "sdk.json") + if os.path.exists(json_filepath): + json_modification_time = os.path.getmtime(json_filepath) + now = time.time() + global ttbuild_sdk_json_validity + minimum_seconds_difference = ttbuild_sdk_json_validity + return (now - json_modification_time) > minimum_seconds_difference + else: + return True + +def update_sdk_json(): + global ttbuild_cdn, ttbuild_path + json_url = f"{ttbuild_cdn}/sdk.json" + json_filepath = os.path.join(ttbuild_path, "sdk.json") + return download_file(json_url, json_filepath) + +def should_fetch_sdkconfig_files(): + for platform in platform_targets: + sdkconfig_filename = f"sdkconfig.app.{platform}" + if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)): + return True + return False + +def fetch_sdkconfig_files(): + for platform in platform_targets: + sdkconfig_filename = f"sdkconfig.app.{platform}" + target_path = os.path.join(ttbuild_path, sdkconfig_filename) + if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path): + exit_with_error(f"Failed to download sdkconfig file for {platform}") + + +def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build): + version_map = sdk_json["versions"] + if not sdk_version in version_map: + exit_with_error(f"Version not found: {sdk_version}") + version_data = version_map[sdk_version] + available_platforms = version_data["platforms"] + for desired_platform in platforms_to_build: + if not desired_platform in available_platforms: + exit_with_error(f"Platform {desired_platform} is not available. Available ones: {available_platforms}") + +def validate_self(sdk_json): + if not "toolVersion" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolVersion not found)") + if not "toolCompatibility" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolCompatibility not found)") + if not "toolDownloadUrl" in sdk_json: + exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)") + tool_version = sdk_json["toolVersion"] + tool_compatibility = sdk_json["toolCompatibility"] + if tool_version != ttbuild_version: + print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})") + print_warning(f"Run 'tactility.py updateself' to update.") + if re.search(tool_compatibility, ttbuild_version) is None: + print_error("The tool is not compatible anymore.") + print_error("Run 'tactility.py updateself' to update.") + sys.exit(1) + +def sdk_download(version, platform): + sdk_root_dir = get_sdk_root_dir(version, platform) + os.makedirs(sdk_root_dir, exist_ok=True) + sdk_url = get_sdk_url(version, platform) + filepath = os.path.join(sdk_root_dir, f"{version}-{platform}.zip") + print(f"Downloading SDK version {version} for {platform}") + if download_file(sdk_url, filepath): + with zipfile.ZipFile(filepath, "r") as zip_ref: + zip_ref.extractall(os.path.join(sdk_root_dir, "TactilitySDK")) + return True + else: + return False + +def sdk_download_all(version, platforms): + for platform in platforms: + if not sdk_exists(version, platform): + if not sdk_download(version, platform): + return False + else: + if verbose: + print(f"Using cached download for SDK version {version} and platform {platform}") + return True + +def find_elf_file(platform): + build_dir = f"build-{platform}" + if os.path.exists(build_dir): + for file in os.listdir(build_dir): + if file.endswith(".app.elf"): + return os.path.join(build_dir, file) + return None + +def build_all(version, platforms, skip_build): + for platform in platforms: + # First build command must be "idf.py build", otherwise it fails to execute "idf.py elf" + # We check if the ELF file exists and run the correct command + # This can lead to code caching issues, so sometimes a clean build is required + if find_elf_file(platform) is None: + if not build_first(version, platform, skip_build): + break + else: + if not build_consecutively(version, platform, skip_build): + break + +def wait_for_build(process, platform): + buffer = [] + os.set_blocking(process.stdout.fileno(), False) + while process.poll() is None: + for i in spinner_pattern: + time.sleep(0.1) + progress_text = f"Building for {platform} {shell_color_cyan}" + str(i) + shell_color_reset + sys.stdout.write(progress_text + "\r") + while True: + line = process.stdout.readline() + decoded_line = line.decode("UTF-8") + if decoded_line != "": + buffer.append(decoded_line) + else: + break + return buffer + +# The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster. +# The problem is that the "idf.py build" always results in an error, even though the elf file is created. +# The solution is to suppress the error if we find that the elf file was created. +def build_first(version, platform, skip_build): + sdk_dir = get_sdk_dir(version, platform) + if verbose: + print(f"Using SDK at {sdk_dir}") + os.environ["TACTILITY_SDK_PATH"] = sdk_dir + sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") + os.system(f"cp {sdkconfig_path} sdkconfig") + elf_path = find_elf_file(platform) + # Remove previous elf file: re-creation of the file is used to measure if the build succeeded, + # as the actual build job will always fail due to technical issues with the elf cmake script + if elf_path is not None: + os.remove(elf_path) + if skip_build: + return True + print("Building first build") + with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + build_output = wait_for_build(process, platform) + # The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case + if process.returncode == 0: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + else: + if find_elf_file(platform) is None: + for line in build_output: + print(line, end="") + print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") + return False + else: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + +def build_consecutively(version, platform, skip_build): + sdk_dir = get_sdk_dir(version, platform) + if verbose: + print(f"Using SDK at {sdk_dir}") + os.environ["TACTILITY_SDK_PATH"] = sdk_dir + sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}") + os.system(f"cp {sdkconfig_path} sdkconfig") + if skip_build: + return True + with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + build_output = wait_for_build(process, platform) + if process.returncode == 0: + print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}") + return True + else: + for line in build_output: + print(line, end="") + print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}") + return False + +def read_sdk_json(): + json_file_path = os.path.join(ttbuild_path, "sdk.json") + json_file = open(json_file_path) + return json.load(json_file) + +def build_action(platform_arg): + # Environment validation + validate_environment() + # Environment setup + setup_environment() + platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg] + if not is_valid_platform_name(platform_arg): + print_help() + exit_with_error("Invalid platform name") + if not use_local_sdk: + if should_fetch_sdkconfig_files(): + fetch_sdkconfig_files() + sdk_json = read_sdk_json() + validate_self(sdk_json) + if not "versions" in sdk_json: + exit_with_error("Version data not found in sdk.json") + # Build + sdk_version = get_sdk_version() + if not use_local_sdk: + validate_version_and_platforms(sdk_json, 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") + build_all(sdk_version, platforms_to_build, skip_build) # Environment validation + +def clean_action(): + count = 0 + for path in os.listdir("."): + if path.startswith("build-"): + print(f"Removing {path}/") + shutil.rmtree(path) + count = count + 1 + if count == 0: + print("Nothing to clean") + +def clear_cache_action(): + if os.path.exists(ttbuild_path): + print(f"Removing {ttbuild_path}/") + shutil.rmtree(ttbuild_path) + else: + print("Nothing to clear") + +def update_self_action(): + sdk_json = read_sdk_json() + tool_download_url = sdk_json["toolDownloadUrl"] + if download_file(tool_download_url, "tactility.py"): + print("Updated") + else: + exit_with_error("Update failed") + +def get_device_info(ip): + print(f"Getting device info from {ip}") + url = get_url(ip, "/info") + try: + response = requests.get(url) + if response.status_code != 200: + print_error("Run failed") + else: + print(response.json()) + print(f"{shell_color_green}Run successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") + +def run_action(ip, app_id): + print(f"Running {app_id} on {ip}") + url = get_url(ip, "/app/run") + params = {'id': app_id} + try: + response = requests.post(url, params=params) + if response.status_code != 200: + print_error("Run failed") + else: + print(f"{shell_color_green}Run successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") + +def install_action(ip, platform): + file_path = find_elf_file(platform) + if file_path is None: + print_error(f"File not found: {file_path}") + return + print(f"Installing {file_path} to {ip}") + url = get_url(ip, "/app/install") + try: + # Prepare multipart form data + with open(file_path, 'rb') as file: + files = { + 'elf': file + } + response = requests.put(url, files=files) + if response.status_code != 200: + print_error("Install failed") + else: + print(f"{shell_color_green}Installation successful ✅{shell_color_reset}") + except requests.RequestException as e: + print_error(f"Installation failed: {e}") + except IOError as e: + print_error(f"File error: {e}") + + +if __name__ == "__main__": + print(f"Tactility Build System v{ttbuild_version}") + if "--help" in sys.argv: + print_help() + sys.exit() + # Argument validation + if len(sys.argv) == 1: + print_help() + sys.exit() + action_arg = sys.argv[1] + verbose = "--verbose" in sys.argv + skip_build = "--skip-build" in sys.argv + use_local_sdk = "--local-sdk" in sys.argv + # Update SDK cache (sdk.json) + if should_update_sdk_json() and not update_sdk_json(): + exit_with_error("Failed to retrieve SDK info") + # Actions + if action_arg == "build": + if len(sys.argv) < 3: + print_help() + exit_with_error("Commandline parameter missing") + build_action(sys.argv[2]) + elif action_arg == "clean": + clean_action() + elif action_arg == "clearcache": + clear_cache_action() + elif action_arg == "updateself": + update_self_action() + elif action_arg == "run": + if len(sys.argv) < 4: + print_help() + exit_with_error("Commandline parameter missing") + run_action(sys.argv[2], sys.argv[3]) + elif action_arg == "install": + if len(sys.argv) < 4: + print_help() + exit_with_error("Commandline parameter missing") + install_action(sys.argv[2], sys.argv[3]) + else: + print_help() + exit_with_error("Unknown commandline parameter") diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py index 310de9f3..e5ec6c3a 100644 --- a/ExternalApps/HelloWorld/tactility.py +++ b/ExternalApps/HelloWorld/tactility.py @@ -9,16 +9,19 @@ import time import urllib.request import zipfile +import requests + # Targetable platforms that represent a specific hardware target platform_targets = ["esp32", "esp32s3"] # All valid platform commandline arguments platform_arguments = platform_targets.copy() platform_arguments.append("all") ttbuild_path = ".tactility" -ttbuild_version = "1.0.0" +ttbuild_version = "1.2.0" ttbuild_properties_file = "tactility.properties" ttbuild_cdn = "https://cdn.tactility.one" ttbuild_sdk_json_validity = 3600 # seconds +ttport = 6666 verbose = False use_local_sdk = False @@ -54,19 +57,21 @@ def print_help(): print("Usage: python tactility.py [action] [options]") print("") print("Actions:") - print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") + print(" build [esp32,esp32s3,all,local] Build the app for the specified platform") print(" esp32: ESP32") print(" esp32s3: ESP32 S3") print(" all: all supported ESP platforms") - print(" clean Clean the build folders") - print(" clearcache Clear the SDK cache") - print(" updateself Update this tool") + print(" clean Clean the build folders") + print(" clearcache Clear the SDK cache") + print(" updateself Update this tool") + print(" run [ip] [app id] Run an application") + print(" install [ip] [esp32,esp32s3] Install an application") print("") print("Options:") - print(" --help Show this commandline info") - print(" --local-sdk Use SDK specifiedc by environment variable TACTILITY_SDK_PATH") - print(" --skip-build Run everything except the idf.py/CMake commands") - print(" --verbose Show extra console output") + print(" --help Show this commandline info") + print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH") + print(" --skip-build Run everything except the idf.py/CMake commands") + print(" --verbose Show extra console output") def download_file(url, filepath): global verbose @@ -100,6 +105,9 @@ def exit_with_error(message): print_error(message) sys.exit(1) +def get_url(ip, path): + return f"http://{ip}:{ttport}{path}" + def is_valid_platform_name(name): global platform_arguments return name in platform_arguments @@ -340,9 +348,6 @@ def build_action(platform_arg): if not use_local_sdk: if should_fetch_sdkconfig_files(): fetch_sdkconfig_files() - # Update SDK cache - if should_update_sdk_json() and not update_sdk_json(): - exit_with_error("Failed to retrieve SDK info") sdk_json = read_sdk_json() validate_self(sdk_json) if not "versions" in sdk_json: @@ -380,6 +385,56 @@ def update_self_action(): else: exit_with_error("Update failed") +def get_device_info(ip): + print(f"Getting device info from {ip}") + url = get_url(ip, "/info") + try: + response = requests.get(url) + if response.status_code != 200: + print_error("Run failed") + else: + print(response.json()) + print(f"{shell_color_green}Run successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") + +def run_action(ip, app_id): + print(f"Running {app_id} on {ip}") + url = get_url(ip, "/app/run") + params = {'id': app_id} + try: + response = requests.post(url, params=params) + if response.status_code != 200: + print_error("Run failed") + else: + print(f"{shell_color_green}Run successful ✅{shell_color_reset}") + except requests.RequestException as e: + print(f"Request failed: {e}") + +def install_action(ip, platform): + file_path = find_elf_file(platform) + if file_path is None: + print_error(f"File not found: {file_path}") + return + print(f"Installing {file_path} to {ip}") + url = get_url(ip, "/app/install") + try: + # Prepare multipart form data + with open(file_path, 'rb') as file: + files = { + 'elf': file + } + response = requests.put(url, files=files) + if response.status_code != 200: + print_error("Install failed") + else: + print(f"{shell_color_green}Installation successful ✅{shell_color_reset}") + except requests.RequestException as e: + print_error(f"Installation failed: {e}") + except IOError as e: + print_error(f"File error: {e}") + + if __name__ == "__main__": print(f"Tactility Build System v{ttbuild_version}") if "--help" in sys.argv: @@ -393,6 +448,9 @@ if __name__ == "__main__": verbose = "--verbose" in sys.argv skip_build = "--skip-build" in sys.argv use_local_sdk = "--local-sdk" in sys.argv + # Update SDK cache (sdk.json) + if should_update_sdk_json() and not update_sdk_json(): + exit_with_error("Failed to retrieve SDK info") # Actions if action_arg == "build": if len(sys.argv) < 3: @@ -405,6 +463,16 @@ if __name__ == "__main__": clear_cache_action() elif action_arg == "updateself": update_self_action() + elif action_arg == "run": + if len(sys.argv) < 4: + print_help() + exit_with_error("Commandline parameter missing") + run_action(sys.argv[2], sys.argv[3]) + elif action_arg == "install": + if len(sys.argv) < 4: + print_help() + exit_with_error("Commandline parameter missing") + install_action(sys.argv[2], sys.argv[3]) else: print_help() exit_with_error("Unknown commandline parameter") diff --git a/SECURITY.md b/SECURITY.md index e5273e54..b3f8a2db 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,19 @@ ## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| main branch | :white_check_mark: | +| Version | Supported | +|-------------|--------------------| +| main branch | :white_check_mark: | + +## Important project context + +Tactility is foremost a tinkering platform as opposed to a user platform. It does not have desktop OS security features +such as user/access management, file system protections, memory protections, app permissions and more. + +[ESP Privilege Separation](https://github.com/espressif/esp-privilege-separation) is not implemented nor planned to be implemented. +It is limited to C3 and S3 hardware, so it wouldn't even work on the original ESP32. + +Keep this in mind when reporting vulnerabilities. ## Reporting a Vulnerability @@ -12,6 +22,7 @@ We appreciate your efforts to responsibly disclose your findings, and we will ma To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/bytewelder/tactility/security/advisories/new) tab. -You can expect a response indicating the next steps in handling your report. After the initial reply to your report, you'll be kept informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. +You can expect a response indicating the next steps in handling your report. After the initial reply to your report, +you'll be kept informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. Report security bugs in third-party dependencies to the person or team maintaining the module. diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index 4ea05b93..cd5e5b90 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -67,7 +67,7 @@ class DevelopmentApp final : public App { if (ip.empty()) { lv_label_set_text(statusLabel, "Waiting for IP..."); } else { - std::string status = std::string("Available at ") + ip; + const std::string status = std::string("Available at ") + ip; lv_label_set_text(statusLabel, status.c_str()); } } @@ -134,6 +134,13 @@ public: statusLabel = lv_label_create(wrapper); lv_obj_align(statusLabel, LV_ALIGN_TOP_LEFT, 0, 50); + auto warning_label = lv_label_create(wrapper); + lv_label_set_text(warning_label, "This feature is experimental and uses an unsecured http connection."); + lv_obj_set_width(warning_label, LV_PCT(100)); + lv_label_set_long_mode(warning_label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(warning_label, lv_color_make(0xff, 0xff, 00), LV_STATE_DEFAULT); + lv_obj_align(warning_label, LV_ALIGN_TOP_LEFT, 0, 80); + updateViewState(); timer.start(1000); diff --git a/Tactility/Source/service/gps/GpsConfiguration.cpp b/Tactility/Source/service/gps/GpsConfiguration.cpp index 5a6ec6fd..14d03212 100644 --- a/Tactility/Source/service/gps/GpsConfiguration.cpp +++ b/Tactility/Source/service/gps/GpsConfiguration.cpp @@ -2,6 +2,7 @@ #include #include +#include using tt::hal::gps::GpsDevice; @@ -30,6 +31,13 @@ bool GpsService::getGpsConfigurations(std::vector& c return false; } + // If file does not exist, return empty list + if (access(path.c_str(), F_OK) != 0) { + TT_LOG_W(TAG, "No configurations (file not found: %s)", path.c_str()); + return true; + } + + TT_LOG_I(TAG, "Reading configuration file %s", path.c_str()); auto reader = file::ObjectFileReader(path, sizeof(hal::gps::GpsConfiguration)); if (!reader.open()) { TT_LOG_E(TAG, "Failed to open configuration file"); diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 9c038e7c..afb75cd0 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -81,11 +81,24 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(strncpy), ESP_ELFSYM_EXPORT(strcpy), ESP_ELFSYM_EXPORT(strcat), + ESP_ELFSYM_EXPORT(strchr), ESP_ELFSYM_EXPORT(strstr), ESP_ELFSYM_EXPORT(memset), ESP_ELFSYM_EXPORT(memcpy), // ctype + ESP_ELFSYM_EXPORT(isalnum), + ESP_ELFSYM_EXPORT(isalpha), + ESP_ELFSYM_EXPORT(iscntrl), ESP_ELFSYM_EXPORT(isdigit), + ESP_ELFSYM_EXPORT(isgraph), + ESP_ELFSYM_EXPORT(islower), + ESP_ELFSYM_EXPORT(isprint), + ESP_ELFSYM_EXPORT(ispunct), + ESP_ELFSYM_EXPORT(isspace), + ESP_ELFSYM_EXPORT(isupper), + ESP_ELFSYM_EXPORT(isxdigit), + ESP_ELFSYM_EXPORT(tolower), + ESP_ELFSYM_EXPORT(toupper), // ESP-IDF ESP_ELFSYM_EXPORT(esp_log_write), ESP_ELFSYM_EXPORT(esp_log_timestamp), diff --git a/TactilityCore/CMakeLists.txt b/TactilityCore/CMakeLists.txt index a55e7dab..f8c64691 100644 --- a/TactilityCore/CMakeLists.txt +++ b/TactilityCore/CMakeLists.txt @@ -9,6 +9,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Include/" + PRIV_INCLUDE_DIRS "Private/" REQUIRES mbedtls nvs_flash esp_rom esp_timer ) @@ -24,6 +25,10 @@ else() PRIVATE ${SOURCES} ) + include_directories( + PRIVATE Private/ + ) + target_include_directories(TactilityCore SYSTEM PUBLIC Include/ ) diff --git a/TactilityCore/Private/Tactility/file/ObjectFilePrivate.h b/TactilityCore/Private/Tactility/file/ObjectFilePrivate.h new file mode 100644 index 00000000..8710ed4e --- /dev/null +++ b/TactilityCore/Private/Tactility/file/ObjectFilePrivate.h @@ -0,0 +1,19 @@ +#pragma once + +namespace tt::file { + +constexpr uint32_t OBJECT_FILE_IDENTIFIER = 0x13371337; +constexpr uint32_t OBJECT_FILE_VERSION = 1; + +struct FileHeader { + uint32_t identifier = OBJECT_FILE_IDENTIFIER; + uint32_t version = OBJECT_FILE_VERSION; +}; + +struct ContentHeader { + uint32_t recordVersion = 0; + uint32_t recordSize = 0; + uint32_t recordCount = 0; +}; + +} diff --git a/TactilityCore/Source/file/ObjectFileReader.cpp b/TactilityCore/Source/file/ObjectFileReader.cpp new file mode 100644 index 00000000..94012901 --- /dev/null +++ b/TactilityCore/Source/file/ObjectFileReader.cpp @@ -0,0 +1,78 @@ +#include "Tactility/file/ObjectFile.h" +#include "Tactility/file/ObjectFilePrivate.h" + +#include +#include + +namespace tt::file { + +constexpr const char* TAG = "ObjectFileReader"; + +bool ObjectFileReader::open() { + auto opening_file = std::unique_ptr(fopen(filePath.c_str(), "r")); + if (opening_file == nullptr) { + TT_LOG_E(TAG, "Failed to open file %s", filePath.c_str()); + return false; + } + + FileHeader file_header; + if (fread(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to read file header from %s", filePath.c_str()); + return false; + } + + if (file_header.identifier != OBJECT_FILE_IDENTIFIER) { + TT_LOG_E(TAG, "Invalid file type for %s", filePath.c_str()); + return false; + } + + if (file_header.version != OBJECT_FILE_VERSION) { + TT_LOG_E(TAG, "Unknown version for %s: %lu", filePath.c_str(), file_header.identifier); + return false; + } + + ContentHeader content_header; + if (fread(&content_header, sizeof(ContentHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to read content header from %s", filePath.c_str()); + return false; + } + + if (recordSize != content_header.recordSize) { + TT_LOG_E(TAG, "Record size mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordSize, content_header.recordSize); + return false; + } + + recordCount = content_header.recordCount; + recordVersion = content_header.recordVersion; + + file = std::move(opening_file); + + TT_LOG_D(TAG, "File version: %lu", file_header.version); + TT_LOG_D(TAG, "Content: version = %lu, size = %lu bytes, count = %lu", content_header.recordVersion, content_header.recordSize, content_header.recordCount); + + return true; +} + +void ObjectFileReader::close() { + recordCount = 0; + recordVersion = 0; + recordsRead = 0; + + file = nullptr; +} + +bool ObjectFileReader::readNext(void* output) { + if (file == nullptr) { + TT_LOG_E(TAG, "File not open"); + return false; + } + + bool result = fread(output, recordSize, 1, file.get()) == 1; + if (result) { + recordsRead++; + } + + return result; +} + +} \ No newline at end of file diff --git a/TactilityCore/Source/file/ObjectFile.cpp b/TactilityCore/Source/file/ObjectFileWriter.cpp similarity index 56% rename from TactilityCore/Source/file/ObjectFile.cpp rename to TactilityCore/Source/file/ObjectFileWriter.cpp index 9ee024da..293ff350 100644 --- a/TactilityCore/Source/file/ObjectFile.cpp +++ b/TactilityCore/Source/file/ObjectFileWriter.cpp @@ -1,107 +1,25 @@ #include "Tactility/file/ObjectFile.h" +#include "Tactility/file/ObjectFilePrivate.h" +#include #include #include namespace tt::file { -constexpr const char* TAG = "ObjectFile"; - -constexpr uint32_t OBJECT_FILE_IDENTIFIER = 0x13371337; -constexpr uint32_t OBJECT_FILE_VERSION = 1; - -struct FileHeader { - uint32_t identifier = OBJECT_FILE_IDENTIFIER; - uint32_t version = OBJECT_FILE_VERSION; -}; - -struct ContentHeader { - uint32_t recordVersion = 0; - uint32_t recordSize = 0; - uint32_t recordCount = 0; -}; - -// region Reader - -bool ObjectFileReader::open() { - auto opening_file = std::unique_ptr(fopen(filePath.c_str(), "r")); - if (opening_file == nullptr) { - TT_LOG_E(TAG, "Failed to open file %s", filePath.c_str()); - return false; - } - - FileHeader file_header; - if (fread(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) { - TT_LOG_E(TAG, "Failed to read file header from %s", filePath.c_str()); - return false; - } - - if (file_header.identifier != OBJECT_FILE_IDENTIFIER) { - TT_LOG_E(TAG, "Invalid file type for %s", filePath.c_str()); - return false; - } - - if (file_header.version != OBJECT_FILE_VERSION) { - TT_LOG_E(TAG, "Unknown version for %s: %lu", filePath.c_str(), file_header.identifier); - return false; - } - - ContentHeader content_header; - if (fread(&content_header, sizeof(ContentHeader), 1, opening_file.get()) != 1) { - TT_LOG_E(TAG, "Failed to read content header from %s", filePath.c_str()); - return false; - } - - if (recordSize != content_header.recordSize) { - TT_LOG_E(TAG, "Record size mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordSize, content_header.recordSize); - return false; - } - - recordCount = content_header.recordCount; - recordVersion = content_header.recordVersion; - - file = std::move(opening_file); - - TT_LOG_D(TAG, "File version: %lu", file_header.version); - TT_LOG_D(TAG, "Content: version = %lu, size = %lu bytes, count = %lu", content_header.recordVersion, content_header.recordSize, content_header.recordCount); - - return true; -} - -void ObjectFileReader::close() { - recordCount = 0; - recordVersion = 0; - recordsRead = 0; - - file = nullptr; -} - -bool ObjectFileReader::readNext(void* output) { - if (file == nullptr) { - TT_LOG_E(TAG, "File not open"); - return false; - } - - bool result = fread(output, recordSize, 1, file.get()) == 1; - if (result) { - recordsRead++; - } - - return result; -} - -// endregion Reader - -// region Writer +constexpr const char* TAG = "ObjectFileWriter"; bool ObjectFileWriter::open() { - // If file exists bool edit_existing = append && access(filePath.c_str(), F_OK) == 0; + if (append && !edit_existing) { + TT_LOG_W(TAG, "access() to %s failed: %s", filePath.c_str(), strerror(errno)); + } + // Edit existing or create a new file auto* mode = edit_existing ? "r+" : "w"; - auto opening_file = std::unique_ptr(fopen(filePath.c_str(), mode)); + auto opening_file = std::unique_ptr(std::fopen(filePath.c_str(), mode)); if (opening_file == nullptr) { - TT_LOG_E(TAG, "Failed to open file %s", filePath.c_str()); + TT_LOG_E(TAG, "Failed to open file %s in %s mode", filePath.c_str(), mode); return false; }