Compare commits
5 Commits
a4d15b2a1e
...
dcf28d0868
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcf28d0868 | ||
|
|
9cc58099b4 | ||
|
|
1216862aec | ||
|
|
7ad0a3cb04 | ||
|
|
bab3eb19bc |
@ -17,7 +17,7 @@ class HelloWorldApp : public App {
|
||||
};
|
||||
|
||||
extern const AppManifest hello_world_app = {
|
||||
.id = "HelloWorld",
|
||||
.name = "Hello World",
|
||||
.appId = "HelloWorld",
|
||||
.appName = "Hello World",
|
||||
.createApp = create<HelloWorldApp>
|
||||
};
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
#include "CYD4848S040C.h"
|
||||
|
||||
#include "Tactility/kernel/SystemEvents.h"
|
||||
#include "Tactility/lvgl/LvglSync.h"
|
||||
#include "devices/St7701Display.h"
|
||||
#include "devices/SdCard.h"
|
||||
|
||||
@ -12,7 +15,7 @@ static bool initBoot() {
|
||||
|
||||
static DeviceVector createDevices() {
|
||||
return {
|
||||
std::reinterpret_pointer_cast<Device>(std::make_shared<St7701Display>()),
|
||||
std::make_shared<St7701Display>(),
|
||||
createSdCard()
|
||||
};
|
||||
}
|
||||
@ -59,7 +62,7 @@ const Configuration cyd_4848s040c_config = {
|
||||
}
|
||||
},
|
||||
.spi {
|
||||
//SD Card
|
||||
// SD Card & display init
|
||||
spi::Configuration {
|
||||
.device = SPI2_HOST,
|
||||
.dma = SPI_DMA_CH_AUTO,
|
||||
@ -68,20 +71,20 @@ const Configuration cyd_4848s040c_config = {
|
||||
.miso_io_num = GPIO_NUM_41,
|
||||
.sclk_io_num = GPIO_NUM_48,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.quadhd_io_num = GPIO_NUM_42,
|
||||
.data4_io_num = -1,
|
||||
.data5_io_num = -1,
|
||||
.data6_io_num = -1,
|
||||
.data7_io_num = -1,
|
||||
.data_io_default_level = false,
|
||||
.max_transfer_sz = 8192,
|
||||
.max_transfer_sz = 1024 * 128,
|
||||
.flags = 0,
|
||||
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
|
||||
.intr_flags = 0
|
||||
},
|
||||
.initMode = spi::InitMode::ByTactility,
|
||||
.isMutable = false,
|
||||
.lock = nullptr
|
||||
.lock = tt::lvgl::getSyncLock()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -11,7 +11,9 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
tt::lvgl::getSyncLock(),
|
||||
std::vector { GPIO_NUM_39 }
|
||||
);
|
||||
|
||||
auto sdcard = std::make_shared<SpiSdCardDevice>(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#include "TpagerEncoder.h"
|
||||
|
||||
#include <Tactility/Log.h>
|
||||
#include <Tactility/hal/Gpio.h>
|
||||
#include <driver/gpio.h>
|
||||
|
||||
constexpr auto* TAG = "TpagerEncoder";
|
||||
constexpr auto ENCODER_A = GPIO_NUM_40;
|
||||
|
||||
@ -11,7 +11,7 @@ std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable createTouch() {
|
||||
240,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
GPIO_NUM_13,
|
||||
GPIO_NUM_5
|
||||
);
|
||||
|
||||
@ -1,39 +1,32 @@
|
||||
# Contributing
|
||||
|
||||
## Accepted changes
|
||||
## New features and boards
|
||||
|
||||
Before releasing version 1.0.0, the APIs are changing rapidly.
|
||||
I want to minimize the amount of changes that I have to maintain, because it limits the amount of breaking changes that I have to deal with when the APIs change.
|
||||
Feel free to open an [issue](https://github.com/ByteWelder/Tactility/issues/new)
|
||||
to discuss ideas you have regarding the implementation of new boards or features.
|
||||
|
||||
### New features
|
||||
Keep in mind that the internal APIs are changing rapidly. They might change considerably in a short timespan.
|
||||
This means it's likely that you get merge conflicts while developing new features or boards.
|
||||
|
||||
These are currently not accepted.
|
||||
## Fixing things
|
||||
|
||||
### Visual / interaction design changes
|
||||
|
||||
These are currently not accepted. I'm aware that there is a lot of room for improvement, but I want to mainly focusing on strong technical foundations before
|
||||
I start building further on top of that.
|
||||
Feel free to open an [issue](https://github.com/ByteWelder/Tactility/issues/new) to discuss ideas you have, if you feel like they will have a considerable impact.
|
||||
|
||||
### Fixing things
|
||||
|
||||
The general take here is that minor changes are not accepted at this stage. I don't want to spend my time reviewing and discussing these during the current stage of the project.
|
||||
The general take here is that minor changes are not accepted at this stage.
|
||||
I don't want to spend my time reviewing and discussing these during the current stage of the project.
|
||||
|
||||
Only fixes for serious issues are accepted, like:
|
||||
|
||||
- Bugs
|
||||
- Crashes
|
||||
- Security issues
|
||||
|
||||
Some examples of non-serious issues include:
|
||||
- Documentation changes
|
||||
|
||||
- Code documentation changes
|
||||
- Renaming code
|
||||
- Fixes for compiler warnings that have low impact on the actual application logic (e.g. only happens on simulator when calling a logging function)
|
||||
- Typographical errors
|
||||
- Fixes for compiler warnings that have a low impact on the actual application logic (e.g. only happens on simulator when calling a logging function)
|
||||
|
||||
### New board implementations
|
||||
|
||||
Please open an [issue](https://github.com/ByteWelder/Tactility/issues/new) on GitHub to discuss new boards.
|
||||
|
||||
### Anything that doesn't fall in the above categories?
|
||||
## Anything that doesn't fall in the above categories?
|
||||
|
||||
Please [contact me](https://tactility.one/#/support) first!
|
||||
|
||||
@ -42,6 +35,13 @@ Please [contact me](https://tactility.one/#/support) first!
|
||||
Pull requests should only contain a single set of changes that are related to each other.
|
||||
That way, an approved set of changes will not be blocked by an unapproved set of changes.
|
||||
|
||||
## Licensing
|
||||
|
||||
All contributions to a Tactility (sub)project will be licensed under the license(s) of that project.
|
||||
|
||||
When third party code is used, its license must be included.
|
||||
It's important that these third-party licenses are compatible with the relevant Tactility (sub)projects.
|
||||
|
||||
## Code Style
|
||||
|
||||
See [this document](CODING_STYLE.md) and [.clang-format](.clang-format).
|
||||
|
||||
|
Before Width: | Height: | Size: 753 B After Width: | Height: | Size: 753 B |
|
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 528 B |
|
Before Width: | Height: | Size: 142 B After Width: | Height: | Size: 142 B |
|
Before Width: | Height: | Size: 144 B After Width: | Height: | Size: 144 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 196 B |
|
Before Width: | Height: | Size: 394 B After Width: | Height: | Size: 394 B |
|
Before Width: | Height: | Size: 407 B After Width: | Height: | Size: 407 B |
|
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 524 B |
|
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 517 B |
|
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 534 B |
@ -15,9 +15,20 @@
|
||||
Write the user choice to a file on the card.
|
||||
File contains 3 statuses: ignore, data, .. initdata?
|
||||
The latter is used for auto-selecting it as data partition.
|
||||
- Support direct installation of an `.app` file with `tactility.py install helloworld.app <ip>`
|
||||
- Support `tactility.py target <ip>` to remember the device IP address.
|
||||
- External app error code 22 should warn that the user might've forgotten a `main()` entry point
|
||||
- Bug: `Buildscript/release-sdk-current.sh` should delete the currently released SDK. It should probably also output it with versioning and target platform naming so it can be referred to as if it is a real release.
|
||||
- Tactility docs: external app dev guide should explain [debugging](https://docs.zephyrproject.org/latest/services/llext/debug.html)
|
||||
- elf_loader changes/suggestions:
|
||||
- Make entry-point optional (so we can build libraries, or have the `manifest` as a global symbol)
|
||||
- Implement support for alternative symbol lists. e.g. a function pointer that resolves a single symbol.
|
||||
- Implement the entire list of [soft-float library functions](https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html) to `tt_init.cpp`
|
||||
- `tactility.py` should stop running applications when it is: uninstalling, installing, or running an application that is already running.
|
||||
|
||||
## Medium Priority
|
||||
|
||||
- TactilityTool: Make API compatibility table (and check for compatibility in the tool itself)
|
||||
- Improve EspLcdDisplay to contain all the standard configuration options, and implement a default init function. Add a configuration class.
|
||||
- Statusbar icon that shows low/critical memory warnings
|
||||
- Make WiFi setup app that starts an access point and hosts a webpage to set up the device.
|
||||
@ -28,6 +39,7 @@
|
||||
- Bug: Turn on WiFi (when testing it wasn't connected/connecting - just active). Open chat. Observe crash.
|
||||
- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device)
|
||||
- I2C app should show error when I2C port is disabled when the scan button was manually pressed
|
||||
- TactilitySDK: Support automatic scanning of header files so that we can generate the `tt_init.cpp` symbols list.
|
||||
|
||||
## Lower Priority
|
||||
|
||||
@ -93,6 +105,7 @@
|
||||
- RSS reader
|
||||
- Static file web server (with option to specify path and port)
|
||||
- Diceware
|
||||
- Port TamaFi https://github.com/cifertech/TamaFi
|
||||
|
||||
# App Store
|
||||
|
||||
|
||||
2
ExternalApps/Calculator/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
build*/
|
||||
.tactility/
|
||||
@ -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(Calculator)
|
||||
tactility_project(Calculator)
|
||||
@ -1,6 +0,0 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK
|
||||
)
|
||||
@ -1,204 +0,0 @@
|
||||
#include "Calculator.h"
|
||||
#include "Stack.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctype.h>
|
||||
#include <tt_lvgl_toolbar.h>
|
||||
|
||||
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<Calculator*>(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<Str> Calculator::infixToRPN(const Str& infix) {
|
||||
Stack<char> opStack;
|
||||
Dequeue<Str> 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<Str> rpnQueue) {
|
||||
Stack<double> 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_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
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, 2, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(buttonmatrix, lv_palette_main(LV_PALETTE_BLUE), LV_PART_ITEMS);
|
||||
|
||||
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);
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "tt_app.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#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<Str> infixToRPN(const Str& infix);
|
||||
static double evaluateRPN(Dequeue<Str> rpnQueue);
|
||||
void resetCalculator();
|
||||
|
||||
public:
|
||||
|
||||
void onShow(AppHandle context, lv_obj_t* parent);
|
||||
};
|
||||
@ -1,95 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
template <typename DataType>
|
||||
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; }
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Dequeue.h"
|
||||
|
||||
template <typename DataType>
|
||||
class Stack {
|
||||
|
||||
Dequeue<DataType> 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(); }
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
#define STR_IMPLEMENTATION
|
||||
#include "Str.h"
|
||||
@ -1,618 +0,0 @@
|
||||
// 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<N> 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 <string> 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 <cstdio>
|
||||
#endif
|
||||
#ifndef STR_MEMFREE
|
||||
#define STR_MEMFREE free
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
#ifndef STR_ASSERT
|
||||
#define STR_ASSERT assert
|
||||
#include <cassert>
|
||||
#endif
|
||||
#ifndef STR_API
|
||||
#define STR_API
|
||||
#endif
|
||||
#include <cstdarg> // for va_list
|
||||
#include <cstring> // 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 <stdio.h> // 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
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
@ -1,29 +0,0 @@
|
||||
#include <tt_app.h>
|
||||
#include "Calculator.h"
|
||||
|
||||
static void onShow(AppHandle appHandle, void* data, lv_obj_t* parent) {
|
||||
static_cast<Calculator*>(data)->onShow(appHandle, parent);
|
||||
}
|
||||
|
||||
static void* createApp() {
|
||||
return new Calculator();
|
||||
}
|
||||
|
||||
static void destroyApp(void* app) {
|
||||
delete static_cast<Calculator*>(app);
|
||||
}
|
||||
|
||||
ExternalAppManifest manifest = {
|
||||
.createData = createApp,
|
||||
.destroyData = destroyApp,
|
||||
.onShow = onShow,
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
tt_app_register(&manifest);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT1
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=com.bytewelder.calculator
|
||||
version=0.1.0
|
||||
name=Calculator
|
||||
description=Math is cool
|
||||
[author]
|
||||
name=ByteWelder
|
||||
website=https://bytewelder.com
|
||||
@ -1,637 +0,0 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import requests
|
||||
import tarfile
|
||||
import shutil
|
||||
import configparser
|
||||
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "2.1.1"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
valid_platforms = ["esp32", "esp32s3"]
|
||||
|
||||
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] Build the app. Optionally specify a platform.")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] Run the application")
|
||||
print(" install [ip] Install the application")
|
||||
print(" uninstall [ip] Uninstall the application")
|
||||
print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.")
|
||||
print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
|
||||
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")
|
||||
|
||||
# region Core
|
||||
|
||||
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 read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
#endregion Core
|
||||
|
||||
#region SDK helpers
|
||||
|
||||
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 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_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(platform_targets):
|
||||
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(platform_targets):
|
||||
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}")
|
||||
|
||||
#endregion SDK helpers
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
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("manifest.properties"):
|
||||
exit_with_error("manifest.properties 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 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)
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Manifest
|
||||
|
||||
def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
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 "version" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] version not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
if not "description" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] description not found")
|
||||
# [author]
|
||||
if not "author" in manifest:
|
||||
exit_with_error("Invalid manifest format: [author] not found")
|
||||
if not "name" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] name not found")
|
||||
if not "website" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] website not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
if not is_valid_manifest_platform(manifest, platform):
|
||||
exit_with_error(f"Platform {platform} is not available in the manifest.")
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
|
||||
#endregion Manifest
|
||||
|
||||
#region SDK download
|
||||
|
||||
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
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
return os.path.join("build", f"cmake-build-{platform}")
|
||||
|
||||
def find_elf_file(platform):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(cmake_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")
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "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
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "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
|
||||
|
||||
#endregion Building
|
||||
|
||||
#region Packaging
|
||||
|
||||
def package_intermediate_manifest(target_path):
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
print_error("manifest.properties not found")
|
||||
return
|
||||
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
|
||||
|
||||
def package_intermediate_binaries(target_path, platforms):
|
||||
elf_dir = os.path.join(target_path, "elf")
|
||||
os.makedirs(elf_dir, exist_ok=True)
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found at {elf_path}")
|
||||
return
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
if os.path.isdir("assets"):
|
||||
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
|
||||
|
||||
def package_intermediate(platforms):
|
||||
target_path = os.path.join("build", "package-intermediate")
|
||||
if os.path.isdir(target_path):
|
||||
shutil.rmtree(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
package_intermediate_manifest(target_path)
|
||||
package_intermediate_binaries(target_path, platforms)
|
||||
package_intermediate_assets(target_path)
|
||||
|
||||
def package_name(platforms):
|
||||
elf_path = find_elf_file(platforms[0])
|
||||
elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf")
|
||||
return os.path.join("build", f"{elf_base_name}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
print("Packaging app")
|
||||
package_intermediate(platforms)
|
||||
# Create build/something.app
|
||||
tar_path = package_name(platforms)
|
||||
tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT)
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
tar.close()
|
||||
|
||||
#endregion Packaging
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
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 = manifest["target"]["sdk"]
|
||||
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
|
||||
if not skip_build:
|
||||
package_all(platforms_to_build)
|
||||
|
||||
def clean_action():
|
||||
if os.path.exists("build"):
|
||||
print(f"Removing build/")
|
||||
shutil.rmtree("build")
|
||||
else:
|
||||
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(manifest, ip):
|
||||
app_id = manifest["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, platforms):
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
exit_with_error(f"ELF file not built for {platform}")
|
||||
package_path = package_name(platforms)
|
||||
print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
# Prepare multipart form data
|
||||
with open(package_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}")
|
||||
|
||||
def uninstall_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
print(f"Uninstalling {app_id} on {ip}")
|
||||
url = get_url(ip, "/app/uninstall")
|
||||
params = {'id': app_id}
|
||||
try:
|
||||
response = requests.put(url, params=params)
|
||||
if response.status_code != 200:
|
||||
print_error("Uninstall failed")
|
||||
else:
|
||||
print(f"{shell_color_green}Uninstall successful ✅{shell_color_reset}")
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
#region Main
|
||||
|
||||
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
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
# 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) < 2:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
if len(sys.argv) > 2:
|
||||
platform = sys.argv[2]
|
||||
build_action(manifest, platform)
|
||||
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) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
run_action(manifest, sys.argv[2])
|
||||
elif action_arg == "install":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "uninstall":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
uninstall_action(manifest, sys.argv[2])
|
||||
elif action_arg == "bir" or action_arg == "brrr":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
build_action(manifest, platform)
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
|
||||
#endregion Main
|
||||
2
ExternalApps/GraphicsDemo/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
build*/
|
||||
.tactility/
|
||||
@ -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(GraphicsDemo)
|
||||
tactility_project(GraphicsDemo)
|
||||
@ -1,7 +0,0 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Include"
|
||||
REQUIRES TactilitySDK
|
||||
)
|
||||
@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "drivers/DisplayDriver.h"
|
||||
#include "drivers/TouchDriver.h"
|
||||
|
||||
void runApplication(DisplayDriver* display, TouchDriver* touch);
|
||||
@ -1,125 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <esp_log.h>
|
||||
#include "drivers/Colors.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <tt_hal_display.h>
|
||||
|
||||
class PixelBuffer {
|
||||
uint16_t pixelWidth;
|
||||
uint16_t pixelHeight;
|
||||
ColorFormat colorFormat;
|
||||
uint8_t* data;
|
||||
|
||||
public:
|
||||
|
||||
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) :
|
||||
pixelWidth(pixelWidth),
|
||||
pixelHeight(pixelHeight),
|
||||
colorFormat(colorFormat)
|
||||
{
|
||||
data = static_cast<uint8_t*>(malloc(pixelWidth * pixelHeight * getPixelSize()));
|
||||
assert(data != nullptr);
|
||||
}
|
||||
|
||||
~PixelBuffer() {
|
||||
free(data);
|
||||
}
|
||||
|
||||
uint16_t getPixelWidth() const {
|
||||
return pixelWidth;
|
||||
}
|
||||
|
||||
uint16_t getPixelHeight() const {
|
||||
return pixelHeight;
|
||||
}
|
||||
|
||||
ColorFormat getColorFormat() const {
|
||||
return colorFormat;
|
||||
}
|
||||
|
||||
void* getData() const {
|
||||
return data;
|
||||
}
|
||||
|
||||
uint32_t getDataSize() const {
|
||||
return pixelWidth * pixelHeight * getPixelSize();
|
||||
}
|
||||
|
||||
void* getDataAtRow(uint16_t row) const {
|
||||
auto address = reinterpret_cast<uint32_t>(data) + (row * getRowDataSize());
|
||||
return reinterpret_cast<void*>(address);
|
||||
}
|
||||
|
||||
uint16_t getRowDataSize() const {
|
||||
return pixelWidth * getPixelSize();
|
||||
}
|
||||
|
||||
uint8_t getPixelSize() const {
|
||||
switch (colorFormat) {
|
||||
case COLOR_FORMAT_MONOCHROME:
|
||||
return 1;
|
||||
case COLOR_FORMAT_BGR565:
|
||||
case COLOR_FORMAT_BGR565_SWAPPED:
|
||||
case COLOR_FORMAT_RGB565:
|
||||
case COLOR_FORMAT_RGB565_SWAPPED:
|
||||
return 2;
|
||||
case COLOR_FORMAT_RGB888:
|
||||
return 3;
|
||||
default:
|
||||
// TODO: Crash with error
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* getPixelAddress(uint16_t x, uint16_t y) const {
|
||||
uint32_t offset = ((y * getPixelWidth()) + x) * getPixelSize();
|
||||
uint32_t address = reinterpret_cast<uint32_t>(data) + offset;
|
||||
return reinterpret_cast<uint8_t*>(address);
|
||||
}
|
||||
|
||||
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
|
||||
auto address = getPixelAddress(x, y);
|
||||
switch (colorFormat) {
|
||||
case COLOR_FORMAT_MONOCHROME:
|
||||
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
|
||||
break;
|
||||
case COLOR_FORMAT_BGR565:
|
||||
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||
break;
|
||||
case COLOR_FORMAT_BGR565_SWAPPED: {
|
||||
// TODO: Make proper conversion function
|
||||
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||
uint8_t temp = *address;
|
||||
*address = *(address + 1);
|
||||
*(address + 1) = temp;
|
||||
break;
|
||||
}
|
||||
case COLOR_FORMAT_RGB565: {
|
||||
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||
break;
|
||||
}
|
||||
case COLOR_FORMAT_RGB565_SWAPPED: {
|
||||
// TODO: Make proper conversion function
|
||||
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
|
||||
uint8_t temp = *address;
|
||||
*address = *(address + 1);
|
||||
*(address + 1) = temp;
|
||||
break;
|
||||
}
|
||||
case COLOR_FORMAT_RGB888: {
|
||||
uint8_t pixel[3] = { r, g, b };
|
||||
memcpy(address, pixel, 3);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// NO-OP
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void clear(int value = 0) const {
|
||||
memset(data, value, getDataSize());
|
||||
}
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
class Colors {
|
||||
|
||||
public:
|
||||
|
||||
static void rgb888ToRgb565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* rgb565) {
|
||||
uint16_t _rgb565 = (red >> 3);
|
||||
_rgb565 = (_rgb565 << 6) | (green >> 2);
|
||||
_rgb565 = (_rgb565 << 5) | (blue >> 3);
|
||||
*rgb565 = _rgb565;
|
||||
}
|
||||
|
||||
static void rgb888ToBgr565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* bgr565) {
|
||||
uint16_t _bgr565 = (blue >> 3);
|
||||
_bgr565 = (_bgr565 << 6) | (green >> 2);
|
||||
_bgr565 = (_bgr565 << 5) | (red >> 3);
|
||||
*bgr565 = _bgr565;
|
||||
}
|
||||
|
||||
static void rgb565ToRgb888(uint16_t rgb565, uint32_t* rgb888) {
|
||||
uint32_t _rgb565 = rgb565;
|
||||
uint8_t b = (_rgb565 >> 8) & 0xF8;
|
||||
uint8_t g = (_rgb565 >> 3) & 0xFC;
|
||||
uint8_t r = (_rgb565 << 3) & 0xF8;
|
||||
|
||||
uint8_t* r8p = reinterpret_cast<uint8_t*>(rgb888);
|
||||
uint8_t* g8p = r8p + 1;
|
||||
uint8_t* b8p = r8p + 2;
|
||||
|
||||
*r8p = r | ((r >> 3) & 0x7);
|
||||
*g8p = g | ((g >> 2) & 0x3);
|
||||
*b8p = b | ((b >> 3) & 0x7);
|
||||
}
|
||||
};
|
||||
@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <tt_hal_display.h>
|
||||
|
||||
/**
|
||||
* Wrapper for tt_hal_display_driver_*
|
||||
*/
|
||||
class DisplayDriver {
|
||||
|
||||
DisplayDriverHandle handle = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
explicit DisplayDriver(DeviceId id) {
|
||||
assert(tt_hal_display_driver_supported(id));
|
||||
handle = tt_hal_display_driver_alloc(id);
|
||||
assert(handle != nullptr);
|
||||
}
|
||||
|
||||
~DisplayDriver() {
|
||||
tt_hal_display_driver_free(handle);
|
||||
}
|
||||
|
||||
bool lock(TickType timeout = TT_MAX_TICKS) const {
|
||||
return tt_hal_display_driver_lock(handle, timeout);
|
||||
}
|
||||
|
||||
void unlock() const {
|
||||
tt_hal_display_driver_unlock(handle);
|
||||
}
|
||||
|
||||
uint16_t getWidth() const {
|
||||
return tt_hal_display_driver_get_pixel_width(handle);
|
||||
}
|
||||
|
||||
uint16_t getHeight() const {
|
||||
return tt_hal_display_driver_get_pixel_height(handle);
|
||||
}
|
||||
|
||||
ColorFormat getColorFormat() const {
|
||||
return tt_hal_display_driver_get_colorformat(handle);
|
||||
}
|
||||
|
||||
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
|
||||
tt_hal_display_driver_draw_bitmap(handle, xStart, yStart, xEnd, yEnd, pixelData);
|
||||
}
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <tt_hal_touch.h>
|
||||
|
||||
/**
|
||||
* Wrapper for tt_hal_touch_driver_*
|
||||
*/
|
||||
class TouchDriver {
|
||||
|
||||
TouchDriverHandle handle = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
explicit TouchDriver(DeviceId id) {
|
||||
assert(tt_hal_touch_driver_supported(id));
|
||||
handle = tt_hal_touch_driver_alloc(id);
|
||||
assert(handle != nullptr);
|
||||
}
|
||||
|
||||
~TouchDriver() {
|
||||
tt_hal_touch_driver_free(handle);
|
||||
}
|
||||
|
||||
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const {
|
||||
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount);
|
||||
}
|
||||
};
|
||||
@ -1,72 +0,0 @@
|
||||
#include "Application.h"
|
||||
#include "PixelBuffer.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include <tt_kernel.h>
|
||||
|
||||
constexpr auto TAG = "Application";
|
||||
|
||||
static bool isTouched(TouchDriver* touch) {
|
||||
uint16_t x, y, strength;
|
||||
uint8_t pointCount = 0;
|
||||
return touch->getTouchedPoints(&x, &y, &strength, &pointCount, 1);
|
||||
}
|
||||
|
||||
void createRgbRow(PixelBuffer& buffer) {
|
||||
uint8_t offset = buffer.getPixelWidth() / 3;
|
||||
for (int i = 0; i < buffer.getPixelWidth(); ++i) {
|
||||
if (i < offset) {
|
||||
buffer.setPixel(i, 0, 255, 0, 0);
|
||||
} else if (i < offset * 2) {
|
||||
buffer.setPixel(i, 0, 0, 255, 0);
|
||||
} else {
|
||||
buffer.setPixel(i, 0, 0, 0, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createRgbFadingRow(PixelBuffer& buffer) {
|
||||
uint8_t stroke = buffer.getPixelWidth() / 3;
|
||||
for (int i = 0; i < buffer.getPixelWidth(); ++i) {
|
||||
if (i < stroke) {
|
||||
auto color = i * 255 / stroke;
|
||||
buffer.setPixel(i, 0, color, 0, 0);
|
||||
} else if (i < stroke * 2) {
|
||||
auto color = (i - stroke) * 255 / stroke;
|
||||
buffer.setPixel(i, 0, 0, color, 0);
|
||||
} else {
|
||||
auto color = (i - (2*stroke)) * 255 / stroke;
|
||||
buffer.setPixel(i, 0, 0, 0, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void runApplication(DisplayDriver* display, TouchDriver* touch) {
|
||||
// Single row buffers
|
||||
PixelBuffer line_clear_buffer(display->getWidth(), 1, display->getColorFormat());
|
||||
line_clear_buffer.clear();
|
||||
PixelBuffer line_buffer(display->getWidth(), 1, display->getColorFormat());
|
||||
line_buffer.clear();
|
||||
|
||||
do {
|
||||
// Draw row by row
|
||||
// This is placed in a loop to test the SPI locking mechanismss
|
||||
for (int i = 0; i < display->getHeight(); i++) {
|
||||
|
||||
if (i == 0) {
|
||||
createRgbRow(line_buffer);
|
||||
} else if (i == display->getHeight() / 2) {
|
||||
createRgbFadingRow(line_buffer);
|
||||
}
|
||||
|
||||
display->lock();
|
||||
display->drawBitmap(0, i, display->getWidth(), i + 1, line_buffer.getData());
|
||||
display->unlock();
|
||||
}
|
||||
|
||||
// Give other tasks space to breathe
|
||||
// SPI displays would otherwise time out SPI SD card access
|
||||
tt_kernel_delay_ticks(1);
|
||||
} while (!isTouched(touch));
|
||||
}
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
#include "Application.h"
|
||||
#include "drivers/DisplayDriver.h"
|
||||
#include "drivers/TouchDriver.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
#include <tt_app.h>
|
||||
#include <tt_app_alertdialog.h>
|
||||
#include <tt_lvgl.h>
|
||||
|
||||
constexpr auto TAG = "Main";
|
||||
|
||||
/** Find a DisplayDevice that supports the DisplayDriver interface */
|
||||
static bool findUsableDisplay(DeviceId& deviceId) {
|
||||
uint16_t display_count = 0;
|
||||
if (!tt_hal_device_find(DEVICE_TYPE_DISPLAY, &deviceId, &display_count, 1)) {
|
||||
ESP_LOGE(TAG, "No display device found");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tt_hal_display_driver_supported(deviceId)) {
|
||||
ESP_LOGE(TAG, "Display doesn't support driver mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Find a TouchDevice that supports the TouchDriver interface */
|
||||
static bool findUsableTouch(DeviceId& deviceId) {
|
||||
uint16_t touch_count = 0;
|
||||
if (!tt_hal_device_find(DEVICE_TYPE_TOUCH, &deviceId, &touch_count, 1)) {
|
||||
ESP_LOGE(TAG, "No touch device found");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tt_hal_touch_driver_supported(deviceId)) {
|
||||
ESP_LOGE(TAG, "Touch doesn't support driver mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void onCreate(AppHandle appHandle, void* data) {
|
||||
DeviceId display_id;
|
||||
if (!findUsableDisplay(display_id)) {
|
||||
tt_app_stop();
|
||||
tt_app_alertdialog_start("Error", "The display doesn't support the required features.", nullptr, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceId touch_id;
|
||||
if (!findUsableTouch(touch_id)) {
|
||||
tt_app_stop();
|
||||
tt_app_alertdialog_start("Error", "The touch driver doesn't support the required features.", nullptr, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop LVGL first (because it's currently using the drivers we want to use)
|
||||
tt_lvgl_stop();
|
||||
|
||||
ESP_LOGI(TAG, "Creating display driver");
|
||||
auto display = new DisplayDriver(display_id);
|
||||
|
||||
ESP_LOGI(TAG, "Creating touch driver");
|
||||
auto touch = new TouchDriver(touch_id);
|
||||
|
||||
// Run the main logic
|
||||
ESP_LOGI(TAG, "Running application");
|
||||
runApplication(display, touch);
|
||||
|
||||
ESP_LOGI(TAG, "Cleanup display driver");
|
||||
delete display;
|
||||
|
||||
ESP_LOGI(TAG, "Cleanup touch driver");
|
||||
delete touch;
|
||||
|
||||
ESP_LOGI(TAG, "Stopping application");
|
||||
tt_app_stop();
|
||||
}
|
||||
|
||||
static void onDestroy(AppHandle appHandle, void* data) {
|
||||
// Restart LVGL to resume rendering of regular apps
|
||||
if (!tt_lvgl_is_started()) {
|
||||
ESP_LOGI(TAG, "Restarting LVGL");
|
||||
tt_lvgl_start();
|
||||
}
|
||||
}
|
||||
|
||||
ExternalAppManifest manifest = {
|
||||
.onCreate = onCreate,
|
||||
.onDestroy = onDestroy
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
tt_app_register(&manifest);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT1
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=com.bytewelder.graphicsdemo
|
||||
version=0.1.0
|
||||
name=Graphics Demo
|
||||
description=A graphics and touch driver demonstration
|
||||
[author]
|
||||
name=ByteWelder
|
||||
website=https://bytewelder.com
|
||||
@ -1,637 +0,0 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import requests
|
||||
import tarfile
|
||||
import shutil
|
||||
import configparser
|
||||
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "2.1.1"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
valid_platforms = ["esp32", "esp32s3"]
|
||||
|
||||
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] Build the app. Optionally specify a platform.")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] Run the application")
|
||||
print(" install [ip] Install the application")
|
||||
print(" uninstall [ip] Uninstall the application")
|
||||
print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.")
|
||||
print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
|
||||
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")
|
||||
|
||||
# region Core
|
||||
|
||||
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 read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
#endregion Core
|
||||
|
||||
#region SDK helpers
|
||||
|
||||
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 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_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(platform_targets):
|
||||
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(platform_targets):
|
||||
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}")
|
||||
|
||||
#endregion SDK helpers
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
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("manifest.properties"):
|
||||
exit_with_error("manifest.properties 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 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)
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Manifest
|
||||
|
||||
def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
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 "version" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] version not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
if not "description" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] description not found")
|
||||
# [author]
|
||||
if not "author" in manifest:
|
||||
exit_with_error("Invalid manifest format: [author] not found")
|
||||
if not "name" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] name not found")
|
||||
if not "website" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] website not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
if not is_valid_manifest_platform(manifest, platform):
|
||||
exit_with_error(f"Platform {platform} is not available in the manifest.")
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
|
||||
#endregion Manifest
|
||||
|
||||
#region SDK download
|
||||
|
||||
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
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
return os.path.join("build", f"cmake-build-{platform}")
|
||||
|
||||
def find_elf_file(platform):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(cmake_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")
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "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
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "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
|
||||
|
||||
#endregion Building
|
||||
|
||||
#region Packaging
|
||||
|
||||
def package_intermediate_manifest(target_path):
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
print_error("manifest.properties not found")
|
||||
return
|
||||
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
|
||||
|
||||
def package_intermediate_binaries(target_path, platforms):
|
||||
elf_dir = os.path.join(target_path, "elf")
|
||||
os.makedirs(elf_dir, exist_ok=True)
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found at {elf_path}")
|
||||
return
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
if os.path.isdir("assets"):
|
||||
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
|
||||
|
||||
def package_intermediate(platforms):
|
||||
target_path = os.path.join("build", "package-intermediate")
|
||||
if os.path.isdir(target_path):
|
||||
shutil.rmtree(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
package_intermediate_manifest(target_path)
|
||||
package_intermediate_binaries(target_path, platforms)
|
||||
package_intermediate_assets(target_path)
|
||||
|
||||
def package_name(platforms):
|
||||
elf_path = find_elf_file(platforms[0])
|
||||
elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf")
|
||||
return os.path.join("build", f"{elf_base_name}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
print("Packaging app")
|
||||
package_intermediate(platforms)
|
||||
# Create build/something.app
|
||||
tar_path = package_name(platforms)
|
||||
tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT)
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
tar.close()
|
||||
|
||||
#endregion Packaging
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
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 = manifest["target"]["sdk"]
|
||||
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
|
||||
if not skip_build:
|
||||
package_all(platforms_to_build)
|
||||
|
||||
def clean_action():
|
||||
if os.path.exists("build"):
|
||||
print(f"Removing build/")
|
||||
shutil.rmtree("build")
|
||||
else:
|
||||
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(manifest, ip):
|
||||
app_id = manifest["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, platforms):
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
exit_with_error(f"ELF file not built for {platform}")
|
||||
package_path = package_name(platforms)
|
||||
print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
# Prepare multipart form data
|
||||
with open(package_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}")
|
||||
|
||||
def uninstall_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
print(f"Uninstalling {app_id} on {ip}")
|
||||
url = get_url(ip, "/app/uninstall")
|
||||
params = {'id': app_id}
|
||||
try:
|
||||
response = requests.put(url, params=params)
|
||||
if response.status_code != 200:
|
||||
print_error("Uninstall failed")
|
||||
else:
|
||||
print(f"{shell_color_green}Uninstall successful ✅{shell_color_reset}")
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
#region Main
|
||||
|
||||
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
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
# 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) < 2:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
if len(sys.argv) > 2:
|
||||
platform = sys.argv[2]
|
||||
build_action(manifest, platform)
|
||||
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) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
run_action(manifest, sys.argv[2])
|
||||
elif action_arg == "install":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "uninstall":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
uninstall_action(manifest, sys.argv[2])
|
||||
elif action_arg == "bir" or action_arg == "brrr":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
build_action(manifest, platform)
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
|
||||
#endregion Main
|
||||
2
ExternalApps/HelloWorld/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
build/
|
||||
.tactility/
|
||||
@ -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(HelloWorld)
|
||||
tactility_project(HelloWorld)
|
||||
@ -1 +0,0 @@
|
||||
Hello, world!
|
||||
@ -1,6 +0,0 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK
|
||||
)
|
||||
@ -1,24 +0,0 @@
|
||||
#include <tt_app_manifest.h>
|
||||
#include <tt_lvgl_toolbar.h>
|
||||
|
||||
/**
|
||||
* Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp
|
||||
* Only C is supported for now (C++ symbols fail to link)
|
||||
*/
|
||||
static void onShow(AppHandle app, void* data, lv_obj_t* parent) {
|
||||
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lv_obj_t* label = lv_label_create(parent);
|
||||
lv_label_set_text(label, "Hello, world!");
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
ExternalAppManifest manifest = {
|
||||
.onShow = onShow
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
tt_app_register(&manifest);
|
||||
return 0;
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.6.0-SNAPSHOT1
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=com.bytewelder.helloworld
|
||||
version=0.1.0
|
||||
name=Hello World
|
||||
description=A demonstration app that says hi
|
||||
[author]
|
||||
name=ByteWelder
|
||||
website=https://bytewelder.com
|
||||
@ -1,637 +0,0 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import requests
|
||||
import tarfile
|
||||
import shutil
|
||||
import configparser
|
||||
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "2.1.1"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
valid_platforms = ["esp32", "esp32s3"]
|
||||
|
||||
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] Build the app. Optionally specify a platform.")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] Run the application")
|
||||
print(" install [ip] Install the application")
|
||||
print(" uninstall [ip] Uninstall the application")
|
||||
print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.")
|
||||
print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
|
||||
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")
|
||||
|
||||
# region Core
|
||||
|
||||
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 read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
#endregion Core
|
||||
|
||||
#region SDK helpers
|
||||
|
||||
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 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_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(platform_targets):
|
||||
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(platform_targets):
|
||||
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}")
|
||||
|
||||
#endregion SDK helpers
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
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("manifest.properties"):
|
||||
exit_with_error("manifest.properties 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 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)
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Manifest
|
||||
|
||||
def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
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 "version" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] version not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
if not "description" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] description not found")
|
||||
# [author]
|
||||
if not "author" in manifest:
|
||||
exit_with_error("Invalid manifest format: [author] not found")
|
||||
if not "name" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] name not found")
|
||||
if not "website" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] website not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
if not is_valid_manifest_platform(manifest, platform):
|
||||
exit_with_error(f"Platform {platform} is not available in the manifest.")
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
|
||||
#endregion Manifest
|
||||
|
||||
#region SDK download
|
||||
|
||||
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
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
return os.path.join("build", f"cmake-build-{platform}")
|
||||
|
||||
def find_elf_file(platform):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(cmake_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")
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "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
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "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
|
||||
|
||||
#endregion Building
|
||||
|
||||
#region Packaging
|
||||
|
||||
def package_intermediate_manifest(target_path):
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
print_error("manifest.properties not found")
|
||||
return
|
||||
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
|
||||
|
||||
def package_intermediate_binaries(target_path, platforms):
|
||||
elf_dir = os.path.join(target_path, "elf")
|
||||
os.makedirs(elf_dir, exist_ok=True)
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found at {elf_path}")
|
||||
return
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
if os.path.isdir("assets"):
|
||||
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
|
||||
|
||||
def package_intermediate(platforms):
|
||||
target_path = os.path.join("build", "package-intermediate")
|
||||
if os.path.isdir(target_path):
|
||||
shutil.rmtree(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
package_intermediate_manifest(target_path)
|
||||
package_intermediate_binaries(target_path, platforms)
|
||||
package_intermediate_assets(target_path)
|
||||
|
||||
def package_name(platforms):
|
||||
elf_path = find_elf_file(platforms[0])
|
||||
elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf")
|
||||
return os.path.join("build", f"{elf_base_name}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
print("Packaging app")
|
||||
package_intermediate(platforms)
|
||||
# Create build/something.app
|
||||
tar_path = package_name(platforms)
|
||||
tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT)
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
tar.close()
|
||||
|
||||
#endregion Packaging
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
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 = manifest["target"]["sdk"]
|
||||
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
|
||||
if not skip_build:
|
||||
package_all(platforms_to_build)
|
||||
|
||||
def clean_action():
|
||||
if os.path.exists("build"):
|
||||
print(f"Removing build/")
|
||||
shutil.rmtree("build")
|
||||
else:
|
||||
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(manifest, ip):
|
||||
app_id = manifest["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, platforms):
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
exit_with_error(f"ELF file not built for {platform}")
|
||||
package_path = package_name(platforms)
|
||||
print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
# Prepare multipart form data
|
||||
with open(package_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}")
|
||||
|
||||
def uninstall_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
print(f"Uninstalling {app_id} on {ip}")
|
||||
url = get_url(ip, "/app/uninstall")
|
||||
params = {'id': app_id}
|
||||
try:
|
||||
response = requests.put(url, params=params)
|
||||
if response.status_code != 200:
|
||||
print_error("Uninstall failed")
|
||||
else:
|
||||
print(f"{shell_color_green}Uninstall successful ✅{shell_color_reset}")
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
#region Main
|
||||
|
||||
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
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
# 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) < 2:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
if len(sys.argv) > 2:
|
||||
platform = sys.argv[2]
|
||||
build_action(manifest, platform)
|
||||
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) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
run_action(manifest, sys.argv[2])
|
||||
elif action_arg == "install":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "uninstall":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
uninstall_action(manifest, sys.argv[2])
|
||||
elif action_arg == "bir" or action_arg == "brrr":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
build_action(manifest, platform)
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
|
||||
#endregion Main
|
||||
@ -1,5 +1,16 @@
|
||||
# ChangeLog
|
||||
|
||||
## v1.1.1 - 2025-06-26
|
||||
|
||||
* Added support for ESP32-C61
|
||||
|
||||
## v1.1.0 - 2025-05-06
|
||||
|
||||
* Added fast build for ELF application
|
||||
* Added a script to generate the symbol table for the ELF APP:
|
||||
* Supports generating symbols table based on ELF file
|
||||
* Supports generating symbols table based on static libraries
|
||||
|
||||
## v1.0.0 - 2024-12-09
|
||||
|
||||
* Added support for the following RISC-V chips: ESP32-P4 and ESP32-C6
|
||||
|
||||
@ -4,6 +4,10 @@ if(CONFIG_ELF_LOADER)
|
||||
"src/esp_elf.c"
|
||||
"src/esp_elf_adapter.c")
|
||||
|
||||
if(CONFIG_ELF_LOADER_CUSTOMER_SYMBOLS)
|
||||
list(APPEND srcs "src/esp_all_symbol.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_IDF_TARGET_ARCH_XTENSA)
|
||||
list(APPEND srcs "src/arch/esp_elf_xtensa.c")
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
menu "Espressif ELF Loader Configuration"
|
||||
visible if (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4)
|
||||
visible if (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61)
|
||||
|
||||
config ELF_LOADER_BUS_ADDRESS_MIRROR
|
||||
bool
|
||||
default y if (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3)
|
||||
default n if (IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4)
|
||||
default n if (IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61)
|
||||
|
||||
config ELF_LOADER
|
||||
bool "Enable Espressif ELF Loader"
|
||||
default y
|
||||
depends on (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4)
|
||||
depends on (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61)
|
||||
help
|
||||
Select this option to enable ELF Loader and show the submenu with ELF Loader configuration choices.
|
||||
|
||||
@ -29,7 +29,7 @@ menu "Espressif ELF Loader Configuration"
|
||||
config ELF_LOADER_LOAD_PSRAM
|
||||
bool "Load ELF to PSRAM"
|
||||
default y
|
||||
depends on (IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4) && SPIRAM
|
||||
depends on (IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61) && SPIRAM
|
||||
select ELF_LOADER_CACHE_OFFSET if (IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3)
|
||||
select ELF_LOADER_SET_MMU if IDF_TARGET_ESP32S2
|
||||
help
|
||||
|
||||
@ -13,6 +13,7 @@ This ELF loader supports following SoCs:
|
||||
- ESP32-S3, support running ELF in PSRAM
|
||||
- ESP32-P4, support running ELF in PSRAM
|
||||
- ESP32-C6
|
||||
- ESP32-C61, support running ELF in PSRAM
|
||||
|
||||
### Usage
|
||||
|
||||
@ -62,6 +63,29 @@ project_elf(XXXX)
|
||||
|
||||
Build the project as an ordinary ESP-IDF project, and then the ELF file named `XXXX.app.elf` is in the build directory.
|
||||
|
||||
### ELF APP Fast Build
|
||||
|
||||
Users can enable ELF fast build functionality by configuring CMAKE's generator as Unit Makefile. The reference command is as follows:
|
||||
|
||||
```bash
|
||||
idf.py -G 'Unix Makefiles' set-target <chip-name>
|
||||
```
|
||||
|
||||
Then input the ELF APP build command as follows:
|
||||
|
||||
```
|
||||
idf.py elf
|
||||
```
|
||||
|
||||
The build system will only build ELF target components and show the following logs:
|
||||
|
||||
```
|
||||
Building C object esp-idf/main/CMakeFiles/__idf_main.dir/main.c.obj
|
||||
Linking C static library libmain.a
|
||||
Build ELF: hello_world.app.elf
|
||||
Built target elf
|
||||
```
|
||||
|
||||
### Adding the Component to Your Project
|
||||
|
||||
Please use the component manager command add-dependency to add the elf_loader component as a dependency in your project. During the CMake step, the component will be downloaded automatically.
|
||||
|
||||
@ -52,7 +52,16 @@ macro(project_elf project_name)
|
||||
if(ELF_COMPONENTS)
|
||||
foreach(c ${ELF_COMPONENTS})
|
||||
list(APPEND elf_libs "esp-idf/${c}/lib${c}.a")
|
||||
list(APPEND elf_dependeces "idf::${c}")
|
||||
|
||||
if(${CMAKE_GENERATOR} STREQUAL "Unix Makefiles")
|
||||
add_custom_command(OUTPUT elf_${c}_app
|
||||
COMMAND +${CMAKE_MAKE_PROGRAM} "__idf_${c}/fast"
|
||||
COMMENT "Build Component: ${c}"
|
||||
)
|
||||
list(APPEND elf_dependeces "elf_${c}_app")
|
||||
else()
|
||||
list(APPEND elf_dependeces "idf::${c}")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
if (ELF_LIBS)
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
version: "1.0.0"
|
||||
version: "1.1.1"
|
||||
targets:
|
||||
- esp32
|
||||
- esp32s2
|
||||
- esp32s3
|
||||
- esp32c6
|
||||
- esp32c61
|
||||
- esp32p4
|
||||
description: Espressif ELF(Executable and Linkable Format) Loader
|
||||
url: https://github.com/espressif/esp-iot-solution/tree/master/components/elf_loader
|
||||
dependencies:
|
||||
|
||||
@ -12,9 +12,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Tactility custom -->
|
||||
#define ESP_ELFSYM_EXPORT(_sym) { #_sym, (const void*)&_sym }
|
||||
// <-- Tactility custom
|
||||
#define ESP_ELFSYM_EXPORT(_sym) { #_sym, (void*)&_sym }
|
||||
#define ESP_ELFSYM_END { NULL, NULL }
|
||||
|
||||
/** @brief Function symbol description */
|
||||
@ -33,9 +31,23 @@ struct esp_elfsym {
|
||||
*/
|
||||
uintptr_t elf_find_sym(const char *sym_name);
|
||||
|
||||
// Tactility custom -->
|
||||
void elf_set_custom_symbols(const struct esp_elfsym* symbols);
|
||||
// <-- Tactility custom
|
||||
|
||||
/**
|
||||
* @brief Resolves a symbol name (e.g. function name) to its address.
|
||||
*
|
||||
* @param sym_name - Symbol name
|
||||
* @return Symbol address if success or 0 if failed.
|
||||
*/
|
||||
typedef uintptr_t (*symbol_resolver)(const char *sym_name);
|
||||
|
||||
/**
|
||||
* @brief Override the internal symbol resolver.
|
||||
* The default resolver is based on static lists that are determined by KConfig.
|
||||
* This override allows for an arbitrary implementation.
|
||||
*
|
||||
* @param resolver the resolver function
|
||||
*/
|
||||
void elf_set_symbol_resolver(symbol_resolver resolver);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
21
Libraries/elf_loader/src/esp_all_symbol.c
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "private/elf_symbol.h"
|
||||
|
||||
/* Extern declarations from ELF symbol table */
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch"
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
/* Available ELF symbols table: g_customer_elfsyms */
|
||||
|
||||
const struct esp_elfsym g_customer_elfsyms[] = {
|
||||
ESP_ELFSYM_END
|
||||
};
|
||||
@ -19,12 +19,27 @@
|
||||
|
||||
#include "private/elf_symbol.h"
|
||||
#include "private/elf_platform.h"
|
||||
#include "esp_elf.h"
|
||||
|
||||
#define stype(_s, _t) ((_s)->type == (_t))
|
||||
#define sflags(_s, _f) (((_s)->flags & (_f)) == (_f))
|
||||
#define ADDR_OFFSET (0x400)
|
||||
|
||||
uintptr_t elf_find_sym_default(const char *sym_name);
|
||||
|
||||
static const char *TAG = "ELF";
|
||||
static symbol_resolver current_resolver = elf_find_sym_default;
|
||||
|
||||
/**
|
||||
* @brief Find symbol address by name.
|
||||
*
|
||||
* @param sym_name - Symbol name
|
||||
*
|
||||
* @return Symbol address if success or 0 if failed.
|
||||
*/
|
||||
uintptr_t elf_find_sym(const char *sym_name) {
|
||||
return current_resolver(sym_name);
|
||||
}
|
||||
|
||||
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
|
||||
|
||||
@ -306,6 +321,18 @@ static int esp_elf_load_segment(esp_elf_t *elf, const uint8_t *pbuf)
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Override the internal symbol resolver.
|
||||
* The default resolver is based on static lists that are determined by KConfig.
|
||||
* This override allows for an arbitrary implementation.
|
||||
*
|
||||
* @param resolver the resolver function
|
||||
*/
|
||||
void elf_set_symbol_resolver(symbol_resolver resolver) {
|
||||
current_resolver = resolver;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Map symbol's address of ELF to physic space.
|
||||
*
|
||||
|
||||
@ -27,13 +27,6 @@ extern int __gtdf2(double a, double b);
|
||||
extern double __floatunsidf(unsigned int i);
|
||||
extern double __divdf3(double a, double b);
|
||||
|
||||
// Tactility custom -->
|
||||
static const struct esp_elfsym* custom_symbols = NULL;
|
||||
void elf_set_custom_symbols(const struct esp_elfsym* symbols) {
|
||||
custom_symbols = symbols;
|
||||
}
|
||||
// <-- Tactility custom
|
||||
|
||||
/** @brief Libc public functions symbols look-up table */
|
||||
|
||||
static const struct esp_elfsym g_esp_libc_elfsyms[] = {
|
||||
@ -163,7 +156,7 @@ static const struct esp_elfsym g_esp_espidf_elfsyms[] = {
|
||||
*
|
||||
* @return Symbol address if success or 0 if failed.
|
||||
*/
|
||||
uintptr_t elf_find_sym(const char *sym_name)
|
||||
uintptr_t elf_find_sym_default(const char *sym_name)
|
||||
{
|
||||
const struct esp_elfsym *syms;
|
||||
|
||||
@ -208,16 +201,5 @@ uintptr_t elf_find_sym(const char *sym_name)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Tactility custom -->
|
||||
syms = custom_symbols;
|
||||
while (syms->name) {
|
||||
if (!strcmp(syms->name, sym_name)) {
|
||||
return (uintptr_t)syms->sym;
|
||||
}
|
||||
|
||||
syms++;
|
||||
}
|
||||
// <-- Tactility custom
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
268
Libraries/elf_loader/tool/symbols.py
Executable file
@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import logging
|
||||
import os
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
def get_global_symbols(lines, type, undefined=False, symbol_types=None):
|
||||
"""
|
||||
Extract global symbols from the given lines of a symbol table.
|
||||
|
||||
:param lines: List of lines from the symbol table, each representing a symbol.
|
||||
:param type: Type of the input file ('e' for ELF files, 'l' for static libraries).
|
||||
:param undefined: If True, only extract undefined (UND) symbols; otherwise, extract all GLOBAL symbols.
|
||||
:param symbol_types: A list of symbol types to filter by (e.g., ['FUNC', 'OBJECT']). If None, no filtering by type.
|
||||
:return: List of symbol names if any match the criteria; otherwise, returns an empty list.
|
||||
"""
|
||||
symbols = []
|
||||
|
||||
if type == 'e':
|
||||
# Pattern for ELF files
|
||||
if not undefined:
|
||||
pattern = re.compile(
|
||||
r'^\s*\d+:\s+(\S+)\s+(\d+)\s+(FUNC|OBJECT)\s+GLOBAL\s+DEFAULT\s+(?:\d+|ABS|UND|COM|DEBUG)\s+(\S+)',
|
||||
re.MULTILINE
|
||||
)
|
||||
else:
|
||||
pattern = re.compile(
|
||||
r'^\s*\d*:\s*\w*\s*\d*\s*NOTYPE\s*GLOBAL\s*DEFAULT\s*UND\s+(\S*)',
|
||||
re.MULTILINE
|
||||
)
|
||||
|
||||
for line in lines:
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
if not undefined:
|
||||
address, size, symbol_type, symbol_name = match.groups()
|
||||
|
||||
# Filter by symbol type if specified
|
||||
if symbol_types and symbol_type not in symbol_types:
|
||||
continue
|
||||
|
||||
symbols.append(symbol_name)
|
||||
else:
|
||||
symbol_name = match.group(1)
|
||||
symbols.append(symbol_name)
|
||||
|
||||
elif type == 'l':
|
||||
# Patterns for static libraries
|
||||
func_pattern = re.compile(r'^\s*[0-9a-fA-F]+\s+[TD]\s+(\S+)$')
|
||||
var_pattern = re.compile(r'^\s*[0-9a-fA-F]+\s+[BD]\s+(\S+)$')
|
||||
|
||||
for line in lines:
|
||||
if not undefined:
|
||||
func_match = func_pattern.match(line)
|
||||
var_match = var_pattern.match(line)
|
||||
|
||||
if func_match:
|
||||
symbols.append(func_match.group(1))
|
||||
elif var_match:
|
||||
symbols.append(var_match.group(1))
|
||||
|
||||
return symbols
|
||||
|
||||
def save_c_file(symbols, output, symbol_table, exclude_symbols=None):
|
||||
"""
|
||||
Write extern declarations and ESP_ELFSYM structure to a C file, excluding specified symbols.
|
||||
|
||||
:param symbols: List of symbol names.
|
||||
:param output: Path to the output C file.
|
||||
:param exclude_symbols: List of symbol names to exclude; defaults to ['elf_find_sym'].
|
||||
"""
|
||||
if exclude_symbols is None:
|
||||
exclude_symbols = ['elf_find_sym'] # Set default excluded symbols
|
||||
|
||||
# Filter out excluded symbols
|
||||
filtered_symbols = [name for name in symbols if name not in exclude_symbols]
|
||||
|
||||
# Build the content of the C file
|
||||
buf = '/*\n'
|
||||
buf += ' * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD\n'
|
||||
buf += ' *\n'
|
||||
buf += ' * SPDX-License-Identifier: Apache-2.0\n'
|
||||
buf += ' */\n\n'
|
||||
|
||||
# Add standard library headers
|
||||
libc_headers = ['stddef'] # Add more headers as needed
|
||||
buf += '\n'.join([f'#include <{h}.h>' for h in libc_headers]) + '\n\n'
|
||||
|
||||
# Add custom header
|
||||
buf += '#include "private/elf_symbol.h"\n\n'
|
||||
|
||||
# Generate extern declarations if there are symbols
|
||||
if filtered_symbols:
|
||||
buf += '/* Extern declarations from ELF symbol table */\n\n'
|
||||
buf += '#pragma GCC diagnostic push\n'
|
||||
buf += '#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch"\n'
|
||||
for symbol_name in filtered_symbols:
|
||||
buf += f'extern int {symbol_name};\n'
|
||||
buf += '#pragma GCC diagnostic pop\n\n'
|
||||
|
||||
# Define the symbol table structure with dynamic variable name
|
||||
symbol_table_var = f'g_{symbol_table}_elfsyms'
|
||||
buf += f'/* Available ELF symbols table: {symbol_table_var} */\n'
|
||||
buf += f'\nconst struct esp_elfsym {symbol_table_var}[] = {{\n'
|
||||
|
||||
# Generate ESP_ELFSYM_EXPORT entries
|
||||
if filtered_symbols:
|
||||
for symbol_name in filtered_symbols:
|
||||
buf += f' ESP_ELFSYM_EXPORT({symbol_name}),\n'
|
||||
|
||||
# End the symbol table
|
||||
buf += ' ESP_ELFSYM_END\n'
|
||||
buf += '};\n'
|
||||
|
||||
# Write to the file
|
||||
with open(output, 'w+') as f:
|
||||
f.write(buf)
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function to parse command-line arguments and process the input file's symbol table.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description='Extract all public functions from an application project', prog='symbols')
|
||||
|
||||
parser.add_argument(
|
||||
'--output-file', '-of',
|
||||
help='Custom output file path with filename (overrides --output)',
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--input', '-i',
|
||||
help='Input file name with full path',
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--undefined', '-u',
|
||||
action='store_true',
|
||||
help='If set, only extract undefined (UND) symbols; otherwise, extract all GLOBAL symbols.',
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--exclude', '-e',
|
||||
nargs='+',
|
||||
help='Symbols to exclude from the generated C file (e.g., memcpy __ltdf2). Default: elf_find_sym',
|
||||
default=[] # User can extend this list
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--type', '-t',
|
||||
choices=['e', 'l'],
|
||||
required=True,
|
||||
help='Type of the input file: "elf" for ELF file, "lib" for static library (.a)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--debug', '-d',
|
||||
help='Debug level(option is \'debug\')',
|
||||
default='no',
|
||||
type=str)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Configure logging
|
||||
if args.debug == 'debug':
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Get the absolute path of the current file
|
||||
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
if args.type == 'e':
|
||||
extracted_part = 'customer'
|
||||
elif args.type == 'l':
|
||||
# Convert relative path to absolute path
|
||||
input_abs_path = os.path.abspath(args.input)
|
||||
|
||||
# Get the base name of the input file (without extension)
|
||||
input_basename = os.path.basename(input_abs_path)
|
||||
|
||||
# Use a regular expression to extract the middle part
|
||||
match = re.search(r'^lib(.+)\.a$', input_basename)
|
||||
if match:
|
||||
extracted_part = match.group(1)
|
||||
else:
|
||||
logging.error('Invalid input file name format. Expected format: lib<name>.a')
|
||||
sys.exit(1)
|
||||
|
||||
# Determine the output file path
|
||||
if args.output_file:
|
||||
# Use the custom file path provided by the user
|
||||
elfsym_file_dir = os.path.abspath(args.output_file)
|
||||
output_dir = os.path.dirname(elfsym_file_dir)
|
||||
output_abs_path = os.path.abspath(args.output_file)
|
||||
output_basename = os.path.basename(output_abs_path)
|
||||
extracted_part = os.path.splitext(output_basename)[0]
|
||||
else:
|
||||
# Use the default behavior: generate the file in the parent directory's 'src' folder
|
||||
parent_dir = os.path.dirname(cur_dir)
|
||||
output_dir = os.path.join(parent_dir, 'src') # Default directory is 'src' under the parent directory
|
||||
output_file_name = f'esp_all_symbol.c'
|
||||
elfsym_file_dir = os.path.join(output_dir, output_file_name)
|
||||
|
||||
# Ensure the output directory exists
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Set default excluded symbols and allow user to extend the list
|
||||
exclude_symbols = ['elf_find_sym', 'g_customer_elfsyms'] + args.exclude
|
||||
|
||||
if args.type == 'e':
|
||||
cmd = ['readelf', '-s', '-W', args.input]
|
||||
elif args.type == 'l':
|
||||
cmd = ['nm', '--defined-only', '-g', args.input]
|
||||
|
||||
# Execute the readelf or nm command for static libraries
|
||||
try:
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
|
||||
lines = result.stdout.splitlines()
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f'Error executing command: {e.stderr}')
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
logging.error('nm command not found. Please ensure it is installed and available in your PATH.')
|
||||
sys.exit(1)
|
||||
|
||||
# Extract global symbols from ELF file or static library
|
||||
symbols = get_global_symbols(lines, type=args.type, undefined=args.undefined, symbol_types=['FUNC', 'OBJECT'])
|
||||
|
||||
if not symbols:
|
||||
logging.warning('No global symbols found in the input file.')
|
||||
sys.exit(0)
|
||||
|
||||
logging.debug('symbols: %s'%(cmd))
|
||||
logging.debug('symbols: %s'%(symbols))
|
||||
logging.debug('elfsym_file_dir: %s'%(elfsym_file_dir))
|
||||
logging.debug('extracted_part: %s'%(extracted_part))
|
||||
logging.debug('exclude_symbols: %s'%(exclude_symbols))
|
||||
|
||||
# Save the C file
|
||||
try:
|
||||
save_c_file(symbols, elfsym_file_dir, extracted_part, exclude_symbols=exclude_symbols)
|
||||
logging.info(f"C file with extern declarations and symbol table has been saved to '{elfsym_file_dir}'.")
|
||||
except Exception as e:
|
||||
logging.error(f'Error writing C file: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
def _main():
|
||||
"""
|
||||
Wrapper for the main function to catch and handle runtime errors.
|
||||
"""
|
||||
try:
|
||||
main()
|
||||
except RuntimeError as e:
|
||||
logging.error(f'A fatal error occurred: {e}')
|
||||
sys.exit(2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
_main()
|
||||
21
Tactility/Include/Tactility/Paths.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tt {
|
||||
|
||||
bool findFirstMountedSdCardPath(std::string& path);
|
||||
|
||||
std::string getSystemRootPath();
|
||||
|
||||
std::string getTempPath();
|
||||
|
||||
std::string getAppInstallPath();
|
||||
|
||||
std::string getAppInstallPath(const std::string& appId);
|
||||
|
||||
std::string getUserPath();
|
||||
|
||||
std::string getAppUserPath(const std::string& appId);
|
||||
|
||||
}
|
||||
@ -94,10 +94,6 @@ std::shared_ptr<AppContext> _Nullable getCurrentAppContext();
|
||||
/** @return the currently running app (it is only ever null before the splash screen is shown) */
|
||||
std::shared_ptr<App> _Nullable getCurrentApp();
|
||||
|
||||
std::string getTempPath();
|
||||
|
||||
std::string getInstallPath();
|
||||
|
||||
bool install(const std::string& path);
|
||||
|
||||
bool uninstall(const std::string& appId);
|
||||
|
||||
@ -7,7 +7,7 @@ namespace tt::app {
|
||||
|
||||
// Forward declarations
|
||||
class App;
|
||||
class Paths;
|
||||
class AppPaths;
|
||||
struct AppManifest;
|
||||
enum class Result;
|
||||
|
||||
@ -32,70 +32,10 @@ public:
|
||||
|
||||
virtual const AppManifest& getManifest() const = 0;
|
||||
virtual std::shared_ptr<const Bundle> getParameters() const = 0;
|
||||
virtual std::unique_ptr<Paths> getPaths() const = 0;
|
||||
virtual std::unique_ptr<AppPaths> getPaths() const = 0;
|
||||
|
||||
virtual std::shared_ptr<App> getApp() const = 0;
|
||||
};
|
||||
|
||||
class Paths {
|
||||
|
||||
public:
|
||||
|
||||
Paths() = default;
|
||||
virtual ~Paths() = default;
|
||||
|
||||
/**
|
||||
* Returns the directory path for the data location for an app.
|
||||
* The data directory is intended to survive OS upgrades.
|
||||
* The path will not end with a "/".
|
||||
*/
|
||||
virtual std::string getDataDirectory() const = 0;
|
||||
|
||||
/**
|
||||
* @see getDataDirectory(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getDataDirectoryLvgl() const = 0;
|
||||
|
||||
/**
|
||||
* Returns the full path for an entry inside the data location for an app.
|
||||
* The data directory is intended to survive OS upgrades.
|
||||
* Configuration data should be stored here.
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
virtual std::string getDataPath(const std::string& childPath) const = 0;
|
||||
|
||||
/**
|
||||
* @see getDataPath(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getDataPathLvgl(const std::string& childPath) const = 0;
|
||||
|
||||
/**
|
||||
* Returns the directory path for the system location for an app.
|
||||
* The system directory is not intended to survive OS upgrades.
|
||||
* You should not store configuration data here.
|
||||
* The path will not end with a "/".
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
*/
|
||||
virtual std::string getSystemDirectory() const = 0;
|
||||
|
||||
/**
|
||||
* @see getSystemDirectory(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getSystemDirectoryLvgl() const = 0;
|
||||
|
||||
/**
|
||||
* Returns the full path for an entry inside the system location for an app.
|
||||
* The data directory is not intended to survive OS upgrades.
|
||||
* You should not store configuration data here.
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
virtual std::string getSystemPath(const std::string& childPath) const = 0;
|
||||
|
||||
/**
|
||||
* @see getSystemPath(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getSystemPathLvgl(const std::string& childPath) const = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -63,30 +63,42 @@ struct AppManifest {
|
||||
constexpr static uint32_t Hidden = 1 << 1;
|
||||
};
|
||||
|
||||
/** The SDK version that was used to compile this app. (e.g. "0.6.0") */
|
||||
std::string targetSdk = {};
|
||||
|
||||
/** Comma-separated list of platforms, e.g. "esp32,esp32s3" */
|
||||
std::string targetPlatforms = {};
|
||||
|
||||
/** The identifier by which the app is launched by the system and other apps. */
|
||||
std::string id = {};
|
||||
std::string appId = {};
|
||||
|
||||
/** The user-readable name of the app. Used in UI. */
|
||||
std::string name = {};
|
||||
std::string appName = {};
|
||||
|
||||
/** Optional icon. */
|
||||
std::string icon = {};
|
||||
std::string appIcon = {};
|
||||
|
||||
/** The version as it is displayed to the user (e.g. "1.2.0") */
|
||||
std::string appVersionName = {};
|
||||
|
||||
/** The technical version (must be incremented with new releases of the app */
|
||||
uint64_t appVersionCode = {};
|
||||
|
||||
/** App category helps with listing apps in Launcher, app list or settings apps. */
|
||||
Category category = Category::User;
|
||||
Category appCategory = Category::User;
|
||||
|
||||
/** Where the app is located */
|
||||
Location location = Location::internal();
|
||||
Location appLocation = Location::internal();
|
||||
|
||||
/** Controls various settings */
|
||||
uint32_t flags = Flags::None;
|
||||
uint32_t appFlags = Flags::None;
|
||||
|
||||
/** Create the instance of the app */
|
||||
CreateApp createApp = nullptr;
|
||||
};
|
||||
|
||||
struct {
|
||||
bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->name < right->name; }
|
||||
bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->appName < right->appName; }
|
||||
} SortAppManifestByName;
|
||||
|
||||
} // namespace
|
||||
|
||||
47
Tactility/Include/Tactility/app/AppPaths.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
// Forward declarations
|
||||
class AppManifest;
|
||||
|
||||
class AppPaths {
|
||||
|
||||
const AppManifest& manifest;
|
||||
|
||||
public:
|
||||
|
||||
explicit AppPaths(const AppManifest& manifest) : manifest(manifest) {}
|
||||
|
||||
/**
|
||||
* The user data directory is intended to survive OS upgrades.
|
||||
* The path will not end with a "/".
|
||||
*/
|
||||
std::string getUserDataPath() const;
|
||||
|
||||
/**
|
||||
* The user data directory is intended to survive OS upgrades.
|
||||
* Configuration data should be stored here.
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
std::string getUserDataPath(const std::string& childPath) const;
|
||||
|
||||
/**
|
||||
* You should not store configuration data here.
|
||||
* The path will not end with a "/".
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
*/
|
||||
std::string getAssetsDirectory() const;
|
||||
|
||||
/**
|
||||
* You should not store configuration data here.
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
std::string getAssetsPath(const std::string& childPath) const;
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <driver/gpio.h>
|
||||
#else
|
||||
typedef unsigned int gpio_num_t;
|
||||
#endif
|
||||
34
Tactility/Include/Tactility/hal/gpio/Gpio.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::hal::gpio {
|
||||
|
||||
typedef unsigned int Pin;
|
||||
constexpr Pin NO_PIN = -1;
|
||||
|
||||
/** @warning The order must match GpioMode from tt_hal_gpio.h */
|
||||
enum class Mode {
|
||||
Disable = 0,
|
||||
Input,
|
||||
Output,
|
||||
OutputOpenDrain,
|
||||
InputOutput,
|
||||
InputOutputOpenDrain
|
||||
};
|
||||
|
||||
/** Configure a single pin */
|
||||
bool configure(Pin pin, Mode mode, bool pullUp, bool pullDown);
|
||||
|
||||
/** Configure a set of pins defined by their bit index */
|
||||
bool configureWithPinBitmask(uint64_t pinBitMask, Mode mode, bool pullUp, bool pullDown);
|
||||
|
||||
bool setMode(Pin pin, Mode mode);
|
||||
|
||||
bool getLevel(Pin pin);
|
||||
|
||||
bool setLevel(Pin pin, bool level);
|
||||
|
||||
int getPinCount();
|
||||
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Gpio.h"
|
||||
#include "../gpio/Gpio.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <driver/spi_common.h>
|
||||
@ -8,6 +8,8 @@
|
||||
|
||||
#define SPI_HOST_MAX 3
|
||||
|
||||
typedef tt::hal::gpio::Pin gpio_num_t;
|
||||
|
||||
typedef int spi_host_device_t;
|
||||
struct spi_bus_config_t {
|
||||
gpio_num_t miso_io_num;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
#include <Tactility/RtosCompat.h>
|
||||
|
||||
#include "../Gpio.h"
|
||||
#include "../gpio/Gpio.h"
|
||||
#include "Tactility/Lock.h"
|
||||
#include "UartCompat.h"
|
||||
#include "Tactility/hal/uart/Configuration.h"
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "ServiceManifest.h"
|
||||
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace tt::service {
|
||||
|
||||
class Paths;
|
||||
struct ServiceManifest;
|
||||
class ServicePaths;
|
||||
|
||||
/**
|
||||
* The public representation of a service instance.
|
||||
@ -26,68 +23,8 @@ public:
|
||||
virtual const ServiceManifest& getManifest() const = 0;
|
||||
|
||||
/** Retrieve the paths that are relevant to this service */
|
||||
virtual std::unique_ptr<Paths> getPaths() const = 0;
|
||||
virtual std::unique_ptr<ServicePaths> getPaths() const = 0;
|
||||
};
|
||||
|
||||
class Paths {
|
||||
|
||||
public:
|
||||
|
||||
Paths() = default;
|
||||
virtual ~Paths() = default;
|
||||
|
||||
/**
|
||||
* Returns the directory path for the data location for a service.
|
||||
* The data directory is intended to survive OS upgrades.
|
||||
* The path will not end with a "/".
|
||||
*/
|
||||
virtual std::string getDataDirectory() const = 0;
|
||||
|
||||
/**
|
||||
* @see getDataDirectory(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getDataDirectoryLvgl() const = 0;
|
||||
|
||||
/**
|
||||
* Returns the full path for an entry inside the data location for a service.
|
||||
* The data directory is intended to survive OS upgrades.
|
||||
* Configuration data should be stored here.
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
virtual std::string getDataPath(const std::string& childPath) const = 0;
|
||||
|
||||
/**
|
||||
* @see getDataPath(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getDataPathLvgl(const std::string& childPath) const = 0;
|
||||
|
||||
/**
|
||||
* Returns the directory path for the system location for a service.
|
||||
* The system directory is not intended to survive OS upgrades.
|
||||
* You should not store configuration data here.
|
||||
* The path will not end with a "/".
|
||||
* This is mainly used for core services.
|
||||
*/
|
||||
virtual std::string getSystemDirectory() const = 0;
|
||||
|
||||
/**
|
||||
* @see getSystemDirectory(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getSystemDirectoryLvgl() const = 0;
|
||||
|
||||
/**
|
||||
* Returns the full path for an entry inside the system location for an app.
|
||||
* The data directory is not intended to survive OS upgrades.
|
||||
* You should not store configuration data here.
|
||||
* This is mainly used for core apps (system/boot/settings type).
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
virtual std::string getSystemPath(const std::string& childPath) const = 0;
|
||||
|
||||
/**
|
||||
* @see getSystemPath(), but with LVGL prefix.
|
||||
*/
|
||||
virtual std::string getSystemPathLvgl(const std::string& childPath) const = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tactility/service/Service.h"
|
||||
#include <Tactility/service/Service.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
|
||||
45
Tactility/Include/Tactility/service/ServicePaths.h
Normal file
@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::service {
|
||||
|
||||
// Forward declarations
|
||||
class ServiceManifest;
|
||||
|
||||
class ServicePaths {
|
||||
|
||||
std::shared_ptr<const ServiceManifest> manifest;
|
||||
|
||||
public:
|
||||
|
||||
explicit ServicePaths(std::shared_ptr<const ServiceManifest> manifest) : manifest(std::move(manifest)) {}
|
||||
|
||||
/**
|
||||
* The user data directory is intended to survive OS upgrades.
|
||||
* The path will not end with a "/".
|
||||
*/
|
||||
std::string getUserDataDirectory() const;
|
||||
|
||||
/**
|
||||
* The user data directory is intended to survive OS upgrades.
|
||||
* Configuration data should be stored here.
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
std::string getUserDataPath(const std::string& childPath) const;
|
||||
|
||||
/**
|
||||
* You should not store configuration data here.
|
||||
* The path will not end with a "/".
|
||||
*/
|
||||
std::string getAssetsDirectory() const;
|
||||
|
||||
/**
|
||||
* You should not store configuration data here.
|
||||
* @param[in] childPath the path without a "/" prefix
|
||||
*/
|
||||
std::string getAssetsPath(const std::string& childPath) const;
|
||||
};
|
||||
|
||||
}
|
||||
@ -24,7 +24,7 @@ class GpsService final : public Service {
|
||||
Mutex stateMutex;
|
||||
std::vector<GpsDeviceRecord> deviceRecords;
|
||||
std::shared_ptr<PubSub<State>> statePubSub = std::make_shared<PubSub<State>>();
|
||||
std::unique_ptr<Paths> paths;
|
||||
std::unique_ptr<ServicePaths> paths;
|
||||
State state = State::Off;
|
||||
|
||||
bool startGpsDevice(GpsDeviceRecord& deviceRecord);
|
||||
|
||||
@ -41,10 +41,10 @@ class AppInstance : public AppContext {
|
||||
static std::shared_ptr<App> createApp(
|
||||
const std::shared_ptr<AppManifest>& manifest
|
||||
) {
|
||||
if (manifest->location.isInternal()) {
|
||||
if (manifest->appLocation.isInternal()) {
|
||||
assert(manifest->createApp != nullptr);
|
||||
return manifest->createApp();
|
||||
} else if (manifest->location.isExternal()) {
|
||||
} else if (manifest->appLocation.isExternal()) {
|
||||
if (manifest->createApp != nullptr) {
|
||||
TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps");
|
||||
}
|
||||
@ -88,7 +88,7 @@ public:
|
||||
|
||||
std::shared_ptr<const Bundle> getParameters() const override;
|
||||
|
||||
std::unique_ptr<Paths> getPaths() const override;
|
||||
std::unique_ptr<AppPaths> getPaths() const override;
|
||||
|
||||
std::shared_ptr<App> getApp() const override { return app; }
|
||||
};
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tactility/app/AppInstance.h"
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
class AppInstancePaths final : public Paths {
|
||||
|
||||
const AppManifest& manifest;
|
||||
|
||||
public:
|
||||
|
||||
explicit AppInstancePaths(const AppManifest& manifest) : manifest(manifest) {}
|
||||
~AppInstancePaths() override = default;
|
||||
|
||||
std::string getDataDirectory() const override;
|
||||
std::string getDataDirectoryLvgl() const override;
|
||||
std::string getDataPath(const std::string& childPath) const override;
|
||||
std::string getDataPathLvgl(const std::string& childPath) const override;
|
||||
std::string getSystemDirectory() const override;
|
||||
std::string getSystemDirectoryLvgl() const override;
|
||||
std::string getSystemPath(const std::string& childPath) const override;
|
||||
std::string getSystemPathLvgl(const std::string& childPath) const override;
|
||||
};
|
||||
|
||||
}
|
||||
14
Tactility/Private/Tactility/app/AppManifestParsing.h
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
bool isValidId(const std::string& id);
|
||||
|
||||
bool parseManifest(const std::map<std::string, std::string>& map, AppManifest& manifest);
|
||||
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include "driver/gpio.h"
|
||||
#define GPIO_NUM_MIN GPIO_NUM_0
|
||||
#else
|
||||
#define GPIO_NUM_MIN 0
|
||||
#define GPIO_NUM_MAX 50
|
||||
#endif
|
||||
|
||||
#ifndef ESP_PLATFORM
|
||||
int gpio_get_level(int gpio_num);
|
||||
#endif
|
||||
@ -4,6 +4,7 @@
|
||||
#include "./State.h"
|
||||
|
||||
#include <Tactility/app/AppContext.h>
|
||||
#include <Tactility/app/AppPaths.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
@ -13,7 +14,7 @@ class View final {
|
||||
|
||||
Bindings* bindings;
|
||||
State* state;
|
||||
std::unique_ptr<Paths> paths;
|
||||
std::unique_ptr<AppPaths> paths;
|
||||
lv_obj_t* root = nullptr;
|
||||
lv_obj_t* enable_switch = nullptr;
|
||||
lv_obj_t* enable_on_boot_switch = nullptr;
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tactility/service/ServiceContext.h"
|
||||
#include "Tactility/service/Service.h"
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/Service.h>
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace tt::service {
|
||||
|
||||
@ -21,7 +24,7 @@ public:
|
||||
const ServiceManifest& getManifest() const override;
|
||||
|
||||
/** Retrieve the paths that are relevant to this service */
|
||||
std::unique_ptr<Paths> getPaths() const override;
|
||||
std::unique_ptr<ServicePaths> getPaths() const override;
|
||||
|
||||
std::shared_ptr<Service> getService() const { return service; }
|
||||
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tactility/service/ServiceInstance.h"
|
||||
|
||||
namespace tt::service {
|
||||
|
||||
class ServiceInstancePaths final : public Paths {
|
||||
|
||||
private:
|
||||
|
||||
std::shared_ptr<const ServiceManifest> manifest;
|
||||
|
||||
public:
|
||||
|
||||
explicit ServiceInstancePaths(std::shared_ptr<const ServiceManifest> manifest) : manifest(std::move(manifest)) {}
|
||||
~ServiceInstancePaths() final = default;
|
||||
|
||||
std::string getDataDirectory() const final;
|
||||
std::string getDataDirectoryLvgl() const final;
|
||||
std::string getDataPath(const std::string& childPath) const final;
|
||||
std::string getDataPathLvgl(const std::string& childPath) const final;
|
||||
std::string getSystemDirectory() const final;
|
||||
std::string getSystemDirectoryLvgl() const final;
|
||||
std::string getSystemPath(const std::string& childPath) const final;
|
||||
std::string getSystemPathLvgl(const std::string& childPath) const final;
|
||||
};
|
||||
|
||||
}
|
||||
56
Tactility/Source/Paths.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
#include <Tactility/Paths.h>
|
||||
|
||||
#include <Tactility/app/AppManifestParsing.h>
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/hal/sdcard/SdCardDevice.h>
|
||||
|
||||
#include <format>
|
||||
|
||||
namespace tt {
|
||||
|
||||
bool findFirstMountedSdCardPath(std::string& path) {
|
||||
// const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
bool is_set = false;
|
||||
hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard, [&is_set, &path](const auto& device) {
|
||||
if (device->isMounted()) {
|
||||
path = device->getMountPath();
|
||||
is_set = true;
|
||||
return false; // stop iterating
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return is_set;
|
||||
}
|
||||
|
||||
std::string getSystemRootPath() {
|
||||
std::string root_path;
|
||||
if (!findFirstMountedSdCardPath(root_path)) {
|
||||
root_path = file::MOUNT_POINT_DATA;
|
||||
}
|
||||
return root_path;
|
||||
}
|
||||
|
||||
std::string getTempPath() {
|
||||
return getSystemRootPath() + "/tmp";
|
||||
}
|
||||
|
||||
std::string getAppInstallPath() {
|
||||
return getSystemRootPath() + "/app";
|
||||
}
|
||||
|
||||
std::string getUserPath() {
|
||||
return getSystemRootPath() + "/user";
|
||||
}
|
||||
|
||||
std::string getAppInstallPath(const std::string& appId) {
|
||||
assert(app::isValidId(appId));
|
||||
return std::format("{}/{}", getAppInstallPath(), appId);
|
||||
}
|
||||
|
||||
std::string getAppUserPath(const std::string& appId) {
|
||||
assert(app::isValidId(appId));
|
||||
return std::format("{}/app/{}", getUserPath(), appId);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
#include "Tactility/app/AppManifestParsing.h"
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
@ -158,29 +160,22 @@ static void registerInstalledApp(std::string path) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> manifest;
|
||||
if (!file::loadPropertiesFile(manifest_path, manifest)) {
|
||||
std::map<std::string, std::string> properties;
|
||||
if (!file::loadPropertiesFile(manifest_path, properties)) {
|
||||
TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str());
|
||||
}
|
||||
|
||||
auto app_id_entry = manifest.find("[app]id");
|
||||
if (app_id_entry == manifest.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app id in manifest");
|
||||
return;
|
||||
}
|
||||
|
||||
auto app_name_entry = manifest.find("[app]name");
|
||||
if (app_name_entry == manifest.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app name in manifest");
|
||||
app::AppManifest manifest;
|
||||
if (!app::parseManifest(properties, manifest)) {
|
||||
TT_LOG_E(TAG, "Failed to parse manifest at %s", manifest_path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
app::addApp({
|
||||
.id = app_id_entry->second,
|
||||
.name = app_name_entry->second,
|
||||
.category = app::Category::User,
|
||||
.location = app::Location::external(path)
|
||||
});
|
||||
manifest.appCategory = app::Category::User;
|
||||
manifest.appLocation = app::Location::external(path);
|
||||
|
||||
app::addApp(manifest);
|
||||
}
|
||||
|
||||
static void registerInstalledApps(const std::string& path) {
|
||||
@ -194,7 +189,7 @@ static void registerInstalledApps(const std::string& path) {
|
||||
|
||||
static void registerInstalledAppsFromSdCard(const std::shared_ptr<hal::sdcard::SdCardDevice>& sdcard) {
|
||||
auto sdcard_root_path = sdcard->getMountPath();
|
||||
auto app_path = std::format("{}/apps", sdcard_root_path);
|
||||
auto app_path = std::format("{}/app", sdcard_root_path);
|
||||
sdcard->getLock()->lock();
|
||||
if (file::isDirectory(app_path)) {
|
||||
registerInstalledApps(app_path);
|
||||
@ -289,7 +284,7 @@ void run(const Configuration& config) {
|
||||
TT_LOG_I(TAG, "Starting boot app");
|
||||
// The boot app takes care of registering system apps, user services and user apps
|
||||
addApp(app::boot::manifest);
|
||||
service::loader::startApp(app::boot::manifest.id);
|
||||
service::loader::startApp(app::boot::manifest.appId);
|
||||
|
||||
TT_LOG_I(TAG, "Main dispatcher ready");
|
||||
while (true) {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#include <Tactility/app/App.h>
|
||||
#include "Tactility/Paths.h"
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/app/AppManifestParsing.h>
|
||||
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/file/File.h>
|
||||
@ -103,130 +105,105 @@ static bool untar(const std::string& tarPath, const std::string& destinationPath
|
||||
return success;
|
||||
}
|
||||
|
||||
bool findFirstMountedSdCardPath(std::string& path) {
|
||||
// const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
bool is_set = false;
|
||||
hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard, [&is_set, &path](const auto& device) {
|
||||
if (device->isMounted()) {
|
||||
path = device->getMountPath();
|
||||
is_set = true;
|
||||
return false; // stop iterating
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return is_set;
|
||||
}
|
||||
|
||||
std::string getTempPath() {
|
||||
std::string root_path;
|
||||
if (!findFirstMountedSdCardPath(root_path)) {
|
||||
root_path = file::MOUNT_POINT_DATA;
|
||||
void cleanupInstallDirectory(const std::string& path) {
|
||||
const auto lock = file::getLock(path);
|
||||
lock->lock();
|
||||
if (!file::deleteRecursively(path)) {
|
||||
TT_LOG_W(TAG, "Failed to delete existing installation at %s", path.c_str());
|
||||
}
|
||||
return root_path + "/tmp";
|
||||
}
|
||||
|
||||
std::string getInstallPath() {
|
||||
std::string root_path;
|
||||
if (!findFirstMountedSdCardPath(root_path)) {
|
||||
root_path = file::MOUNT_POINT_DATA;
|
||||
}
|
||||
return root_path + "/apps";
|
||||
lock->unlock();
|
||||
}
|
||||
|
||||
bool install(const std::string& path) {
|
||||
// TODO: Make better: lock for each path type properly (source vs target)
|
||||
|
||||
// We lock and unlock frequently because SPI SD card devices share
|
||||
// the lock with the display. We don't want to lock the display for very long.
|
||||
|
||||
auto app_parent_path = getInstallPath();
|
||||
auto app_parent_path = getAppInstallPath();
|
||||
TT_LOG_I(TAG, "Installing app %s to %s", path.c_str(), app_parent_path.c_str());
|
||||
|
||||
auto lock = file::getLock(app_parent_path)->asScopedLock();
|
||||
auto target_path_lock = file::getLock(app_parent_path)->asScopedLock();
|
||||
|
||||
lock.lock();
|
||||
target_path_lock.lock();
|
||||
auto filename = file::getLastPathSegment(path);
|
||||
const std::string app_target_path = std::format("{}/{}", app_parent_path, filename);
|
||||
if (file::isDirectory(app_target_path) && !file::deleteRecursively(app_target_path)) {
|
||||
TT_LOG_W(TAG, "Failed to delete %s", app_target_path.c_str());
|
||||
}
|
||||
lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
target_path_lock.lock();
|
||||
if (!file::findOrCreateDirectory(app_target_path, 0777)) {
|
||||
TT_LOG_I(TAG, "Failed to create directory %s", app_target_path.c_str());
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
auto source_path_lock = file::getLock(path)->asScopedLock();
|
||||
target_path_lock.lock();
|
||||
source_path_lock.lock();
|
||||
TT_LOG_I(TAG, "Extracting app from %s to %s", path.c_str(), app_target_path.c_str());
|
||||
if (!untar(path, app_target_path)) {
|
||||
TT_LOG_E(TAG, "Failed to extract");
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
source_path_lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
target_path_lock.lock();
|
||||
auto manifest_path = app_target_path + "/manifest.properties";
|
||||
if (!file::isFile(manifest_path)) {
|
||||
TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
target_path_lock.lock();
|
||||
std::map<std::string, std::string> properties;
|
||||
if (!file::loadPropertiesFile(manifest_path, properties)) {
|
||||
TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str());
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
auto app_id_iterator = properties.find("[app]id");
|
||||
if (app_id_iterator == properties.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app id in manifest");
|
||||
AppManifest manifest;
|
||||
if (!parseManifest(properties, manifest)) {
|
||||
TT_LOG_W(TAG, "Invalid manifest");
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto app_name_entry = properties.find("[app]name");
|
||||
if (app_name_entry == properties.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app name in manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, app_id_iterator->second);
|
||||
target_path_lock.lock();
|
||||
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, manifest.appId);
|
||||
if (file::isDirectory(renamed_target_path)) {
|
||||
if (!file::deleteRecursively(renamed_target_path)) {
|
||||
TT_LOG_W(TAG, "Failed to delete existing installation at %s", renamed_target_path.c_str());
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
target_path_lock.lock();
|
||||
if (rename(app_target_path.c_str(), renamed_target_path.c_str()) != 0) {
|
||||
TT_LOG_E(TAG, "Failed to rename %s to %s", app_target_path.c_str(), app_id_iterator->second.c_str());
|
||||
TT_LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", app_target_path.c_str(), manifest.appId.c_str());
|
||||
cleanupInstallDirectory(app_target_path);
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
target_path_lock.unlock();
|
||||
|
||||
addApp({
|
||||
.id = app_id_iterator->second,
|
||||
.name = app_name_entry->second,
|
||||
.category = Category::User,
|
||||
.location = Location::external(renamed_target_path)
|
||||
});
|
||||
manifest.appLocation = Location::external(renamed_target_path);
|
||||
|
||||
addApp(manifest);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uninstall(const std::string& appId) {
|
||||
TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str());
|
||||
auto app_path = getInstallPath() + "/" + appId;
|
||||
return file::withLock<bool>(app_path, [&app_path, &appId]() {
|
||||
auto app_path = getAppInstallPath(appId);
|
||||
return file::withLock<bool>(app_path, [&app_path, &appId] {
|
||||
if (!file::isDirectory(app_path)) {
|
||||
TT_LOG_E(TAG, "App %s not found at ", app_path.c_str());
|
||||
return false;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "Tactility/app/AppInstance.h"
|
||||
#include "Tactility/app/AppInstancePaths.h"
|
||||
#include <Tactility/app/AppInstance.h>
|
||||
#include <Tactility/app/AppPaths.h>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
@ -49,9 +49,9 @@ std::shared_ptr<const Bundle> AppInstance::getParameters() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Paths> AppInstance::getPaths() const {
|
||||
std::unique_ptr<AppPaths> AppInstance::getPaths() const {
|
||||
assert(manifest != nullptr);
|
||||
return std::make_unique<AppInstancePaths>(*manifest);
|
||||
return std::make_unique<AppPaths>(*manifest);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
#include "Tactility/app/AppInstancePaths.h"
|
||||
|
||||
#include <Tactility/MountPoints.h>
|
||||
|
||||
#define LVGL_PATH_PREFIX std::string("A:/")
|
||||
#ifdef ESP_PLATFORM
|
||||
#define PARTITION_PREFIX std::string("/")
|
||||
#else
|
||||
#define PARTITION_PREFIX std::string("")
|
||||
#endif
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
std::string AppInstancePaths::getDataDirectory() const {
|
||||
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getDataDirectoryLvgl() const {
|
||||
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getDataPath(const std::string& childPath) const {
|
||||
assert(!childPath.starts_with('/'));
|
||||
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getDataPathLvgl(const std::string& childPath) const {
|
||||
assert(!childPath.starts_with('/'));
|
||||
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getSystemDirectory() const {
|
||||
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getSystemDirectoryLvgl() const {
|
||||
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getSystemPath(const std::string& childPath) const {
|
||||
assert(!childPath.starts_with('/'));
|
||||
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
|
||||
}
|
||||
|
||||
std::string AppInstancePaths::getSystemPathLvgl(const std::string& childPath) const {
|
||||
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
|
||||
}
|
||||
|
||||
}
|
||||
132
Tactility/Source/app/AppManifestParsing.cpp
Normal file
@ -0,0 +1,132 @@
|
||||
#include <Tactility/app/AppManifestParsing.h>
|
||||
|
||||
#include <regex>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
constexpr auto* TAG = "App";
|
||||
|
||||
static bool validateString(const std::string& value, const std::function<bool(const char)>& isValidChar) {
|
||||
for (const auto& c : value) {
|
||||
if (!isValidChar(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool getValueFromManifest(const std::map<std::string, std::string>& map, const std::string& key, std::string& output) {
|
||||
const auto iterator = map.find(key);
|
||||
if (iterator == map.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
|
||||
return false;
|
||||
}
|
||||
output = iterator->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isValidId(const std::string& id) {
|
||||
return id.size() >= 5 && validateString(id, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == '.';
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidManifestVersion(const std::string& version) {
|
||||
return version.size() > 0 && validateString(version, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == '.';
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidAppVersionName(const std::string& version) {
|
||||
return version.size() > 0 && validateString(version, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == '.' || c == '-' || c == '_';
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidAppVersionCode(const std::string& version) {
|
||||
return version.size() > 0 && validateString(version, [](const char c) {
|
||||
return std::isdigit(c) != 0;
|
||||
});
|
||||
}
|
||||
|
||||
static bool isValidName(const std::string& name) {
|
||||
return name.size() >= 2 && validateString(name, [](const char c) {
|
||||
return std::isalnum(c) != 0 || c == ' ' || c == '-';
|
||||
});
|
||||
}
|
||||
|
||||
bool parseManifest(const std::map<std::string, std::string>& map, AppManifest& manifest) {
|
||||
TT_LOG_I(TAG, "Parsing manifest");
|
||||
|
||||
// [manifest]
|
||||
|
||||
std::string manifest_version;
|
||||
if (!getValueFromManifest(map, "[manifest]version", manifest_version)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidManifestVersion(manifest_version)) {
|
||||
TT_LOG_E(TAG, "Invalid version");
|
||||
return false;
|
||||
}
|
||||
|
||||
// [app]
|
||||
|
||||
if (!getValueFromManifest(map, "[app]id", manifest.appId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidId(manifest.appId)) {
|
||||
TT_LOG_E(TAG, "Invalid app id");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[app]name", manifest.appName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidName(manifest.appName)) {
|
||||
TT_LOG_I(TAG, "Invalid app name");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[app]versionName", manifest.appVersionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionName(manifest.appVersionName)) {
|
||||
TT_LOG_E(TAG, "Invalid app version name");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string version_code_string;
|
||||
if (!getValueFromManifest(map, "[app]versionCode", version_code_string)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAppVersionCode(version_code_string)) {
|
||||
TT_LOG_E(TAG, "Invalid app version code");
|
||||
return false;
|
||||
}
|
||||
|
||||
manifest.appVersionCode = std::stoull(version_code_string);
|
||||
|
||||
// [target]
|
||||
|
||||
if (!getValueFromManifest(map, "[target]sdk", manifest.targetSdk)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!getValueFromManifest(map, "[target]platforms", manifest.targetPlatforms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Defaults
|
||||
|
||||
manifest.appCategory = Category::User;
|
||||
manifest.appLocation = Location::external("");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
44
Tactility/Source/app/AppPaths.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include <Tactility/app/AppPaths.h>
|
||||
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/file/File.h>
|
||||
|
||||
#include <format>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
constexpr auto PARTITION_PREFIX = std::string("/");
|
||||
#else
|
||||
constexpr auto PARTITION_PREFIX = std::string("");
|
||||
#endif
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
std::string AppPaths::getUserDataPath() const {
|
||||
if (manifest.appLocation.isInternal()) {
|
||||
return std::format("{}{}/user/app/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest.appId);
|
||||
} else {
|
||||
return std::format("{}/user/app/{}", file::getFirstPathSegment(manifest.appLocation.getPath()), manifest.appId);
|
||||
}
|
||||
}
|
||||
|
||||
std::string AppPaths::getUserDataPath(const std::string& childPath) const {
|
||||
assert(!childPath.starts_with('/'));
|
||||
return std::format("{}/{}", getUserDataPath(), childPath);
|
||||
}
|
||||
|
||||
|
||||
std::string AppPaths::getAssetsDirectory() const {
|
||||
if (manifest.appLocation.isInternal()) {
|
||||
return std::format("{}{}/app/{}/assets", PARTITION_PREFIX, file::SYSTEM_PARTITION_NAME, manifest.appId);
|
||||
} else {
|
||||
return std::format("{}/assets", manifest.appLocation.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
std::string AppPaths::getAssetsPath(const std::string& childPath) const {
|
||||
assert(!childPath.starts_with('/'));
|
||||
return std::format("{}/{}", getAssetsDirectory(), childPath);
|
||||
}
|
||||
|
||||
}
|
||||
@ -16,15 +16,15 @@ static AppManifestMap app_manifest_map;
|
||||
static Mutex hash_mutex(Mutex::Type::Normal);
|
||||
|
||||
void addApp(const AppManifest& manifest) {
|
||||
TT_LOG_I(TAG, "Registering manifest %s", manifest.id.c_str());
|
||||
TT_LOG_I(TAG, "Registering manifest %s", manifest.appId.c_str());
|
||||
|
||||
hash_mutex.lock();
|
||||
|
||||
if (app_manifest_map.contains(manifest.id)) {
|
||||
TT_LOG_W(TAG, "Overwriting existing manifest for %s", manifest.id.c_str());
|
||||
if (app_manifest_map.contains(manifest.appId)) {
|
||||
TT_LOG_W(TAG, "Overwriting existing manifest for %s", manifest.appId.c_str());
|
||||
}
|
||||
|
||||
app_manifest_map[manifest.id] = std::make_shared<AppManifest>(manifest);
|
||||
app_manifest_map[manifest.appId] = std::make_shared<AppManifest>(manifest);
|
||||
|
||||
hash_mutex.unlock();
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ private:
|
||||
|
||||
auto relocate_result = esp_elf_relocate(&elf, elfFileData.get());
|
||||
if (relocate_result != 0) {
|
||||
// Note: the result code mapes to values from cstdlib's errno.h
|
||||
// Note: the result code maps to values from cstdlib's errno.h
|
||||
lastError = getErrorCodeString(-relocate_result);
|
||||
TT_LOG_E(TAG, "Application failed to load: %s", lastError.c_str());
|
||||
elfFileData = nullptr;
|
||||
@ -223,8 +223,8 @@ void setElfAppParameters(
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
|
||||
TT_LOG_I(TAG, "createElfApp");
|
||||
assert(manifest != nullptr);
|
||||
assert(manifest->location.isExternal());
|
||||
return std::make_shared<ElfApp>(manifest->location.getPath());
|
||||
assert(manifest->appLocation.isExternal());
|
||||
return std::make_shared<ElfApp>(manifest->appLocation.getPath());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -172,16 +172,16 @@ public:
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "AddGps",
|
||||
.name = "Add GPS",
|
||||
.icon = LV_SYMBOL_GPS,
|
||||
.category = Category::System,
|
||||
.flags = AppManifest::Flags::Hidden,
|
||||
.appId = "AddGps",
|
||||
.appName = "Add GPS",
|
||||
.appIcon = LV_SYMBOL_GPS,
|
||||
.appCategory = Category::System,
|
||||
.appFlags = AppManifest::Flags::Hidden,
|
||||
.createApp = create<AddGpsApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
app::start(manifest.id);
|
||||
app::start(manifest.appId);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -28,7 +28,7 @@ LaunchId start(const std::string& title, const std::string& message, const std::
|
||||
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
|
||||
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
|
||||
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined);
|
||||
return service::loader::startApp(manifest.id, bundle);
|
||||
return service::loader::startApp(manifest.appId, bundle);
|
||||
}
|
||||
|
||||
LaunchId start(const std::string& title, const std::string& message) {
|
||||
@ -36,7 +36,7 @@ LaunchId start(const std::string& title, const std::string& message) {
|
||||
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
|
||||
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
|
||||
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK");
|
||||
return service::loader::startApp(manifest.id, bundle);
|
||||
return service::loader::startApp(manifest.appId, bundle);
|
||||
}
|
||||
|
||||
int32_t getResultIndex(const Bundle& bundle) {
|
||||
@ -126,10 +126,10 @@ public:
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "AlertDialog",
|
||||
.name = "Alert Dialog",
|
||||
.category = Category::System,
|
||||
.flags = AppManifest::Flags::Hidden,
|
||||
.appId = "AlertDialog",
|
||||
.appName = "Alert Dialog",
|
||||
.appCategory = Category::System,
|
||||
.appFlags = AppManifest::Flags::Hidden,
|
||||
.createApp = create<AlertDialogApp>
|
||||
};
|
||||
|
||||
|
||||