Compare commits

...

2 Commits

Author SHA1 Message Date
Ken Van Hoeylandt
34ca63ec3c
LVGL terminal rendering, elf memleak fixes and posix support (#669) 2026-10-03 19:02:07 +02:00
Shadowtrance
ffab897fe4
LilyGo T-Embed CC1101 Plus & Led Strip Driver (#670) 2026-10-03 18:58:55 +02:00
72 changed files with 4374 additions and 77 deletions

1
.gitignore vendored
View File

@ -26,5 +26,6 @@ sdkconfig.board.*.dev
.caveman.json
.ai/mcp
.clinerules/
__pycache__

View File

@ -0,0 +1,6 @@
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
REQUIRES TactilityKernel Tactility
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,28 @@
general.vendor=LilyGO
general.name=T-Embed CC1101 Plus
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=80M
hardware.spiRamXipDisabled=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=80M
hardware.bluetooth=true
storage.userDataLocation=SD
display.size=1.9"
display.shape=rectangle
display.dpi=191
lvgl.colorDepth=16
lvgl.fontSize=16
lvgl.uiDensity=compact
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y

View File

@ -0,0 +1,186 @@
/dts-v1/;
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789.h>
#include <bindings/dummy_i2s_amp.h>
#include <bindings/pdm_mic.h>
#include <bindings/bq25896.h>
#include <bindings/bq27220.h>
#include <bindings/gpio_encoder.h>
#include <bindings/esp32_led_strip.h>
// Reference:
// https://wiki.lilygo.cc/products/t-embed-series/t-embed-cc1101/
// https://github.com/Xinyuan-LilyGO/T-Embed-CC1101
// https://lilygo.cc/products/t-embed-cc1101-plus
/ {
compatible = "root";
model = "T-Embed CC1101 Plus";
adc0 {
compatible = "espressif,esp32-adc-oneshot";
unit-id = <ADC_UNIT_1>;
clk-src = <ADC_RTC_CLK_SRC_DEFAULT>;
channels = <ADC_CHANNEL_3 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2110>;
};
wifi0 {
compatible = "espressif,esp32-wifi-pinned";
};
ble0 {
compatible = "espressif,esp32-ble";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
// Required to enable peripherals like BQ25896
peripheral-power-hog {
compatible = "gpio-hog";
pin = <&gpio0 15 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_HIGH>;
};
// Speaker I2S (MAX98357A amplifier)
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 46 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 40 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 7 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 42 GPIO_FLAG_NONE>;
pin-mclk = <&gpio0 39 GPIO_FLAG_NONE>;
};
// MAX98357A class-D speaker amplifier. No I2C.
speaker0 {
compatible = "maxim,max98357a";
i2s = <&i2s0>;
};
mic0 {
compatible = "generic,spm1423";
i2s = <&i2s0>;
channels = <1>;
};
// WS2812 RGB LED Ring
// TODO - Sync on/off with DisplayIdle?
ledstrip0 {
compatible = "espressif,esp32-led-strip";
pin-data = <&gpio0 14 GPIO_FLAG_NONE>;
led-count = <8>;
};
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>;
clock-frequency = <100000>;
pin-sda = <&gpio0 8 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 18 GPIO_FLAG_NONE>;
bq27220 {
compatible = "ti,bq27220";
reg = <0x55>;
};
bq25896 {
compatible = "ti,bq25896";
reg = <0x6B>;
};
// TODO - Add PN532 NFC - IRQ GPIO 17, RES GPIO 45, address 0x24 - driver needed
};
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 21 GPIO_FLAG_NONE>;
period-ns = <33333>;
ledc-timer = <0>;
ledc-channel = <0>;
};
display_backlight {
compatible = "pwm-backlight";
// Off by default so dispaly power-on won't show the screen from before the last power loss.
// The display backlight is turned on during the boot process.
status = "disabled";
pwm = <&display_backlight_pwm>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
cs-gpios = <&gpio0 41 GPIO_FLAG_NONE>, // Display
<&gpio0 13 GPIO_FLAG_NONE>, // SD card
<&gpio0 12 GPIO_FLAG_NONE>, // Radio - CC1101
<&gpio0 44 GPIO_FLAG_NONE>; // Radio - nRF24L01
pin-mosi = <&gpio0 9 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 10 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>;
display@0 {
compatible = "sitronix,st7789";
horizontal-resolution = <320>;
vertical-resolution = <170>;
gap-x = <35>;
swap-xy;
mirror-y;
invert-color;
pixel-clock-hz = <80000000>;
pin-dc = <&gpio0 16 GPIO_FLAG_NONE>;
pin-reset = <&gpio0 40 GPIO_FLAG_NONE>;
backlight = <&display_backlight>;
};
sdcard@1 {
compatible = "espressif,esp32-sdspi";
status = "disabled";
frequency-khz = <20000>;
};
// TODO - Add CC1101 Radio - CS GPIO 12, SCK GPIO 11, MOSI GPIO 9, MISO GPIO 10, IO2 GPIO 38, IO0 GPIO 3, SW1 GPIO 47, SW0 GPIO 48 - driver needed
// TODO - Add nRF24L01 Radio - CS GPIO 44, CE GPIO 43, IQR -1/NC, SCK GPIO 11, MOSI GPIO 9, MISO GPIO 10 - driver needed
};
encoder {
compatible = "tactility,gpio-encoder";
pin-a = <&gpio0 5 GPIO_FLAG_NONE>;
pin-b = <&gpio0 4 GPIO_FLAG_NONE>;
pin-enter = <&gpio0 0 GPIO_FLAG_NONE>;
pulses-per-detent = <2>;
pending-capacity = <8>;
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};

View File

@ -0,0 +1,11 @@
dependencies:
- Platforms/platform-esp32
- Drivers/bq27220-module
- Drivers/bq25896-module
- Drivers/gpio-encoder-module
- Drivers/st7789-module
- Drivers/dummy-i2s-amp-module
- Drivers/pdm-mic-module
- Drivers/audio-stream-module
- Drivers/esp32-led-strip-module
dts: lilygo,tembed-cc1101-plus.dts

View File

@ -0,0 +1,49 @@
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <tactility/device.h>
#include <tactility/drivers/led_strip.h>
#include <tactility/system_event.h>
#include <Tactility/settings/LedStripSettings.h>
constexpr auto* TAG = "tembed-cc1101-plus";
extern "C" {
static void on_boot_completed(SystemEvent* event, void* context) {
(void)event;
(void)context;
Device* device = nullptr;
if (device_get_first_by_type(&LED_STRIP_TYPE, &device) != ERROR_NONE) {
LOG_W(TAG, "No LED strip found during boot restore");
return;
}
const auto settings = tt::settings::ledstrip::loadOrGetDefault(device->name);
const error_t result = tt::settings::ledstrip::apply(device, settings);
if (result != ERROR_NONE) {
LOG_E(TAG, "Failed to restore LED strip %s: %d", device->name, result);
} else {
LOG_I(TAG, "Restored LED strip %s", device->name);
}
device_put(device);
}
static error_t start() {
return system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr);
}
static error_t stop() {
return system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed);
}
Module lilygo_tembed_cc1101_plus_module = {
.name = "lilygo-tembed-cc1101-plus",
.start = start,
.stop = stop,
.drivers = nullptr,
.symbols = nullptr,
.internal = nullptr
};
}

View File

@ -7,7 +7,7 @@
## Higher Priority
- coreutils apps: Only emit colour escapes when stdout is the terminal (command_support.h color() function)
- NimBLE looses pairing key after reboot
- CrashDiagnostics shouldn't show a QR when there's no callstack
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
- Add bold fonts for e-ink readability improvement
@ -18,7 +18,7 @@
- TCA9534 keyboards should use interrupts
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
- Support direct installation of an `.app` file with `tactility.py install helloworld.app <ip>`
- Support direct installation of an `.app` file with `tactility.py install --app helloworld.app`
- minitar/untarFile(): "entry->metadata.path" can escape its confined path (e.g. "../something")
## Medium Priority

View File

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(esp32-led-strip-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_driver_rmt driver
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,88 @@
# esp32-led-strip-module
WS2812-compatible RGB LED strip driver for Tactility. It uses ESP-IDF's RMT peripheral to transmit pixel data and provides per-pixel and range color control, brightness adjustment, and gamma-corrected output.
## Devicetree Configuration
Declare the strip with the compatible string `"espressif,esp32-led-strip"`. `pin-data` references the GPIO device and pin used for the data signal; `led-count` is the number of LEDs on the strip.
```dts
ws2812 {
compatible = "espressif,esp32-led-strip";
pin-data = <&gpio0 14 GPIO_FLAG_NONE>;
led-count = <8>;
};
```
The driver reads these properties into `LedStripConfig`.
## API
Include `<tactility/drivers/led_strip.h>`. The kernel API operates on a `struct Device *` and returns `error_t`; successful calls return `ERROR_NONE`.
| Function | Description |
|---|---|
| `led_strip_set_single_led_color` | Set one LED using global brightness |
| `led_strip_set_single_led_color_and_brightness` | Set one LED with an explicit brightness |
| `led_strip_set_led_range_color` | Set a range from an array of `LedRgb` values using global brightness |
| `led_strip_set_led_range_color_and_brightness` | Set a range from an array with explicit brightness |
| `led_strip_fill_led_range_color` | Fill a range using global brightness |
| `led_strip_fill_led_range_color_and_brightness` | Fill a range with explicit brightness |
| `led_strip_set_brightness` | Set global brightness |
| `led_strip_show` | Transmit buffered pixel values to the strip |
| `led_strip_clear` | Turn off all LEDs |
| `led_strip_get_length` | Get the configured LED count |
LED indices and range offsets are zero-based. Color channels in `struct LedRgb` range from 0 to 255. Global and per-operation brightness also range from 0 (off) to 255 (full); the default global brightness is 255. Call `led_strip_show()` after setting colors to transmit the updated buffer. `led_strip_clear()` clears and refreshes the strip.
## Usage Example
```c
#include <tactility/drivers/led_strip.h>
void led_demo(struct Device *strip) {
uint16_t length = 0;
if (led_strip_get_length(strip, &length) != ERROR_NONE || length == 0) {
return;
}
led_strip_set_brightness(strip, 128); // About 50% brightness
led_strip_fill_led_range_color(strip, 0, length, LED_RGB_COLOR_GREEN);
led_strip_show(strip);
led_strip_set_single_led_color(strip, 0, LED_RGB_COLOR_RED);
led_strip_show(strip);
led_strip_clear(strip);
}
```
## Using Literal RGB Values
Color macros are optional. Initialize `struct LedRgb` directly when a color is calculated or loaded at runtime. This example assumes the strip has at least four LEDs.
```c
#include <tactility/drivers/led_strip.h>
void show_custom_colors(struct Device *strip) {
const struct LedRgb colors[] = {
{ .r = 255, .g = 32, .b = 0 },
{ .r = 0, .g = 180, .b = 64 },
{ .r = 24, .g = 48, .b = 255 },
{ .r = 200, .g = 80, .b = 160 }
};
led_strip_set_led_range_color_and_brightness(strip, 0, 4, colors, 192);
led_strip_show(strip);
}
```
## Color Presets
The kernel header also provides `LED_RGB_COLOR_*` macros such as `LED_RGB_COLOR_RED`, `LED_RGB_COLOR_GREEN`, and `LED_RGB_COLOR_WHITE`, plus `LED_COLOR_RGB_CUSTOM(r, g, b)` for a custom color. These expand to `struct LedRgb` values.
## Implementation Notes
- The driver configures the ESP-IDF RMT backend for WS2812 RGB output in GRB component order.
- An 8-bit gamma correction lookup table is applied after brightness scaling.
- The devicetree compatible string and driver registration are `"espressif,esp32-led-strip"`; the kernel device type is `LED_STRIP_TYPE`.

View File

@ -0,0 +1,13 @@
description: RGB LED strip. ESP-IDF's RMT peripheral to transmit pixel data to drive WS2812-compatible LEDs.
compatible: "espressif,esp32-led-strip"
properties:
pin-data:
type: phandles
required: true
description: GPIO pin for strip data output
led-count:
type: int
required: true
description: Number of LEDs in the strip

View File

@ -0,0 +1,14 @@
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/esp32_led_strip.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_led_strip, struct LedStripConfig)
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/device.h>
#include <tactility/error.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/led_strip.h>
#include <esp32_led_strip_types.h>
struct Device;
#ifdef __cplusplus
extern "C" {
#endif
struct LedStripConfig {
/** Data Pin */
struct GpioPinSpec pin_data;
/** LED Count */
uint16_t led_count;
};
/**
* @brief Sets the color of a single LED on the strip with global brightness.
* @param[in] device the LED strip device
* @param[in] index index of the LED in the strip (0-based)
* @param[in] color the color to set
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_set_single_led_color(struct Device* device, size_t index, const struct LedRgb color);
/**
* @brief Sets the color of a single LED on the strip with an explicit brightness value.
* @param[in] device the LED strip device
* @param[in] index index of the LED in the strip (0-based)
* @param[in] color the color to set
* @param[in] brightness 0-255
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_set_single_led_color_and_brightness(struct Device* device, size_t index, const struct LedRgb color, uint8_t brightness);
/**
* @brief Sets colors for a range of LEDs on the strip using the global brightness.
* @param[in] device the LED strip device
* @param[in] first starting LED index (0-based)
* @param[in] count number of LEDs to set
* @param[in] color array of colors to set
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_set_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb* color);
/**
* @brief Sets colors for a range of LEDs on the strip with an explicit brightness value.
* @param[in] device the LED strip device
* @param[in] first starting LED index (0-based)
* @param[in] count number of LEDs to set
* @param[in] color array of colors to set
* @param[in] brightness 0-255
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_set_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb* color, uint8_t brightness);
/**
* @brief Fills a range of LEDs with a single color using the global brightness.
* @param[in] device the LED strip device
* @param[in] first starting LED index (0-based)
* @param[in] count number of LEDs to fill
* @param[in] color the color to fill with
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_fill_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb color);
/**
* @brief Fills a range of LEDs with a single color and an explicit brightness value.
* @param[in] device the LED strip device
* @param[in] first starting LED index (0-based)
* @param[in] count number of LEDs to fill
* @param[in] color the color to fill with
* @param[in] brightness 0-255
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_fill_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb color, uint8_t brightness);
/**
* @brief Sets global brightness for the LED strip.
* @param[in] device the LED strip device
* @param[in] brightness 0-255
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_set_brightness(struct Device* device, uint8_t brightness);
/**
* @brief Refreshes the strip to update the displayed colors.
* @param[in] device the LED strip device
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_show(struct Device* device);
/**
* @brief Clears the strip to black.
* @param[in] device the LED strip device
* @retval ERROR_NONE when the operation was successful
*/
error_t esp32_led_strip_clear(struct Device* device);
/**
* @brief Gets the length of a strip.
* @param[in] device the LED strip device
* @param[out] out_length receives the strip length
* @retval ERROR_NONE when the operation was successful
* @retval ERROR_INVALID_ARGUMENT if out of range
*/
error_t esp32_led_strip_get_length(struct Device* device, uint16_t* out_length);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module esp32_led_strip_module;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "esp32_led_strip_types.h"
#include "esp_idf_version.h"
#include "driver/rmt_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LED Strip RMT specific configuration
*/
typedef struct {
rmt_clock_source_t clk_src; /*!< RMT clock source */
uint32_t resolution_hz; /*!< RMT tick resolution, if set to zero, a default resolution (10MHz) will be applied */
size_t mem_block_symbols; /*!< How many RMT symbols can one RMT channel hold at one time. Set to 0 will fallback to use the default size. */
/*!< Extra RMT specific driver flags */
struct led_strip_rmt_extra_config {
uint32_t with_dma: 1; /*!< Use DMA to transmit data */
} flags; /*!< Extra driver flags */
} led_strip_rmt_config_t;
/**
* @brief Create LED strip based on RMT TX channel
*
* @param led_config LED strip configuration
* @param rmt_config RMT specific configuration
* @param ret_strip Returned LED strip handle
* @return
* - ESP_OK: create LED strip handle successfully
* - ESP_ERR_INVALID_ARG: create LED strip handle failed because of invalid argument
* - ESP_ERR_NO_MEM: create LED strip handle failed because of out of memory
* - ESP_FAIL: create LED strip handle failed because some other error
*/
esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, led_strip_handle_t *ret_strip);
/**
* @brief Set RGB for a specific pixel
*
* @param strip: LED strip
* @param index: index of pixel to set
* @param red: red part of color
* @param green: green part of color
* @param blue: blue part of color
*
* @return
* - ESP_OK: Set RGB for a specific pixel successfully
* - ESP_ERR_INVALID_ARG: Set RGB for a specific pixel failed because of invalid parameters
* - ESP_FAIL: Set RGB for a specific pixel failed because other error occurred
*/
esp_err_t led_strip_rmt_set_pixel(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue);
/**
* @brief Refresh memory colors to LEDs
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Refresh successfully
* - ESP_FAIL: Refresh failed because some other error occurred
*
* @note:
* After updating the LED colors in the memory, a following invocation of this API is needed to flush colors to strip.
*/
esp_err_t led_strip_rmt_refresh(led_strip_handle_t strip);
/**
* @brief Clear LED strip (turn off all LEDs)
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Clear LEDs successfully
* - ESP_FAIL: Clear LEDs failed because some other error occurred
*/
esp_err_t led_strip_rmt_clear(led_strip_handle_t strip);
/**
* @brief Free LED strip resources
*
* @param strip: LED strip
*
* @return
* - ESP_OK: Free resources successfully
* - ESP_FAIL: Free resources failed because error occurred
*/
esp_err_t led_strip_rmt_del(led_strip_handle_t strip);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "driver/rmt_encoder.h"
#include <esp32_led_strip_types.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Type of led strip encoder configuration
*/
typedef struct {
uint32_t resolution; /*!< Encoder resolution, in Hz */
led_model_t led_model; /*!< LED model */
} led_strip_encoder_config_t;
/**
* @brief Create RMT encoder for encoding LED strip pixels into RMT symbols
*
* @param[in] config Encoder configuration
* @param[out] ret_encoder Returned encoder handle
* @return
* - ESP_ERR_INVALID_ARG for any invalid arguments
* - ESP_ERR_NO_MEM out of memory when creating led strip encoder
* - ESP_OK if creating encoder successfully
*/
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Type of LED strip handle
*/
typedef struct led_strip_t led_strip_t;
typedef led_strip_t *led_strip_handle_t;
/**
* @brief LED strip model
* @note Different led model may have different timing parameters, so we need to distinguish them.
*/
typedef enum {
LED_MODEL_WS2812, /*!< LED strip model: WS2812 */
LED_MODEL_SK6812, /*!< LED strip model: SK6812 */
LED_MODEL_WS2811, /*!< LED strip model: WS2811 */
LED_MODEL_WS2816, /*!< LED strip model: WS2816 */
LED_MODEL_INVALID /*!< Invalid LED strip model */
} led_model_t;
/**
* @brief LED color component format
* @note The format is used to specify the order of color components in each pixel, also the number of color components.
*/
typedef union {
struct format_layout {
uint32_t r_pos: 2; /*!< Position of the red channel in the color order: 0~3 */
uint32_t g_pos: 2; /*!< Position of the green channel in the color order: 0~3 */
uint32_t b_pos: 2; /*!< Position of the blue channel in the color order: 0~3 */
uint32_t w_pos: 2; /*!< Position of the white channel in the color order: 0~3 */
uint32_t reserved: 19; /*!< Reserved */
uint32_t bytes_per_color: 2; /*!< Bytes per color component: 1 or 2. If set to 0, it will fallback to 1 */
uint32_t num_components: 3; /*!< Number of color components per pixel: 3 or 4. If set to 0, it will fallback to 3 */
} format; /*!< Format layout */
uint32_t format_id; /*!< Format ID */
} led_color_component_format_t;
/// Helper macros to set the color component format
#define LED_STRIP_COLOR_COMPONENT_FMT_GRB (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 3}}
#define LED_STRIP_COLOR_COMPONENT_FMT_GRB_16 (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 3}}
#define LED_STRIP_COLOR_COMPONENT_FMT_GRBW (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 4}}
#define LED_STRIP_COLOR_COMPONENT_FMT_GRBW_16 (led_color_component_format_t){.format = {.r_pos = 1, .g_pos = 0, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 4}}
#define LED_STRIP_COLOR_COMPONENT_FMT_RGB (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 3}}
#define LED_STRIP_COLOR_COMPONENT_FMT_RGB_16 (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 3}}
#define LED_STRIP_COLOR_COMPONENT_FMT_RGBW (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 1, .num_components = 4}}
#define LED_STRIP_COLOR_COMPONENT_FMT_RGBW_16 (led_color_component_format_t){.format = {.r_pos = 0, .g_pos = 1, .b_pos = 2, .w_pos = 3, .reserved = 0, .bytes_per_color = 2, .num_components = 4}}
/**
* @brief LED Strip common configurations
* The common configurations are not specific to any backend peripheral.
*/
typedef struct {
int strip_gpio_num; /*!< GPIO number that used by LED strip */
uint32_t max_leds; /*!< Maximum number of LEDs that can be controlled in a single strip */
led_model_t led_model; /*!< Specifies the LED strip model (e.g., WS2812, SK6812) */
led_color_component_format_t color_component_format; /*!< Specifies the order of color components in each pixel.
Use helper macros like `LED_STRIP_COLOR_COMPONENT_FMT_GRB` to set the format */
/*!< LED strip extra driver flags */
struct led_strip_extra_flags {
uint32_t invert_out: 1; /*!< Invert output signal */
} flags; /*!< Extra driver flags */
} led_strip_config_t;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings

View File

@ -0,0 +1,258 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <esp32_led_strip_module.h>
#include <tactility/drivers/led_strip.h>
#include <tactility/drivers/gpio.h>
#include <driver/gpio.h>
#include <esp_log.h>
#include <drivers/esp32_led_strip.h>
#include <cstdlib>
#include <esp32_led_strip_rmt.h>
constexpr auto* TAG = "Esp32LedStrip";
#define GET_CONFIG(device) (static_cast<const LedStripConfig*>((device)->config))
#define GET_INTERNAL(device) (static_cast<LedStripInternal*>(device_get_driver_data(device)))
struct LedStripInternal {
led_strip_handle_t strip;
GpioPinSpec data_pin;
uint16_t led_count;
uint8_t global_brightness;
};
// Gamma correction lookup table for 8-bit brightness/color scaling
static const uint8_t gamma8[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3,
3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 7,
7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11, 11, 11, 12, 12, 13,
13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20,
21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 27, 28, 29, 29, 30,
31, 31, 32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58,
59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75,
76, 77, 78, 79, 81, 82, 83, 84, 86, 87, 88, 90, 91, 92, 94, 95,
96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117, 119,
121, 122, 124, 125, 127, 129, 130, 132, 134, 135, 137, 139, 141, 142, 144, 146,
148, 150, 151, 153, 155, 157, 159, 161, 163, 165, 166, 168, 170, 172, 174, 176,
178, 180, 182, 184, 186, 188, 191, 193, 195, 197, 199, 201, 204, 206, 208, 210,
213, 215, 217, 220, 222, 224, 227, 229, 231, 234, 236, 239, 241, 244, 246, 249,
251, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
};
static error_t create_strip(const LedStripConfig* source, led_strip_handle_t* out_strip) {
led_strip_config_t strip_config_internal = {
.strip_gpio_num = static_cast<gpio_num_t>(source->pin_data.pin),
.max_leds = source->led_count,
.led_model = LED_MODEL_WS2812,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
.flags = {
.invert_out = false,
}
};
led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000,
.mem_block_symbols = 0,
.flags = {
.with_dma = 0,
}
};
led_strip_handle_t strip = nullptr;
esp_err_t err = led_strip_new_rmt_device(&strip_config_internal, &rmt_config, &strip);
if (err != ESP_OK) {
return ERROR_RESOURCE;
}
*out_strip = strip;
return ERROR_NONE;
}
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<LedStripInternal*>(std::calloc(1, sizeof(LedStripInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->data_pin = static_cast<GpioPinSpec>(config->pin_data);
internal->led_count = config->led_count;
internal->global_brightness = 255;
led_strip_handle_t strip = nullptr;
if (create_strip(config, &strip) != ERROR_NONE) {
std::free(internal);
return ERROR_RESOURCE;
}
internal->strip = strip;
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = GET_INTERNAL(device);
led_strip_rmt_del(internal->strip);
std::free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static error_t apply_single(Device* device, size_t index, const LedRgb& color, uint8_t brightness) {
auto* internal = GET_INTERNAL(device);
if (index >= internal->led_count) return ERROR_INVALID_ARGUMENT;
uint32_t sr = (color.r * brightness) / 255;
uint32_t sg = (color.g * brightness) / 255;
uint32_t sb = (color.b * brightness) / 255;
esp_err_t err = led_strip_rmt_set_pixel(internal->strip, index, gamma8[sr], gamma8[sg], gamma8[sb]);
if (err != ESP_OK) {
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static error_t apply_range(Device* device, size_t first, size_t count, const LedRgb* colors, uint8_t brightness) {
auto* internal = GET_INTERNAL(device);
if (first > internal->led_count) return ERROR_INVALID_ARGUMENT;
if (count > internal->led_count - first) return ERROR_INVALID_ARGUMENT;
if (colors == nullptr) return ERROR_INVALID_ARGUMENT;
for (size_t i = 0; i < count; i++) {
uint32_t sr = (colors[i].r * brightness) / 255;
uint32_t sg = (colors[i].g * brightness) / 255;
uint32_t sb = (colors[i].b * brightness) / 255;
esp_err_t err = led_strip_rmt_set_pixel(internal->strip, first + i, gamma8[sr], gamma8[sg], gamma8[sb]);
if (err != ESP_OK) {
return ERROR_RESOURCE;
}
}
return ERROR_NONE;
}
static error_t apply_fill(Device* device, size_t first, size_t count, const LedRgb& color, uint8_t brightness)
{
auto* internal = GET_INTERNAL(device);
if (first > internal->led_count) return ERROR_INVALID_ARGUMENT;
if (count > internal->led_count - first) return ERROR_INVALID_ARGUMENT;
for (size_t i = 0; i < count; i++) {
uint32_t sr = (color.r * brightness) / 255;
uint32_t sg = (color.g * brightness) / 255;
uint32_t sb = (color.b * brightness) / 255;
esp_err_t err = led_strip_rmt_set_pixel(internal->strip, first + i, gamma8[sr], gamma8[sg], gamma8[sb]);
if (err != ESP_OK) {
return ERROR_RESOURCE;
}
}
return ERROR_NONE;
}
extern "C" {
error_t esp32_led_strip_set_single_led_color(Device* device, size_t index, const LedRgb color) {
auto* internal = GET_INTERNAL(device);
return apply_single(device, index, color, internal->global_brightness);
}
error_t esp32_led_strip_set_single_led_color_and_brightness(Device* device, size_t index, const LedRgb color, uint8_t brightness) {
return apply_single(device, index, color, brightness);
}
error_t esp32_led_strip_set_led_range_color(Device* device, size_t first, size_t count, const LedRgb* color) {
auto* internal = GET_INTERNAL(device);
return apply_range(device, first, count, color, internal->global_brightness);
}
error_t esp32_led_strip_set_led_range_color_and_brightness(Device* device, size_t first, size_t count, const LedRgb* color, uint8_t brightness) {
return apply_range(device, first, count, color, brightness);
}
error_t esp32_led_strip_fill_led_range_color(Device* device, size_t first, size_t count, const LedRgb color) {
auto* internal = GET_INTERNAL(device);
return apply_fill(device, first, count, color, internal->global_brightness);
}
error_t esp32_led_strip_fill_led_range_color_and_brightness(Device* device, size_t first, size_t count, const LedRgb color, uint8_t brightness) {
return apply_fill(device, first, count, color, brightness);
}
error_t esp32_led_strip_set_brightness(Device* device, uint8_t brightness) {
auto* internal = GET_INTERNAL(device);
internal->global_brightness = brightness;
return ERROR_NONE;
}
error_t esp32_led_strip_show(Device* device) {
auto* internal = GET_INTERNAL(device);
if (internal->strip != nullptr) {
esp_err_t err = led_strip_rmt_refresh(internal->strip);
if (err != ESP_OK) {
return ERROR_RESOURCE;
}
}
return ERROR_NONE;
}
error_t esp32_led_strip_clear(Device* device) {
auto* internal = GET_INTERNAL(device);
if (internal->strip != nullptr) {
esp_err_t err = led_strip_rmt_clear(internal->strip);
if (err != ESP_OK) {
return ERROR_RESOURCE;
}
}
return ERROR_NONE;
}
error_t esp32_led_strip_get_length(Device* device, uint16_t* out_length) {
auto* internal = GET_INTERNAL(device);
*out_length = internal->led_count;
return ERROR_NONE;
}
static constexpr LedStripApi LED_STRIP_API = {
.set_single_led_color = esp32_led_strip_set_single_led_color,
.set_single_led_color_and_brightness = esp32_led_strip_set_single_led_color_and_brightness,
.set_led_range_color = esp32_led_strip_set_led_range_color,
.set_led_range_color_and_brightness = esp32_led_strip_set_led_range_color_and_brightness,
.fill_led_range_color = esp32_led_strip_fill_led_range_color,
.fill_led_range_color_and_brightness = esp32_led_strip_fill_led_range_color_and_brightness,
.set_brightness = esp32_led_strip_set_brightness,
.show = esp32_led_strip_show,
.clear = esp32_led_strip_clear,
.get_length = esp32_led_strip_get_length,
};
extern Module esp32_led_strip_module;
Driver esp32_led_strip_driver = {
.name = "esp32_led_strip",
.compatible = (const char*[]) { "espressif,esp32-led-strip", nullptr },
.start_device = start,
.stop_device = stop,
.probe = nullptr,
.api = &LED_STRIP_API,
.device_type = &LED_STRIP_TYPE,
.owner = &esp32_led_strip_module,
.internal = nullptr,
};
} // extern "C"

View File

@ -0,0 +1,170 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdlib.h>
#include <string.h>
#include <sys/cdefs.h>
#include "esp_log.h"
#include "esp_check.h"
#include "driver/rmt_tx.h"
#include "drivers/esp32_led_strip.h"
#include "esp32_led_strip_rmt_encoder.h"
#include "esp32_led_strip_rmt.h"
#define LED_STRIP_RMT_DEFAULT_RESOLUTION 10000000 // 10MHz resolution
#define LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE 4
// the memory size of each RMT channel, in words (4 bytes)
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 64
#else
#define LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 48
#endif
static const char *TAG = "led_strip_rmt";
typedef struct led_strip_t {
rmt_channel_handle_t rmt_chan;
rmt_encoder_handle_t strip_encoder;
uint32_t strip_len;
uint8_t bytes_per_pixel;
led_color_component_format_t component_fmt;
uint8_t pixel_buf[];
} led_strip_rmt_obj;
esp_err_t led_strip_rmt_set_pixel(led_strip_handle_t strip, uint32_t index, uint32_t red, uint32_t green, uint32_t blue)
{
led_strip_rmt_obj *rmt_strip = strip;
ESP_RETURN_ON_FALSE(index < rmt_strip->strip_len, ESP_ERR_INVALID_ARG, TAG, "index out of maximum number of LEDs");
struct format_layout format = rmt_strip->component_fmt.format;
uint32_t start = index * rmt_strip->bytes_per_pixel;
uint8_t *pixel_buf = rmt_strip->pixel_buf;
uint8_t pos_bytes = format.bytes_per_color;
for (uint8_t i = 0; i < format.bytes_per_color; i++) {
uint8_t color_shift = 8 * (format.bytes_per_color - 1 - i);
pixel_buf[start + format.r_pos * pos_bytes + i] = (red >> color_shift) & 0xFF;
pixel_buf[start + format.g_pos * pos_bytes + i] = (green >> color_shift) & 0xFF;
pixel_buf[start + format.b_pos * pos_bytes + i] = (blue >> color_shift) & 0xFF;
if (format.num_components > 3) {
pixel_buf[start + format.w_pos * pos_bytes + i] = 0;
}
}
return ESP_OK;
}
esp_err_t led_strip_rmt_refresh(led_strip_handle_t strip)
{
led_strip_rmt_obj *rmt_strip = strip;
rmt_transmit_config_t tx_conf = {
.loop_count = 0,
};
ESP_RETURN_ON_ERROR(rmt_enable(rmt_strip->rmt_chan), TAG, "enable RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_transmit(rmt_strip->rmt_chan, rmt_strip->strip_encoder, rmt_strip->pixel_buf,
rmt_strip->strip_len * rmt_strip->bytes_per_pixel, &tx_conf), TAG, "transmit pixels by RMT failed");
ESP_RETURN_ON_ERROR(rmt_tx_wait_all_done(rmt_strip->rmt_chan, -1), TAG, "flush RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_disable(rmt_strip->rmt_chan), TAG, "disable RMT channel failed");
return ESP_OK;
}
esp_err_t led_strip_rmt_clear(led_strip_handle_t strip)
{
led_strip_rmt_obj *rmt_strip = strip;
// Write zero to turn off all leds
memset(rmt_strip->pixel_buf, 0, rmt_strip->strip_len * rmt_strip->bytes_per_pixel);
return led_strip_rmt_refresh(strip);
}
esp_err_t led_strip_rmt_del(led_strip_handle_t strip)
{
led_strip_rmt_obj *rmt_strip = strip;
ESP_RETURN_ON_ERROR(rmt_del_channel(rmt_strip->rmt_chan), TAG, "delete RMT channel failed");
ESP_RETURN_ON_ERROR(rmt_del_encoder(rmt_strip->strip_encoder), TAG, "delete strip encoder failed");
free(rmt_strip);
return ESP_OK;
}
esp_err_t led_strip_new_rmt_device(const led_strip_config_t *led_config, const led_strip_rmt_config_t *rmt_config, led_strip_handle_t *ret_strip)
{
led_strip_rmt_obj *rmt_strip = NULL;
esp_err_t ret = ESP_OK;
ESP_GOTO_ON_FALSE(led_config && rmt_config && ret_strip, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
led_color_component_format_t component_fmt = led_config->color_component_format;
// If R/G/B order is not specified, set default GRB order as fallback
if (component_fmt.format_id == 0) {
component_fmt = LED_STRIP_COLOR_COMPONENT_FMT_GRB;
}
if (led_config->led_model == LED_MODEL_WS2816) {
component_fmt.format.bytes_per_color = 2;
}
if (component_fmt.format.bytes_per_color == 0) {
component_fmt.format.bytes_per_color = 1;
}
// check the validation of the color component format
uint8_t mask = 0;
if (component_fmt.format.num_components == 3) {
mask = BIT(component_fmt.format.r_pos) | BIT(component_fmt.format.g_pos) | BIT(component_fmt.format.b_pos);
// Check for invalid values
ESP_RETURN_ON_FALSE(mask == 0x07, ESP_ERR_INVALID_ARG, TAG, "invalid order argument");
} else if (component_fmt.format.num_components == 4) {
mask = BIT(component_fmt.format.r_pos) | BIT(component_fmt.format.g_pos) | BIT(component_fmt.format.b_pos) | BIT(component_fmt.format.w_pos);
// Check for invalid values
ESP_RETURN_ON_FALSE(mask == 0x0F, ESP_ERR_INVALID_ARG, TAG, "invalid order argument");
} else {
ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_ARG, TAG, "invalid number of color components: %d", component_fmt.format.num_components);
}
uint8_t bytes_per_pixel = component_fmt.format.num_components;
if (component_fmt.format.bytes_per_color > 1) {
bytes_per_pixel *= component_fmt.format.bytes_per_color;
}
rmt_strip = calloc(1, sizeof(led_strip_rmt_obj) + led_config->max_leds * bytes_per_pixel);
ESP_GOTO_ON_FALSE(rmt_strip, ESP_ERR_NO_MEM, err, TAG, "no mem for rmt strip");
uint32_t resolution = rmt_config->resolution_hz ? rmt_config->resolution_hz : LED_STRIP_RMT_DEFAULT_RESOLUTION;
// for backward compatibility, if the user does not set the clk_src, use the default value
rmt_clock_source_t clk_src = RMT_CLK_SRC_DEFAULT;
if (rmt_config->clk_src) {
clk_src = rmt_config->clk_src;
}
size_t mem_block_symbols = LED_STRIP_RMT_DEFAULT_MEM_BLOCK_SYMBOLS;
// override the default value if the user sets it
if (rmt_config->mem_block_symbols) {
mem_block_symbols = rmt_config->mem_block_symbols;
}
rmt_tx_channel_config_t rmt_chan_config = {
.clk_src = clk_src,
.gpio_num = led_config->strip_gpio_num,
.mem_block_symbols = mem_block_symbols,
.resolution_hz = resolution,
.trans_queue_depth = LED_STRIP_RMT_DEFAULT_TRANS_QUEUE_SIZE,
.flags.with_dma = rmt_config->flags.with_dma,
.flags.invert_out = led_config->flags.invert_out,
};
ESP_GOTO_ON_ERROR(rmt_new_tx_channel(&rmt_chan_config, &rmt_strip->rmt_chan), err, TAG, "create RMT TX channel failed");
led_strip_encoder_config_t strip_encoder_conf = {
.resolution = resolution,
.led_model = led_config->led_model
};
ESP_GOTO_ON_ERROR(rmt_new_led_strip_encoder(&strip_encoder_conf, &rmt_strip->strip_encoder), err, TAG, "create LED strip encoder failed");
rmt_strip->component_fmt = component_fmt;
rmt_strip->bytes_per_pixel = bytes_per_pixel;
rmt_strip->strip_len = led_config->max_leds;
*ret_strip = rmt_strip;
return ESP_OK;
err:
if (rmt_strip) {
if (rmt_strip->rmt_chan) {
rmt_del_channel(rmt_strip->rmt_chan);
}
if (rmt_strip->strip_encoder) {
rmt_del_encoder(rmt_strip->strip_encoder);
}
free(rmt_strip);
}
return ret;
}

View File

@ -0,0 +1,194 @@
/*
* SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "sdkconfig.h"
#include "esp_idf_version.h"
#include "esp_check.h"
#include "esp_attr.h"
#include "esp32_led_strip_rmt_encoder.h"
static const char *TAG = "led_rmt_encoder";
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 5, 0)
#if CONFIG_RMT_ISR_IRAM_SAFE
#define RMT_ENCODER_FUNC_ATTR IRAM_ATTR
#else
#define RMT_ENCODER_FUNC_ATTR
#endif // CONFIG_RMT_ISR_IRAM_SAFE
#endif // ESP_IDF_VERSION
typedef struct {
rmt_encoder_t base;
rmt_encoder_t *bytes_encoder;
rmt_encoder_t *copy_encoder;
int state;
rmt_symbol_word_t reset_code;
} rmt_led_strip_encoder_t;
RMT_ENCODER_FUNC_ATTR
static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
rmt_encode_state_t session_state = 0;
rmt_encode_state_t state = 0;
size_t encoded_symbols = 0;
switch (led_encoder->state) {
case 0: // send RGB data
encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 1; // switch to next state when current encoding session finished
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
// fall-through
case 1: // send reset code
encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code,
sizeof(led_encoder->reset_code), &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 0; // back to the initial encoding session
state |= RMT_ENCODING_COMPLETE;
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
}
out:
*ret_state = state;
return encoded_symbols;
}
static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_del_encoder(led_encoder->bytes_encoder);
rmt_del_encoder(led_encoder->copy_encoder);
free(led_encoder);
return ESP_OK;
}
RMT_ENCODER_FUNC_ATTR
static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_reset(led_encoder->bytes_encoder);
rmt_encoder_reset(led_encoder->copy_encoder);
led_encoder->state = 0;
return ESP_OK;
}
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
rmt_led_strip_encoder_t *led_encoder = NULL;
ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
ESP_GOTO_ON_FALSE(config->led_model < LED_MODEL_INVALID, ESP_ERR_INVALID_ARG, err, TAG, "invalid led model");
led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t));
ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
led_encoder->base.encode = rmt_encode_led_strip;
led_encoder->base.del = rmt_del_led_strip_encoder;
led_encoder->base.reset = rmt_led_strip_encoder_reset;
rmt_bytes_encoder_config_t bytes_encoder_config;
uint32_t reset_ticks = config->resolution / 1000000 * 280 / 2; // reset code duration defaults to 280us to accommodate WS2812B-V5
if (config->led_model == LED_MODEL_SK6812) {
bytes_encoder_config = (rmt_bytes_encoder_config_t) {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.6 * config->resolution / 1000000, // T1H=0.6us
.level1 = 0,
.duration1 = 0.6 * config->resolution / 1000000, // T1L=0.6us
},
.flags.msb_first = 1 // SK6812 transfer bit order: G7...G0R7...R0B7...B0(W7...W0)
};
} else if (config->led_model == LED_MODEL_WS2812) {
// different led strip might have its own timing requirements, following parameter is for WS2812
bytes_encoder_config = (rmt_bytes_encoder_config_t) {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us
.level1 = 0,
.duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us
},
.flags.msb_first = 1 // WS2812 transfer bit order: G7...G0R7...R0B7...B0
};
} else if (config->led_model == LED_MODEL_WS2811) {
// different led strip might have its own timing requirements, following parameter is for WS2811
bytes_encoder_config = (rmt_bytes_encoder_config_t) {
.bit0 = {
.level0 = 1,
.duration0 = 0.5 * config->resolution / 1000000., // T0H=0.5us
.level1 = 0,
.duration1 = 2.0 * config->resolution / 1000000., // T0L=2.0us
},
.bit1 = {
.level0 = 1,
.duration0 = 1.2 * config->resolution / 1000000., // T1H=1.2us
.level1 = 0,
.duration1 = 1.3 * config->resolution / 1000000., // T1L=1.3us
},
.flags.msb_first = 1
};
reset_ticks = config->resolution / 1000000 * 50 / 2; // divide by 2... signal is sent twice
} else if (config->led_model == LED_MODEL_WS2816) {
// different led strip might have its own timing requirements, following parameter is for WS2816
bytes_encoder_config = (rmt_bytes_encoder_config_t) {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.95 * config->resolution / 1000000, // T0L=0.95us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.75 * config->resolution / 1000000, // T1H=0.75us
.level1 = 0,
.duration1 = 0.5 * config->resolution / 1000000, // T1L=0.5us
},
.flags.msb_first = 1
};
} else {
assert(false);
}
ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
rmt_copy_encoder_config_t copy_encoder_config = {};
ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(&copy_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");
led_encoder->reset_code = (rmt_symbol_word_t) {
.level0 = 0,
.duration0 = reset_ticks,
.level1 = 0,
.duration1 = reset_ticks,
};
*ret_encoder = &led_encoder->base;
return ESP_OK;
err:
if (led_encoder) {
if (led_encoder->bytes_encoder) {
rmt_del_encoder(led_encoder->bytes_encoder);
}
if (led_encoder->copy_encoder) {
rmt_del_encoder(led_encoder->copy_encoder);
}
free(led_encoder);
}
return ret;
}

View File

@ -0,0 +1,23 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver esp32_led_strip_driver;
static Driver* const esp32_led_strip_drivers[] = {
&esp32_led_strip_driver,
nullptr
};
Module esp32_led_strip_module = {
.name = "esp32_led_strip",
.start = nullptr,
.stop = nullptr,
.drivers = esp32_led_strip_drivers,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"

View File

@ -66,6 +66,10 @@ static error_t tdeck_keyboard_read_key(Device* device, KeyboardKeyData* data) {
data->key = internal->last_key;
data->pressed = false;
} else if (read_buffer != 0) {
// Return key is keycode 10, but keyboard passes 13
if (read_buffer == 13) {
read_buffer = 10;
}
data->key = read_buffer;
data->pressed = true;
} else {

View File

@ -146,7 +146,7 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
// esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure
free(runtime->file_data);
delete runtime;
LOG_E(TAG, "Failed to map elf");
LOG_E(TAG, "Failed to map elf. Possibly out of memory.");
return ERROR_RESOURCE;
}

View File

@ -0,0 +1,192 @@
// SPDX-License-Identifier: Apache-2.0
// Symbols resolved for ELF apps ahead of every other module's (see app_esp32_symbol_resolver()).
// Firmware code calling the same functions never gets here.
// Pending signals are delivered here, at the entry and again when a call was interrupted by one,
// since the app holds no lock of the system itself there.
// Allocations are counted per app instance (see app/memory.h).
#include <app/libc.h>
#include <app/memory.h>
#include <app/signal.h>
#include <esp_heap_caps.h>
#include <tactility/module.h>
#include <sys/poll.h>
#include <unistd.h>
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <new>
namespace {
ssize_t app_read(int fd, void* buffer, size_t size) {
app_signal_deliver_pending();
const ssize_t result = read(fd, buffer, size);
if (result < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
return result;
}
ssize_t app_write(int fd, const void* buffer, size_t size) {
app_signal_deliver_pending();
const ssize_t result = write(fd, buffer, size);
if (result < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
return result;
}
int app_poll(struct pollfd* fds, nfds_t nfds, int timeout) {
app_signal_deliver_pending();
const int result = poll(fds, nfds, timeout);
if (result < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
return result;
}
int app_usleep(useconds_t usec) {
app_signal_deliver_pending();
int result;
if (!app_libc_try_usleep(usec, &result)) {
return usleep(usec);
}
app_signal_deliver_pending();
return result;
}
unsigned int app_sleep(unsigned int seconds) {
app_signal_deliver_pending();
unsigned int result;
if (!app_libc_try_sleep(seconds, &result)) {
return sleep(seconds);
}
app_signal_deliver_pending();
return result;
}
int app_kill(pid_t pid, int sig) {
const int result = kill(pid, sig);
// A signal sent to the app itself is delivered before kill() returns
app_signal_deliver_pending();
return result;
}
pid_t app_getpid() {
int result;
return app_libc_try_getpid(&result) ? result : 0;
}
pid_t app_getppid() {
int result;
return app_libc_try_getppid(&result) ? result : 0;
}
void* record_alloc(void* ptr) {
if (ptr != nullptr) {
app_memory_record_alloc(heap_caps_get_allocated_size(ptr));
}
return ptr;
}
void record_free(void* ptr) {
if (ptr != nullptr) {
app_memory_record_free(heap_caps_get_allocated_size(ptr));
}
}
void* app_malloc(size_t size) {
return record_alloc(malloc(size));
}
void* app_calloc(size_t count, size_t size) {
return record_alloc(calloc(count, size));
}
void* app_realloc(void* ptr, size_t size) {
const size_t old_size = (ptr != nullptr) ? heap_caps_get_allocated_size(ptr) : 0;
void* result = realloc(ptr, size);
// A failed realloc() leaves the old block allocated
if (result != nullptr || size == 0) {
if (ptr != nullptr) {
app_memory_record_free(old_size);
}
record_alloc(result);
}
return result;
}
void app_free(void* ptr) {
record_free(ptr);
free(ptr);
}
char* app_strdup(const char* s) {
return static_cast<char*>(record_alloc(strdup(s)));
}
char* app_strndup(const char* s, size_t n) {
return static_cast<char*>(record_alloc(strndup(s, n)));
}
void* app_operator_new(size_t size) {
return record_alloc(::operator new(size));
}
void* app_operator_new_array(size_t size) {
return record_alloc(::operator new[](size));
}
void app_operator_delete(void* ptr) {
record_free(ptr);
::operator delete(ptr);
}
void app_operator_delete_array(void* ptr) {
record_free(ptr);
::operator delete[](ptr);
}
void app_operator_delete_sized(void* ptr, size_t) {
app_operator_delete(ptr);
}
void app_operator_delete_array_sized(void* ptr, size_t) {
app_operator_delete_array(ptr);
}
} // namespace
extern "C" {
extern const ModuleSymbol app_esp32_symbols[] = {
{ "read", reinterpret_cast<void*>(app_read) },
{ "write", reinterpret_cast<void*>(app_write) },
{ "poll", reinterpret_cast<void*>(app_poll) },
{ "usleep", reinterpret_cast<void*>(app_usleep) },
{ "sleep", reinterpret_cast<void*>(app_sleep) },
{ "kill", reinterpret_cast<void*>(app_kill) },
{ "getpid", reinterpret_cast<void*>(app_getpid) },
{ "getppid", reinterpret_cast<void*>(app_getppid) },
{ "malloc", reinterpret_cast<void*>(app_malloc) },
{ "calloc", reinterpret_cast<void*>(app_calloc) },
{ "realloc", reinterpret_cast<void*>(app_realloc) },
{ "free", reinterpret_cast<void*>(app_free) },
{ "strdup", reinterpret_cast<void*>(app_strdup) },
{ "strndup", reinterpret_cast<void*>(app_strndup) },
{ "_Znwj", reinterpret_cast<void*>(app_operator_new) }, // operator new(unsigned int)
{ "_Znaj", reinterpret_cast<void*>(app_operator_new_array) }, // operator new[](unsigned int)
{ "_ZdlPv", reinterpret_cast<void*>(app_operator_delete) }, // operator delete(void*)
{ "_ZdaPv", reinterpret_cast<void*>(app_operator_delete_array) }, // operator delete[](void*)
{ "_ZdlPvj", reinterpret_cast<void*>(app_operator_delete_sized) }, // operator delete(void*, unsigned int)
{ "_ZdaPvj", reinterpret_cast<void*>(app_operator_delete_array_sized) }, // operator delete[](void*, unsigned int)
MODULE_SYMBOL_TERMINATOR
};
}

View File

@ -11,12 +11,16 @@
extern "C" {
extern ServiceManifest loader_service_manifest;
extern const ModuleSymbol app_esp32_symbols[];
extern Module app_esp32_module;
// Overrides elf_loader's default KConfig-based symbol resolver with one that looks up symbols
// across every started kernel module's own symbol table instead.
// across every started kernel module's own symbol table instead. This module's own symbols come
// first, as they replace libc functions for apps (see app_symbols.cpp).
uintptr_t app_esp32_symbol_resolver(const char* symbolName) {
uintptr_t symbol_address;
if (module_resolve_symbol_global(symbolName, &symbol_address)) {
if (module_resolve_symbol(&app_esp32_module, symbolName, &symbol_address) ||
module_resolve_symbol_global(symbolName, &symbol_address)) {
return symbol_address;
}
return 0;
@ -37,7 +41,7 @@ Module app_esp32_module = {
.start = start,
.stop = stop,
.drivers = nullptr,
.symbols = nullptr,
.symbols = app_esp32_symbols,
.internal = nullptr,
};

View File

@ -17,6 +17,7 @@ extern "C" {
enum AppEventType {
APP_EVENT_RESULT, // struct AppResultEventData
APP_EVENT_CLOSE, // no data - terminate now, permanently
APP_EVENT_SIGNAL, // struct AppSignalEventData, see app/signal.h
};
/** Data for APP_EVENT_RESULT. */
@ -29,12 +30,22 @@ struct AppResultEventData {
int32_t result;
};
/** Data for APP_EVENT_SIGNAL. */
struct AppSignalEventData {
/** A POSIX signal number, e.g. SIGHUP */
int sig;
};
struct AppEvent {
enum AppEventType type;
/** Stamped by app_event_emit(); any value passed in by the caller is ignored. */
uint64_t timestamp;
/** Valid only when type == APP_EVENT_RESULT. */
struct AppResultEventData result;
union {
/** Valid only when type == APP_EVENT_RESULT. */
struct AppResultEventData result;
/** Valid only when type == APP_EVENT_SIGNAL. */
struct AppSignalEventData signal;
};
};
/**

View File

@ -48,20 +48,40 @@ bool app_libc_try_tcsetattr(int fd, const struct termios* t, int* out_result);
typedef void (*AppLibcSignalHandler)(int sig);
/**
* Records @a handler for @a sig in the calling app instance. Signals aren't delivered to apps yet,
* so the handler is never called. SIGKILL, SIGSTOP and out-of-range signals fail with EINVAL.
* Records @a handler for @a sig in the calling app instance, called when a signal is delivered (see
* app/signal.h). SIGKILL, SIGSTOP and out-of-range signals fail with EINVAL.
* @param[out] out_previous the previously registered handler, or SIG_ERR on failure
*/
bool app_libc_try_signal(int sig, AppLibcSignalHandler handler, AppLibcSignalHandler* out_previous);
/** Apps can't send signals: kill() from an app fails with ENOSYS rather than reaching the process. */
/**
* Sends @a sig to the app instance @a pid via app_signal_send(), never to the process.
* A @a sig of 0 only checks that @a pid exists. SIGKILL, SIGSTOP and out-of-range signals fail
* with EINVAL. A @a pid that is not a running app instance fails with ESRCH, including pid <= 0,
* since there are no process groups.
*/
bool app_libc_try_kill(int pid, int sig, int* out_result);
/** The calling app instance's AppInstanceId. */
bool app_libc_try_getpid(int* out_result);
/** The AppInstanceId of the calling app instance's parent, or 0 for a top-level launch. */
bool app_libc_try_getppid(int* out_result);
/** Sleeps the calling app instance. Fails with EINTR when a signal is pending or arrives (see app/signal.h). */
bool app_libc_try_usleep(unsigned long usec, int* out_result);
/**
* Sleeps the calling app instance. Returns early when a signal is pending or arrives (see app/signal.h).
* @param[out] out_result the whole seconds not slept, 0 when not interrupted
*/
bool app_libc_try_sleep(unsigned int seconds, unsigned int* out_result);
typedef int (*AppLibcPollFunction)(struct pollfd* fds, nfds_t nfds, int timeout);
/**
* Handles poll() when @a fds contains an app fd. Other fds are checked with @a real_poll.
* Closed app fds report POLLNVAL.
* Closed app fds report POLLNVAL. Fails with EINTR when a signal is pending (see app/signal.h).
*/
bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result);

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Counts an allocation made by the calling app instance's own code. When the app ends with
* allocations left, a warning is logged. Does nothing outside an app's own task.
* @param[in] size the allocated block's size
*/
void app_memory_record_alloc(size_t size);
/**
* Counts a free by the calling app instance's own code. Does nothing outside an app's own task.
* @param[in] size the freed block's size
*/
void app_memory_record_free(size_t size);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "instance.h"
#include <stdbool.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Sends a POSIX signal to an app instance. A signal's pid is its AppInstanceId.
*
* An app with an event subscription (app/event.h) receives the signal as an AppEvent: SIGTERM as
* APP_EVENT_CLOSE, any other signal as APP_EVENT_SIGNAL. Its signal() handlers are not used.
*
* Any other app gets the signal queued, and delivered on its own task at its next libc call (see
* app_signal_deliver_pending()). A blocking read, write, poll or sleep it is waiting in returns -1
* with errno EINTR within about 100 ms.
* A signal the app ignores is discarded right away.
*
* @param[in] sig 1 to APP_LIBC_SIGNAL_COUNT - 1 (app/libc.h)
* @retval ERROR_NONE the signal was sent or discarded
* @retval ERROR_INVALID_ARGUMENT @a sig is out of range
* @retval ERROR_NOT_FOUND no such app instance
* @retval ERROR_RESOURCE an event queue of the app was full
*/
error_t app_signal_send(AppInstanceId app_instance_id, int sig);
/** @return true when the calling app instance has a signal that was not yet delivered */
bool app_signal_is_pending(void);
/**
* Delivers the calling app instance's pending signals: calls signal() handlers, and ends the app
* (as exit(128 + sig)) for a signal without a handler whose default action terminates.
* Does nothing outside an app task.
* @warning Only call where the app holds no lock of the system itself, e.g. at the entry of a libc wrap.
*/
void app_signal_deliver_pending(void);
#ifdef __cplusplus
}
#endif

View File

@ -77,6 +77,9 @@ struct AppInstanceRecord {
/** Handlers registered via signal() (app/libc.h), indexed by signal number. Null means SIG_DFL. */
AppLibcSignalHandler signal_handlers[APP_LIBC_SIGNAL_COUNT] {};
/** Signals sent via app_signal_send() but not yet delivered, one bit per signal number (app/signal.h). */
uint32_t pending_signals = 0;
/** Terminal input flags set via tcsetattr() (app/libc.h). ICRNL is applied when reading stdin. */
tcflag_t termios_iflag = ICRNL;
};

View File

@ -3,8 +3,10 @@
#include <app/dir.h>
#include <app/io.h>
#include <app/manager.h>
#include <app/private/ledger.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <tactility/delay.h>
#include <tactility/paths.h>
@ -44,6 +46,28 @@ AppFdState get_app_fd_state(int fd) {
// Upper bound on wake latency while waiting on more than one fd: app streams can only be awaited one at a time.
constexpr TickType_t POLL_INTERVAL_TICKS = pdMS_TO_TICKS(10);
// Upper bound on how long a sleep or poll takes to notice a signal (see app/signal.h)
constexpr TickType_t SIGNAL_CHECK_INTERVAL_TICKS = pdMS_TO_TICKS(100);
/**
* @param[out] out_remaining the ticks not slept
* @return true when interrupted by a signal
*/
bool sleep_unless_signalled(TickType_t ticks, TickType_t* out_remaining) {
const TickType_t start = get_ticks();
while (true) {
const TickType_t remaining = get_timeout_remaining_ticks(ticks, start);
*out_remaining = remaining;
if (app_signal_is_pending()) {
return true;
}
if (remaining == 0) {
return false;
}
delay_ticks(remaining < SIGNAL_CHECK_INTERVAL_TICKS ? remaining : SIGNAL_CHECK_INTERVAL_TICKS);
}
}
} // namespace
extern "C" {
@ -232,8 +256,79 @@ bool app_libc_try_kill(int pid, int sig, int* out_result) {
if (app_scheduler_current_app_id() == 0) {
return false;
}
errno = ENOSYS;
*out_result = -1;
if (sig < 0 || sig >= APP_LIBC_SIGNAL_COUNT || sig == SIGKILL || sig == SIGSTOP) {
errno = EINVAL;
*out_result = -1;
return true;
}
const bool exists = pid > 0 && app_manager_get_state(static_cast<AppInstanceId>(pid)) != APP_INSTANCE_STATE_STOPPED;
if (!exists) {
errno = ESRCH;
*out_result = -1;
return true;
}
const error_t result = (sig == 0) ? ERROR_NONE : app_signal_send(static_cast<AppInstanceId>(pid), sig);
switch (result) {
case ERROR_NONE:
*out_result = 0;
break;
case ERROR_NOT_FOUND:
errno = ESRCH;
*out_result = -1;
break;
default:
errno = EAGAIN;
*out_result = -1;
break;
}
return true;
}
bool app_libc_try_getpid(int* out_result) {
const AppInstanceId app_instance_id = app_scheduler_current_app_id();
if (app_instance_id == 0) {
return false;
}
*out_result = static_cast<int>(app_instance_id);
return true;
}
bool app_libc_try_getppid(int* out_result) {
const AppInstanceId app_instance_id = app_scheduler_current_app_id();
if (app_instance_id == 0) {
return false;
}
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
const uint32_t parent_id = (iterator != ledger.instances.end()) ? iterator->second.parent_id : 0;
mutex_unlock(&ledger.mutex);
*out_result = static_cast<int>(parent_id);
return true;
}
bool app_libc_try_usleep(unsigned long usec, int* out_result) {
if (app_scheduler_current_app_id() == 0) {
return false;
}
const auto ticks = static_cast<TickType_t>((static_cast<uint64_t>(usec) * configTICK_RATE_HZ + 999999) / 1000000);
TickType_t remaining;
if (sleep_unless_signalled(ticks, &remaining)) {
errno = EINTR;
*out_result = -1;
} else {
*out_result = 0;
}
return true;
}
bool app_libc_try_sleep(unsigned int seconds, unsigned int* out_result) {
if (app_scheduler_current_app_id() == 0) {
return false;
}
TickType_t remaining;
sleep_unless_signalled(static_cast<TickType_t>(static_cast<uint64_t>(seconds) * configTICK_RATE_HZ), &remaining);
*out_result = static_cast<unsigned int>(remaining / configTICK_RATE_HZ);
return true;
}
@ -264,6 +359,11 @@ bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPoll
const TickType_t start = get_ticks();
const TickType_t timeout_ticks = (timeout < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout);
while (true) {
if (app_signal_is_pending()) {
errno = EINTR;
*out_result = -1;
return true;
}
int ready = 0;
for (nfds_t i = 0; i < nfds; i++) {
fds[i].revents = 0;
@ -302,10 +402,11 @@ bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPoll
remaining = POLL_INTERVAL_TICKS;
}
const struct pollfd& first = fds[first_app_index];
const TickType_t wait_ticks = remaining < SIGNAL_CHECK_INTERVAL_TICKS ? remaining : SIGNAL_CHECK_INTERVAL_TICKS;
if (first.events & POLLIN) {
app_io_await(first.fd, APP_FILE_WAIT_READABLE, remaining);
app_io_await(first.fd, APP_FILE_WAIT_READABLE, wait_ticks);
} else if (first.events & POLLOUT) {
app_io_await(first.fd, APP_FILE_WAIT_WRITABLE, remaining);
app_io_await(first.fd, APP_FILE_WAIT_WRITABLE, wait_ticks);
} else {
delay_ticks(remaining < POLL_INTERVAL_TICKS ? remaining : POLL_INTERVAL_TICKS);
}

View File

@ -7,9 +7,11 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/memory.h>
#include <app/package_manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/start.h>
#include <app/stream.h>
@ -76,6 +78,9 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid),
// app/memory
DEFINE_MODULE_SYMBOL(app_memory_record_alloc),
DEFINE_MODULE_SYMBOL(app_memory_record_free),
// app/package_manifest
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
DEFINE_MODULE_SYMBOL(app_package_manifest_is_compatible),
@ -86,6 +91,8 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// app/signal
DEFINE_MODULE_SYMBOL(app_signal_send),
// app/start
DEFINE_MODULE_SYMBOL(app_start_context_for_manifest),
DEFINE_MODULE_SYMBOL(app_start_context_for_location),

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/instance.h>
#include <app/loader.h>
#include <app/memory.h>
#include <app/private/arguments.h>
#include <app/private/event.h>
#include <app/private/fd_table.h>
@ -8,6 +9,7 @@
#include <app/private/scheduler.h>
#include <app/private/stream_internal.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/stream.h>
#include <service/instance.h>
@ -20,6 +22,7 @@
#include <tactility/time.h>
#include <atomic>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <new>
@ -82,6 +85,9 @@ struct TaskContext {
AppCompletionSignal* completion;
StackType_t* stackBuffer;
StaticTask_t* taskTcb;
// Live allocations of the app's own code, see app/memory.h. Only updated by the app's own task.
int32_t allocCount;
int64_t allocBytes;
};
struct ReaperContext {
@ -306,6 +312,10 @@ void finish_app_task(TaskContext* ctx, int32_t result, bool exiting) {
ctx->loader->unload(ctx->runtime);
#endif
if (ctx->allocCount > 0 || ctx->allocBytes > 0) {
LOG_W(TAG, "[instance %lu] %ld allocations (%lld bytes) not freed", ctx->app_instance_id, static_cast<long>(ctx->allocCount), static_cast<long long>(ctx->allocBytes));
}
deliver_result_to_parent_if_any(ctx->app_instance_id, result);
// The terminal marker for every exit path: an app instance is Stopped exactly when its
@ -454,6 +464,8 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, const AppStartContext
.completion = completion,
.stackBuffer = stack_buffer,
.taskTcb = task_tcb,
.allocCount = 0,
.allocBytes = 0,
};
if (context == nullptr) {
@ -515,8 +527,8 @@ error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeou
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
if (completion != nullptr) {
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
app_event_emit(app_instance_id, &event);
// An app with an event subscription receives this as APP_EVENT_CLOSE, see app/signal.h
app_signal_send(app_instance_id, SIGTERM);
// Marked as soon as the app has been told to close, not once its task has actually
// unwound - so app_manager_get_state()/app_manager_get_topmost_instance_id() reflect the
@ -549,6 +561,29 @@ AppInstanceId app_scheduler_current_app_id(void) {
return get_current_app_id();
}
void app_memory_record_alloc(size_t size) {
// Checked first: thread_local can't be read before the scheduler starts on ESP32
if (get_current_app_id() == 0) {
return;
}
TaskContext* ctx = current_task_context;
if (ctx != nullptr) {
ctx->allocCount++;
ctx->allocBytes += static_cast<int64_t>(size);
}
}
void app_memory_record_free(size_t size) {
if (get_current_app_id() == 0) {
return;
}
TaskContext* ctx = current_task_context;
if (ctx != nullptr) {
ctx->allocCount--;
ctx->allocBytes -= static_cast<int64_t>(size);
}
}
void app_scheduler_exit_current(int32_t status) {
// Checked first: thread_local can't be read before the scheduler starts on ESP32
if (get_current_app_id() == 0) {

View File

@ -0,0 +1,125 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/signal.h>
#include <app/libc.h>
#include <app/manager.h>
#include <app/private/event.h>
#include <app/private/ledger.h>
#include <app/scheduler.h>
#include <signal.h>
namespace {
// Signals whose default action is to do nothing, so they never interrupt or end an app.
// There is no job control, so the stop signals are discarded like for an orphaned process group,
// and SIGCONT never finds a stopped app.
bool is_ignored_by_default(int sig) {
switch (sig) {
case SIGCHLD:
case SIGWINCH:
case SIGURG:
case SIGCONT:
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
return true;
default:
return false;
}
}
bool is_ignored(AppLibcSignalHandler handler, int sig) {
if (handler == SIG_IGN) {
return true;
}
return handler == SIG_DFL && is_ignored_by_default(sig);
}
} // namespace
extern "C" {
error_t app_signal_send(AppInstanceId app_instance_id, int sig) {
if (sig <= 0 || sig >= APP_LIBC_SIGNAL_COUNT) {
return ERROR_INVALID_ARGUMENT;
}
if (app_manager_get_state(app_instance_id) == APP_INSTANCE_STATE_STOPPED) {
return ERROR_NOT_FOUND;
}
AppEvent event {};
if (sig == SIGTERM) {
event.type = APP_EVENT_CLOSE;
} else {
event.type = APP_EVENT_SIGNAL;
event.signal.sig = sig;
}
const error_t emit_result = app_event_emit(app_instance_id, &event);
if (emit_result != ERROR_NOT_FOUND) {
return emit_result;
}
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator == ledger.instances.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
AppInstanceRecord& record = iterator->second;
if (is_ignored(record.signal_handlers[sig], sig)) {
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
record.pending_signals |= 1u << sig;
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
bool app_signal_is_pending(void) {
const AppInstanceId app_instance_id = app_scheduler_current_app_id();
if (app_instance_id == 0) {
return false;
}
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
const bool pending = iterator != ledger.instances.end() && iterator->second.pending_signals != 0;
mutex_unlock(&ledger.mutex);
return pending;
}
void app_signal_deliver_pending(void) {
const AppInstanceId app_instance_id = app_scheduler_current_app_id();
if (app_instance_id == 0) {
return;
}
auto& ledger = app_ledger();
while (true) {
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator == ledger.instances.end() || iterator->second.pending_signals == 0) {
mutex_unlock(&ledger.mutex);
return;
}
AppInstanceRecord& record = iterator->second;
const int sig = __builtin_ctz(record.pending_signals);
record.pending_signals &= ~(1u << sig);
// Read at delivery rather than when sent, as the app may have changed it since
const AppLibcSignalHandler handler = record.signal_handlers[sig];
mutex_unlock(&ledger.mutex);
if (is_ignored(handler, sig)) {
continue;
}
if (handler == SIG_DFL) {
app_scheduler_exit_current(128 + sig);
return;
}
handler(sig);
}
}
} // extern "C"

View File

@ -4,12 +4,18 @@
#include <app/private/fd_table.h>
#include <app/private/ledger.h>
#include <app/private/stream_internal.h>
#include <app/signal.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <cerrno>
namespace {
// Upper bound on how long a blocked read or write takes to notice a signal (see app/signal.h)
constexpr TickType_t SIGNAL_CHECK_INTERVAL_TICKS = pdMS_TO_TICKS(100);
// Caller must hold stream->mutex.
bool is_readable_locked(AppStream* stream) {
return stream->buffer.count > 0 || stream->closed;
@ -44,11 +50,28 @@ private:
AppStream* stream_;
};
/**
* Waits until @a stream is ready, or a signal is pending for the calling app (see app/signal.h).
* @return false when the wait failed, with errno EINTR when interrupted by a signal
*/
bool await_unless_signalled(AppStream* stream, AppFileWait wait) {
while (true) {
if (app_signal_is_pending()) {
errno = EINTR;
return false;
}
const error_t result = app_stream_await(stream, wait, SIGNAL_CHECK_INTERVAL_TICKS);
if (result != ERROR_TIMEOUT) {
return result == ERROR_NONE;
}
}
}
ssize_t stream_file_read(void* object, void* buffer, size_t size) {
auto* stream = static_cast<AppStream*>(object);
StreamOperationGuard guard(stream);
while (true) {
if (app_stream_await(stream, APP_FILE_WAIT_READABLE, portMAX_DELAY) != ERROR_NONE) {
if (!await_unless_signalled(stream, APP_FILE_WAIT_READABLE)) {
return -1;
}
size_t read = app_stream_read(stream, buffer, size);
@ -69,7 +92,7 @@ ssize_t stream_file_read(void* object, void* buffer, size_t size) {
ssize_t stream_file_write(void* object, const void* buffer, size_t size) {
auto* stream = static_cast<AppStream*>(object);
StreamOperationGuard guard(stream);
if (app_stream_await(stream, APP_FILE_WAIT_WRITABLE, portMAX_DELAY) != ERROR_NONE) {
if (!await_unless_signalled(stream, APP_FILE_WAIT_WRITABLE)) {
return -1;
}
// app_stream_write() itself refuses to copy anything once closed (checked under the same

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cstdint>
#ifndef __APPLE__
/**
* Sets the address range of the app image running on the calling thread. Allocations made by code
* in [start, end) are counted for the current app instance (see app/memory.h).
*/
void app_posix_set_current_image(uintptr_t start, uintptr_t end);
#else
// Allocations aren't counted on Apple platforms
inline void app_posix_set_current_image(uintptr_t, uintptr_t) {}
#endif

View File

@ -11,6 +11,7 @@
#include <signal.h>
#include <termios.h>
#include <unistd.h>
// Implemented in stdio_wrap.cpp, installed under the real names by stdio_wrap_elf.cpp or stdio_wrap_apple.cpp.
@ -27,6 +28,10 @@ int __wrap_tcgetattr(int fd, struct termios* p);
int __wrap_tcsetattr(int fd, int optional_actions, const struct termios* p);
AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler);
int __wrap_kill(pid_t pid, int sig);
pid_t __wrap_getpid();
pid_t __wrap_getppid();
int __wrap_usleep(useconds_t usec);
unsigned int __wrap_sleep(unsigned int seconds);
[[noreturn]] void __wrap_exit(int status);
int __wrap_vprintf(const char* format, va_list args);

View File

@ -1,4 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <app_posix/malloc_wrap.h>
#include <app/elf_check.h>
#include <app/loader.h>
#include <app/location.h>
@ -11,6 +13,10 @@
#include <dlfcn.h>
#include <sys/stat.h>
#ifndef __APPLE__
#include <link.h>
#endif
#include <new>
#include <string>
@ -21,8 +27,52 @@ namespace {
/** load()-allocated state, passed back through run()/unload(). */
struct PosixAppRuntime {
void* handle = nullptr;
// Address range of the loaded image, see app_posix_set_current_image()
uintptr_t image_start = 0;
uintptr_t image_end = 0;
};
#ifndef __APPLE__
struct ImageRangeSearch {
ElfW(Addr) base;
uintptr_t start;
uintptr_t end;
};
int find_image_range(struct dl_phdr_info* info, size_t, void* context) {
auto* search = static_cast<ImageRangeSearch*>(context);
if (info->dlpi_addr != search->base) {
return 0;
}
for (ElfW(Half) i = 0; i < info->dlpi_phnum; i++) {
const ElfW(Phdr)& header = info->dlpi_phdr[i];
if (header.p_type != PT_LOAD) {
continue;
}
const uintptr_t start = info->dlpi_addr + header.p_vaddr;
const uintptr_t end = start + header.p_memsz;
if (search->start == 0 || start < search->start) {
search->start = start;
}
if (end > search->end) {
search->end = end;
}
}
return 1;
}
void resolve_image_range(PosixAppRuntime* runtime) {
struct link_map* map = nullptr;
if (dlinfo(runtime->handle, RTLD_DI_LINKMAP, &map) != 0 || map == nullptr) {
return;
}
ImageRangeSearch search { .base = map->l_addr, .start = 0, .end = 0 };
dl_iterate_phdr(find_image_range, &search);
runtime->image_start = search.start;
runtime->image_end = search.end;
}
#endif
bool is_regular_file(const std::string& path) {
struct stat path_stat {};
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
@ -109,6 +159,10 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
return ERROR_OUT_OF_MEMORY;
}
#ifndef __APPLE__
resolve_image_range(runtime);
#endif
*out_runtime = runtime;
return ERROR_NONE;
}
@ -126,7 +180,10 @@ int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc,
}
auto* main_fn = reinterpret_cast<AppMainFn>(symbol);
return main_fn(argc, argv);
app_posix_set_current_image(runtime->image_start, runtime->image_end);
const int32_t result = main_fn(argc, argv);
app_posix_set_current_image(0, 0);
return result;
}
void api_unload(AppRuntime runtime_ptr) {

View File

@ -0,0 +1,176 @@
// SPDX-License-Identifier: Apache-2.0
#ifndef __APPLE__
// Counts the allocations made by a dlopen()ed app's own code (see app/memory.h). A .so resolves
// malloc through the dynamic linker, so these replace the process-wide functions and forward to
// glibc's own allocator. A call is counted only when its caller lies inside the image of the app
// running on the calling thread.
#include <app_posix/malloc_wrap.h>
#include <app/memory.h>
#include <malloc.h>
#include <cstdlib>
#include <cstring>
#include <new>
extern "C" {
void* __libc_malloc(size_t size);
void* __libc_calloc(size_t count, size_t size);
void* __libc_realloc(void* ptr, size_t size);
void __libc_free(void* ptr);
}
namespace {
// initial-exec, since the default TLS model of position independent code can call malloc itself
__attribute__((tls_model("initial-exec"))) thread_local uintptr_t image_start = 0;
__attribute__((tls_model("initial-exec"))) thread_local uintptr_t image_end = 0;
inline bool is_app_caller(void* caller) {
const auto address = reinterpret_cast<uintptr_t>(caller);
return address >= image_start && address < image_end;
}
inline void* record_alloc(void* ptr, void* caller) {
if (ptr != nullptr && is_app_caller(caller)) {
app_memory_record_alloc(malloc_usable_size(ptr));
}
return ptr;
}
inline void record_free(void* ptr, void* caller) {
if (ptr != nullptr && is_app_caller(caller)) {
app_memory_record_free(malloc_usable_size(ptr));
}
}
void* new_or_throw(size_t size) {
while (true) {
void* ptr = __libc_malloc(size);
if (ptr != nullptr) {
return ptr;
}
std::new_handler handler = std::get_new_handler();
if (handler == nullptr) {
// Required by operator new's contract, so callers in libraries behave as before
#if __cpp_exceptions
throw std::bad_alloc();
#else
abort();
#endif
}
handler();
}
}
} // namespace
void app_posix_set_current_image(uintptr_t start, uintptr_t end) {
image_start = start;
image_end = end;
}
extern "C" {
void* malloc(size_t size) {
return record_alloc(__libc_malloc(size), __builtin_return_address(0));
}
void* calloc(size_t count, size_t size) {
return record_alloc(__libc_calloc(count, size), __builtin_return_address(0));
}
void* realloc(void* ptr, size_t size) {
void* caller = __builtin_return_address(0);
if (!is_app_caller(caller)) {
return __libc_realloc(ptr, size);
}
const size_t old_size = (ptr != nullptr) ? malloc_usable_size(ptr) : 0;
void* result = __libc_realloc(ptr, size);
// A failed realloc() leaves the old block allocated
if (result != nullptr || size == 0) {
if (ptr != nullptr) {
app_memory_record_free(old_size);
}
if (result != nullptr) {
app_memory_record_alloc(malloc_usable_size(result));
}
}
return result;
}
void free(void* ptr) {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
char* strdup(const char* s) {
const size_t length = strlen(s) + 1;
auto* copy = static_cast<char*>(__libc_malloc(length));
if (copy != nullptr) {
memcpy(copy, s, length);
}
return static_cast<char*>(record_alloc(copy, __builtin_return_address(0)));
}
char* strndup(const char* s, size_t n) {
const size_t length = strnlen(s, n);
auto* copy = static_cast<char*>(__libc_malloc(length + 1));
if (copy != nullptr) {
memcpy(copy, s, length);
copy[length] = '\0';
}
return static_cast<char*>(record_alloc(copy, __builtin_return_address(0)));
}
} // extern "C"
void* operator new(size_t size) {
return record_alloc(new_or_throw(size), __builtin_return_address(0));
}
void* operator new[](size_t size) {
return record_alloc(new_or_throw(size), __builtin_return_address(0));
}
void* operator new(size_t size, const std::nothrow_t&) noexcept {
return record_alloc(__libc_malloc(size), __builtin_return_address(0));
}
void* operator new[](size_t size, const std::nothrow_t&) noexcept {
return record_alloc(__libc_malloc(size), __builtin_return_address(0));
}
void operator delete(void* ptr) noexcept {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
void operator delete[](void* ptr) noexcept {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
void operator delete(void* ptr, size_t) noexcept {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
void operator delete[](void* ptr, size_t) noexcept {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
void operator delete(void* ptr, const std::nothrow_t&) noexcept {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
void operator delete[](void* ptr, const std::nothrow_t&) noexcept {
record_free(ptr, __builtin_return_address(0));
__libc_free(ptr);
}
#endif

View File

@ -10,6 +10,7 @@
#include <app/io.h>
#include <app/libc.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <cerrno>
#include <cstring>
@ -83,18 +84,51 @@ int __real_kill(pid_t pid, int sig) {
return real(pid, sig);
}
pid_t __real_getpid() {
static auto real = reinterpret_cast<pid_t (*)()>(dlsym(RTLD_NEXT, "getpid"));
return real();
}
pid_t __real_getppid() {
static auto real = reinterpret_cast<pid_t (*)()>(dlsym(RTLD_NEXT, "getppid"));
return real();
}
int __real_usleep(useconds_t usec) {
static auto real = reinterpret_cast<int (*)(useconds_t)>(dlsym(RTLD_NEXT, "usleep"));
return real(usec);
}
unsigned int __real_sleep(unsigned int seconds) {
static auto real = reinterpret_cast<unsigned int (*)(unsigned int)>(dlsym(RTLD_NEXT, "sleep"));
return real(seconds);
}
[[noreturn]] void __real_exit(int status) {
static auto real = reinterpret_cast<void (*)(int)>(dlsym(RTLD_NEXT, "exit"));
real(status);
__builtin_unreachable();
}
// Pending signals are delivered at the entry of these wraps, and again when a call was interrupted
// by one, since the app holds no lock of the system itself there.
ssize_t __wrap_read(int fd, void* buffer, size_t size) {
return app_io_read(fd, buffer, size);
app_signal_deliver_pending();
const ssize_t result = app_io_read(fd, buffer, size);
if (result < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
return result;
}
ssize_t __wrap_write(int fd, const void* buffer, size_t size) {
return app_io_write(fd, buffer, size);
app_signal_deliver_pending();
const ssize_t result = app_io_write(fd, buffer, size);
if (result < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
return result;
}
int __wrap_close(int fd) {
@ -139,8 +173,12 @@ int __wrap_fstat(int fd, struct stat* st) {
}
int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout) {
app_signal_deliver_pending();
int result;
if (app_libc_try_poll(fds, nfds, timeout, __real_poll, &result)) {
if (result < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
return result;
}
return __real_poll(fds, nfds, timeout);
@ -175,11 +213,49 @@ AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler) {
int __wrap_kill(pid_t pid, int sig) {
int result;
if (app_libc_try_kill(pid, sig, &result)) {
// A signal sent to the app itself is delivered before kill() returns
app_signal_deliver_pending();
return result;
}
return __real_kill(pid, sig);
}
pid_t __wrap_getpid() {
int result;
if (app_libc_try_getpid(&result)) {
return result;
}
return __real_getpid();
}
pid_t __wrap_getppid() {
int result;
if (app_libc_try_getppid(&result)) {
return result;
}
return __real_getppid();
}
int __wrap_usleep(useconds_t usec) {
app_signal_deliver_pending();
int result;
if (app_libc_try_usleep(usec, &result)) {
app_signal_deliver_pending();
return result;
}
return __real_usleep(usec);
}
unsigned int __wrap_sleep(unsigned int seconds) {
app_signal_deliver_pending();
unsigned int result;
if (app_libc_try_sleep(seconds, &result)) {
app_signal_deliver_pending();
return result;
}
return __real_sleep(seconds);
}
// Called by an app, the real exit() would end the whole simulator
void __wrap_exit(int status) {
app_scheduler_exit_current(status);
@ -238,11 +314,15 @@ namespace {
/** @return the number of bytes written, less than `size` if the fd stopped accepting data */
size_t writeAllTo(int fd, const void* data, size_t size) {
app_signal_deliver_pending();
const auto* bytes = static_cast<const char*>(data);
size_t remaining = size;
while (remaining > 0) {
ssize_t written = app_io_write(fd, bytes, remaining);
if (written <= 0) {
if (written < 0 && errno == EINTR) {
app_signal_deliver_pending();
}
break;
}
bytes += written;
@ -274,7 +354,7 @@ int formatTo(int fd, const char* format, va_list args) {
}
int readOneFromStdin(char& out) {
return static_cast<int>(app_io_read(STDIN_FILENO, &out, 1));
return static_cast<int>(__wrap_read(STDIN_FILENO, &out, 1));
}
// The process' own streams, captured before anything can reassign stdin/stdout/stderr. A caller

View File

@ -27,6 +27,10 @@ TT_DYLD_INTERPOSE(__wrap_tcsetattr, tcsetattr)
TT_DYLD_INTERPOSE(__wrap_exit, exit)
TT_DYLD_INTERPOSE(__wrap_signal, signal)
TT_DYLD_INTERPOSE(__wrap_kill, kill)
TT_DYLD_INTERPOSE(__wrap_getpid, getpid)
TT_DYLD_INTERPOSE(__wrap_getppid, getppid)
TT_DYLD_INTERPOSE(__wrap_usleep, usleep)
TT_DYLD_INTERPOSE(__wrap_sleep, sleep)
TT_DYLD_INTERPOSE(__wrap_vprintf, vprintf)
TT_DYLD_INTERPOSE(__wrap_printf, printf)

View File

@ -59,6 +59,22 @@ int kill(pid_t pid, int sig) {
return __wrap_kill(pid, sig);
}
pid_t getpid() {
return __wrap_getpid();
}
pid_t getppid() {
return __wrap_getppid();
}
int usleep(useconds_t usec) {
return __wrap_usleep(usec);
}
unsigned int sleep(unsigned int seconds) {
return __wrap_sleep(seconds);
}
void exit(int status) {
__wrap_exit(status);
}

View File

@ -8,6 +8,7 @@
#include <app/loader.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/start.h>
#include <app/stream.h>
@ -175,14 +176,14 @@ void test_signal_handler(int) {
std::atomic<bool> g_signal_first_default { false };
std::atomic<bool> g_signal_returns_previous { false };
std::atomic<bool> g_signal_sigkill_rejected { false };
std::atomic<bool> g_kill_enosys { false };
std::atomic<bool> g_kill_sigstop_rejected { false };
int32_t signal_app_main(int, char*[]) {
g_signal_first_default.store(signal(SIGWINCH, test_signal_handler) == SIG_DFL, std::memory_order_release);
g_signal_returns_previous.store(signal(SIGWINCH, SIG_IGN) == test_signal_handler, std::memory_order_release);
g_signal_sigkill_rejected.store(signal(SIGKILL, test_signal_handler) == SIG_ERR && errno == EINVAL, std::memory_order_release);
// Would stop the whole test process if it reached the real kill()
g_kill_enosys.store(kill(0, SIGSTOP) == -1 && errno == ENOSYS, std::memory_order_release);
g_kill_sigstop_rejected.store(kill(getpid(), SIGSTOP) == -1 && errno == EINVAL, std::memory_order_release);
return 0;
}
@ -249,7 +250,7 @@ TEST_CASE("signal() in an app records handlers per app instance, and kill() neve
g_signal_first_default.store(false, std::memory_order_relaxed);
g_signal_returns_previous.store(false, std::memory_order_relaxed);
g_signal_sigkill_rejected.store(false, std::memory_order_relaxed);
g_kill_enosys.store(false, std::memory_order_relaxed);
g_kill_sigstop_rejected.store(false, std::memory_order_relaxed);
AppManifest manifest { "test.libc.signal", "Signal", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(signal_app_main) } };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
@ -263,7 +264,7 @@ TEST_CASE("signal() in an app records handlers per app instance, and kill() neve
CHECK(g_signal_first_default.load(std::memory_order_acquire));
CHECK(g_signal_returns_previous.load(std::memory_order_acquire));
CHECK(g_signal_sigkill_rejected.load(std::memory_order_acquire));
CHECK(g_kill_enosys.load(std::memory_order_acquire));
CHECK(g_kill_sigstop_rejected.load(std::memory_order_acquire));
app_manager_remove("test.libc.signal");
}
@ -413,3 +414,281 @@ TEST_CASE("app_execute_for_result_with_streams pipes a child's plain printf() ca
app_manager_stop(parent_id);
app_manager_remove("test.app.execute.printf_parent");
}
namespace {
std::atomic<int> g_signal_handled { 0 };
std::atomic<bool> g_signal_app_blocked { false };
std::atomic<int> g_signal_call_result { -2 };
std::atomic<int> g_signal_call_errno { 0 };
std::atomic<bool> g_signal_app_continued { false };
void record_signal_handler(int sig) {
g_signal_handled.store(sig, std::memory_order_release);
}
void reset_signal_flags() {
g_signal_handled.store(0, std::memory_order_relaxed);
g_signal_app_blocked.store(false, std::memory_order_relaxed);
g_signal_call_result.store(-2, std::memory_order_relaxed);
g_signal_call_errno.store(0, std::memory_order_relaxed);
g_signal_app_continued.store(false, std::memory_order_relaxed);
}
int32_t handled_signal_app_main(int, char*[]) {
signal(SIGUSR1, record_signal_handler);
g_signal_app_blocked.store(true, std::memory_order_release);
char c;
const ssize_t result = read(STDIN_FILENO, &c, 1);
g_signal_call_errno.store(errno, std::memory_order_release);
g_signal_call_result.store(static_cast<int>(result), std::memory_order_release);
return 0;
}
int32_t default_signal_app_main(int, char*[]) {
g_signal_app_blocked.store(true, std::memory_order_release);
char c;
read(STDIN_FILENO, &c, 1);
g_signal_app_continued.store(true, std::memory_order_release);
return 0;
}
int32_t ignored_signal_app_main(int, char*[]) {
signal(SIGHUP, SIG_IGN);
g_signal_app_blocked.store(true, std::memory_order_release);
g_signal_call_result.store(usleep(300 * 1000), std::memory_order_release);
g_signal_app_continued.store(true, std::memory_order_release);
return 0;
}
int32_t default_sleep_app_main(int, char*[]) {
g_signal_app_blocked.store(true, std::memory_order_release);
g_signal_call_result.store(usleep(300 * 1000), std::memory_order_release);
g_signal_app_continued.store(true, std::memory_order_release);
return 0;
}
int32_t interrupted_sleep_app_main(int, char*[]) {
signal(SIGUSR1, record_signal_handler);
g_signal_app_blocked.store(true, std::memory_order_release);
const int result = usleep(10 * 1000 * 1000);
g_signal_call_errno.store(errno, std::memory_order_release);
g_signal_call_result.store(result, std::memory_order_release);
return 0;
}
int32_t interrupted_poll_app_main(int, char*[]) {
signal(SIGUSR1, record_signal_handler);
g_signal_app_blocked.store(true, std::memory_order_release);
struct pollfd fd { STDIN_FILENO, POLLIN, 0 };
const int result = poll(&fd, 1, -1);
g_signal_call_errno.store(errno, std::memory_order_release);
g_signal_call_result.store(result, std::memory_order_release);
return 0;
}
std::atomic<bool> g_kill_probe_ok { false };
std::atomic<bool> g_kill_missing_esrch { false };
std::atomic<bool> g_kill_group_esrch { false };
std::atomic<bool> g_getppid_top_level { false };
std::atomic<bool> g_kill_self_handled_before_return { false };
int32_t kill_app_main(int, char*[]) {
signal(SIGUSR2, record_signal_handler);
g_kill_probe_ok.store(kill(getpid(), 0) == 0, std::memory_order_release);
g_kill_missing_esrch.store(kill(0x7FFFFFF0, SIGTERM) == -1 && errno == ESRCH, std::memory_order_release);
g_kill_group_esrch.store(kill(0, SIGTERM) == -1 && errno == ESRCH, std::memory_order_release);
g_getppid_top_level.store(getppid() == 0, std::memory_order_release);
kill(getpid(), SIGUSR2);
g_kill_self_handled_before_return.store(g_signal_handled.load(std::memory_order_acquire) == SIGUSR2, std::memory_order_release);
return 0;
}
std::atomic<int> g_event_signal { 0 };
std::atomic<bool> g_event_closed { false };
int32_t evented_signal_app_main(int, char*[]) {
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
app_event_subscribe(&sub, &event_group);
g_signal_app_blocked.store(true, std::memory_order_release);
bool closed = false;
while (!closed) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_SIGNAL) {
g_event_signal.store(event.signal.sig, std::memory_order_release);
} else if (event.type == APP_EVENT_CLOSE) {
closed = true;
}
}
}
g_event_closed.store(true, std::memory_order_release);
app_event_unsubscribe(&sub);
task_event_group_destruct(&event_group);
return 0;
}
/** Runs @a app_main with a stdin that is never written to, so a read() of it blocks. */
struct BlockedStdinApp {
TaskEventGroup event_group {};
uint8_t storage[16] {};
AppStream stdin_stream {};
AppInstanceId id = 0;
const char* manifest_id;
AppManifest manifest { "", "Signal", APP_CATEGORY_USER, {} };
BlockedStdinApp(const char* manifestId, int32_t (*app_main)(int, char*[])) : manifest_id(manifestId) {
ensure_memory_loader_registered();
reset_signal_flags();
strncpy(manifest.id, manifestId, sizeof(manifest.id) - 1);
manifest.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(app_main) };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
task_event_group_construct(&event_group);
AppStreamBinding binding { STDIN_FILENO, &stdin_stream, storage, sizeof(storage), &event_group, {}, -1 };
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id(manifest_id, &context), ERROR_NONE);
app_start_context_set_streams(&context, &binding, 1);
REQUIRE_EQ(app_start_with_context(&context, &id), ERROR_NONE);
REQUIRE(wait_for_flag(g_signal_app_blocked, 1000));
// Lets the app reach the blocking call after raising its flag
delay_millis(50);
}
~BlockedStdinApp() {
app_manager_stop(id);
app_stream_unsubscribe(&stdin_stream);
task_event_group_destruct(&event_group);
app_manager_remove(manifest_id);
}
};
} // namespace
TEST_CASE("A signal with a handler interrupts a blocked read() with EINTR and calls the handler") {
BlockedStdinApp app("test.libc.signal_handled", handled_signal_app_main);
CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_signal_handled.load(std::memory_order_acquire), SIGUSR1);
CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), -1);
CHECK_EQ(g_signal_call_errno.load(std::memory_order_acquire), EINTR);
}
TEST_CASE("A signal without a handler ends an app blocked in read()") {
BlockedStdinApp app("test.libc.signal_default", default_signal_app_main);
CHECK_EQ(app_signal_send(app.id, SIGHUP), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_FALSE(g_signal_app_continued.load(std::memory_order_acquire));
}
TEST_CASE("app_manager_stop() ends an app without an event subscription via SIGTERM") {
BlockedStdinApp app("test.libc.signal_stop", default_signal_app_main);
CHECK_EQ(app_manager_stop(app.id), ERROR_NONE);
CHECK_EQ(app_manager_get_state(app.id), APP_INSTANCE_STATE_STOPPED);
CHECK_FALSE(g_signal_app_continued.load(std::memory_order_acquire));
}
TEST_CASE("An ignored signal does not interrupt the app") {
BlockedStdinApp app("test.libc.signal_ignored", ignored_signal_app_main);
CHECK_EQ(app_signal_send(app.id, SIGHUP), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK(g_signal_app_continued.load(std::memory_order_acquire));
CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), 0);
}
TEST_CASE("SIGCONT and the stop signals without a handler neither interrupt nor end the app") {
BlockedStdinApp app("test.libc.signal_job_control", default_sleep_app_main);
CHECK_EQ(app_signal_send(app.id, SIGCONT), ERROR_NONE);
CHECK_EQ(app_signal_send(app.id, SIGTSTP), ERROR_NONE);
CHECK_EQ(app_signal_send(app.id, SIGTTIN), ERROR_NONE);
CHECK_EQ(app_signal_send(app.id, SIGTTOU), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK(g_signal_app_continued.load(std::memory_order_acquire));
CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), 0);
}
TEST_CASE("A signal interrupts usleep() with EINTR") {
BlockedStdinApp app("test.libc.signal_sleep", interrupted_sleep_app_main);
CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_signal_handled.load(std::memory_order_acquire), SIGUSR1);
CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), -1);
CHECK_EQ(g_signal_call_errno.load(std::memory_order_acquire), EINTR);
}
TEST_CASE("A signal interrupts poll() without a timeout with EINTR") {
BlockedStdinApp app("test.libc.signal_poll", interrupted_poll_app_main);
CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK_EQ(g_signal_handled.load(std::memory_order_acquire), SIGUSR1);
CHECK_EQ(g_signal_call_result.load(std::memory_order_acquire), -1);
CHECK_EQ(g_signal_call_errno.load(std::memory_order_acquire), EINTR);
}
TEST_CASE("An app with an event subscription receives signals as events") {
g_event_signal.store(0, std::memory_order_relaxed);
g_event_closed.store(false, std::memory_order_relaxed);
BlockedStdinApp app("test.libc.signal_evented", evented_signal_app_main);
CHECK_EQ(app_signal_send(app.id, SIGUSR1), ERROR_NONE);
uint32_t waited = 0;
while (g_event_signal.load(std::memory_order_acquire) == 0 && waited < 1000) {
delay_millis(10);
waited += 10;
}
CHECK_EQ(g_event_signal.load(std::memory_order_acquire), SIGUSR1);
CHECK_FALSE(g_event_closed.load(std::memory_order_acquire));
CHECK_EQ(app_signal_send(app.id, SIGTERM), ERROR_NONE);
REQUIRE(wait_for_state(app.id, APP_INSTANCE_STATE_STOPPED, 1000));
CHECK(g_event_closed.load(std::memory_order_acquire));
}
TEST_CASE("kill() and getpid() address app instances") {
ensure_memory_loader_registered();
reset_signal_flags();
g_kill_probe_ok.store(false, std::memory_order_relaxed);
g_kill_missing_esrch.store(false, std::memory_order_relaxed);
g_kill_group_esrch.store(false, std::memory_order_relaxed);
g_getppid_top_level.store(false, std::memory_order_relaxed);
g_kill_self_handled_before_return.store(false, std::memory_order_relaxed);
AppManifest manifest { "test.libc.kill", "Kill", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(kill_app_main) } };
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
AppInstanceId instance_id = 0;
AppStartContext context;
REQUIRE_EQ(app_start_context_from_id("test.libc.kill", &context), ERROR_NONE);
REQUIRE_EQ(app_start_with_context(&context, &instance_id), ERROR_NONE);
REQUIRE(wait_for_state(instance_id, APP_INSTANCE_STATE_STOPPED, 2000));
CHECK(g_kill_probe_ok.load(std::memory_order_acquire));
CHECK(g_kill_missing_esrch.load(std::memory_order_acquire));
CHECK(g_kill_group_esrch.load(std::memory_order_acquire));
CHECK(g_getppid_top_level.load(std::memory_order_acquire));
CHECK(g_kill_self_handled_before_return.load(std::memory_order_acquire));
app_manager_remove("test.libc.kill");
}
TEST_CASE("app_signal_send() rejects an out-of-range signal and an unknown app") {
CHECK_EQ(app_signal_send(1, 0), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_signal_send(1, 32), ERROR_INVALID_ARGUMENT);
CHECK_EQ(app_signal_send(0x7FFFFFF0, SIGTERM), ERROR_NOT_FOUND);
}

View File

@ -114,6 +114,7 @@ shared_symbol_code_point_names = [
"terminal",
"logo_dev",
"camera",
"lightstrip",
]
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded

View File

@ -48,3 +48,4 @@
#define LVGL_ICON_SHARED_TERMINAL "\xEE\xAE\x8E"
#define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96"
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
#define LVGL_ICON_SHARED_LIGHTSTRIP "\xF3\xBF\xBD\xB5"

View File

@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 12 px
* Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@ -258,7 +258,13 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x80, 0x0, 0x9, 0x0, 0x70, 0x91, 0xcd,
0x9, 0xd, 0xc0, 0x90, 0xac, 0x4, 0x6, 0xd0,
0x0, 0x97, 0x0, 0x1c, 0x2c, 0x7, 0x0, 0x7f,
0x80
0x80,
/* U+FFF75 "󿽵" */
0xff, 0xff, 0xec, 0x41, 0x13, 0xd5, 0x55, 0x7a,
0xaf, 0xef, 0xb, 0xd1, 0x3b, 0x81, 0x2e, 0xd5,
0x7f, 0x5e, 0xaa, 0xab, 0xc4, 0x11, 0x3b, 0xff,
0xff
};
@ -313,7 +319,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 883, .adv_w = 192, .box_w = 8, .box_h = 8, .ofs_x = 2, .ofs_y = 2},
{.bitmap_index = 899, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2},
{.bitmap_index = 923, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = 1},
{.bitmap_index = 946, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}
{.bitmap_index = 946, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1},
{.bitmap_index = 971, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}
};
/*---------------------
@ -335,6 +342,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@ -359,7 +370,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,

View File

@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 16 px
* Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@ -361,7 +361,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xca, 0x0, 0x80, 0xc, 0xa0, 0x0, 0x0, 0xc7,
0x0, 0x0, 0x2c, 0x38, 0x0, 0x3, 0x40, 0xe0,
0x1, 0xe0, 0x3, 0xea, 0xf8, 0x0, 0x6, 0xf8,
0x0
0x0,
/* U+FFF75 "󿽵" */
0x55, 0x55, 0x55, 0xf, 0xff, 0xff, 0xfc, 0xd1,
0x4, 0x4, 0xde, 0x55, 0x55, 0x5d, 0xff, 0xff,
0xff, 0xd0, 0x7, 0xf4, 0xd, 0x1b, 0xe0, 0x21,
0xd7, 0x90, 0x82, 0xf8, 0xe1, 0x7, 0xf8, 0xf,
0xff, 0xff, 0xfd, 0xe5, 0x55, 0x55, 0xdd, 0x20,
0x80, 0x8d, 0xb5, 0x55, 0x55, 0xd2, 0xff, 0xff,
0xfd
};
@ -416,7 +425,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1630, .adv_w = 256, .box_w = 12, .box_h = 12, .ofs_x = 2, .ofs_y = 2},
{.bitmap_index = 1666, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2},
{.bitmap_index = 1714, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1},
{.bitmap_index = 1756, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}
{.bitmap_index = 1756, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1},
{.bitmap_index = 1805, .adv_w = 256, .box_w = 14, .box_h = 14, .ofs_x = 1, .ofs_y = 1}
};
/*---------------------
@ -438,6 +448,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@ -462,7 +476,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,

View File

@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 20 px
* Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@ -480,6 +480,19 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x7, 0x80, 0xf4, 0x0, 0x0, 0xf0, 0x3,
0xe0, 0x0, 0x7c, 0x0, 0xb, 0xea, 0xbf, 0x0,
0x0, 0x1b, 0xff, 0x40, 0x0, 0x0, 0x0, 0x0,
0x0,
/* U+FFF75 "󿽵" */
0x2a, 0xaa, 0xaa, 0xa9, 0x3, 0xff, 0xff, 0xff,
0xfc, 0x38, 0x0, 0x0, 0x1, 0xd3, 0x89, 0x7,
0x3, 0xe, 0x38, 0x0, 0x0, 0x0, 0xe3, 0xff,
0xff, 0xff, 0xfe, 0x2a, 0xab, 0xff, 0xab, 0xe0,
0x1, 0xff, 0x44, 0xe, 0x6, 0xfe, 0x0, 0x82,
0xd2, 0xf9, 0x2, 0x6, 0xf8, 0x38, 0x30, 0x1b,
0xf8, 0x3, 0xea, 0xaf, 0xfe, 0xa9, 0x3f, 0xff,
0xff, 0xff, 0xe3, 0x80, 0x0, 0x1, 0xe, 0x38,
0x90, 0x20, 0x20, 0xe3, 0xea, 0xaa, 0xaa, 0xae,
0x1f, 0xff, 0xff, 0xff, 0xe0, 0x0, 0x0, 0x0,
0x0
};
@ -535,7 +548,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 2555, .adv_w = 320, .box_w = 14, .box_h = 14, .ofs_x = 3, .ofs_y = 3},
{.bitmap_index = 2604, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3},
{.bitmap_index = 2674, .adv_w = 320, .box_w = 16, .box_h = 18, .ofs_x = 2, .ofs_y = 1},
{.bitmap_index = 2746, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}
{.bitmap_index = 2746, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1},
{.bitmap_index = 2827, .adv_w = 320, .box_w = 18, .box_h = 18, .ofs_x = 1, .ofs_y = 1}
};
/*---------------------
@ -557,6 +571,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@ -581,7 +599,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,

View File

@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 24 px
* Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@ -582,7 +582,22 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0x0, 0x7c, 0x1f, 0x0, 0x0, 0x0, 0xf4,
0xb, 0xd0, 0x0, 0x7, 0xe0, 0x2, 0xfd, 0x0,
0x7f, 0x80, 0x0, 0x7f, 0xff, 0xfd, 0x0, 0x0,
0x6, 0xff, 0x90, 0x0
0x6, 0xff, 0x90, 0x0,
/* U+FFF75 "󿽵" */
0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff,
0xff, 0xfe, 0xf0, 0x0, 0x0, 0x0, 0xf, 0xf0,
0xd0, 0x28, 0x7, 0xf, 0xf0, 0x0, 0x0, 0x0,
0xf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x0, 0x2, 0xff, 0x80, 0xf,
0x0, 0x7f, 0xf4, 0x28, 0xf, 0xb, 0xfe, 0x0,
0x0, 0x2f, 0x7f, 0x90, 0x28, 0x7, 0xfd, 0xf8,
0x34, 0x1, 0xbf, 0xe0, 0xf0, 0x10, 0x6f, 0xf9,
0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf0, 0x0, 0x0, 0x0, 0xf,
0xf0, 0xd0, 0x28, 0x7, 0xf, 0xf0, 0x0, 0x0,
0x0, 0xf, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x2f,
0xff, 0xff, 0xff, 0xff
};
@ -637,7 +652,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 3288, .adv_w = 384, .box_w = 16, .box_h = 16, .ofs_x = 4, .ofs_y = 4},
{.bitmap_index = 3352, .adv_w = 384, .box_w = 22, .box_h = 16, .ofs_x = 1, .ofs_y = 4},
{.bitmap_index = 3440, .adv_w = 384, .box_w = 18, .box_h = 20, .ofs_x = 3, .ofs_y = 2},
{.bitmap_index = 3530, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}
{.bitmap_index = 3530, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2},
{.bitmap_index = 3630, .adv_w = 384, .box_w = 20, .box_h = 20, .ofs_x = 2, .ofs_y = 2}
};
/*---------------------
@ -659,6 +675,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@ -683,7 +703,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,

View File

@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 32 px
* Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@ -980,6 +980,33 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x0, 0xb, 0xff, 0xaa, 0xbf, 0xf4, 0x0, 0x0,
0x1, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
0x1b, 0xff, 0xe4, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0,
/* U+FFF75 "󿽵" */
0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x40, 0x3f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfc, 0x3e, 0x0, 0x0,
0x0, 0x0, 0x0, 0xbd, 0x3e, 0x7, 0x0, 0x1c,
0x0, 0x70, 0x3e, 0x3e, 0xb, 0x0, 0x2c, 0x0,
0x70, 0x3e, 0x3e, 0x0, 0x0, 0x0, 0x0, 0x0,
0x3e, 0x3f, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbe,
0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0,
0x6, 0xff, 0xf8, 0x0, 0x7e, 0x0, 0x0, 0xbf,
0xfe, 0x42, 0x40, 0x3e, 0x0, 0x1f, 0xff, 0xd0,
0x3, 0x80, 0x3e, 0x2, 0xff, 0xf8, 0x1c, 0x0,
0x1, 0xfd, 0xf, 0xfe, 0x40, 0x2c, 0x0, 0x6f,
0xf8, 0x3f, 0xe0, 0xd0, 0x0, 0xb, 0xff, 0xe0,
0x3e, 0x1, 0xd0, 0x2, 0xff, 0xfd, 0x0, 0x3f,
0x0, 0x0, 0x7f, 0xff, 0x80, 0x0, 0x3f, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfe, 0x3f, 0xaa, 0xaa, 0xaa,
0xaa, 0xaa, 0xbe, 0x3e, 0x1, 0x0, 0x4, 0x0,
0x10, 0x3e, 0x3e, 0xb, 0x0, 0x2c, 0x0, 0xb4,
0x3e, 0x3e, 0x1, 0x0, 0x4, 0x0, 0x10, 0x3e,
0x3f, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbe, 0x1f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0
};
@ -1035,7 +1062,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 6130, .adv_w = 512, .box_w = 22, .box_h = 22, .ofs_x = 5, .ofs_y = 5},
{.bitmap_index = 6251, .adv_w = 512, .box_w = 30, .box_h = 22, .ofs_x = 1, .ofs_y = 5},
{.bitmap_index = 6416, .adv_w = 512, .box_w = 25, .box_h = 28, .ofs_x = 4, .ofs_y = 2},
{.bitmap_index = 6591, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}
{.bitmap_index = 6591, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2},
{.bitmap_index = 6787, .adv_w = 512, .box_w = 28, .box_h = 28, .ofs_x = 2, .ofs_y = 2}
};
/*---------------------
@ -1057,6 +1085,10 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
{
.range_start = 57669, .range_length = 6019, .glyph_id_start = 1,
.unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 46, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 1048437, .range_length = 1, .glyph_id_start = 47,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
@ -1081,7 +1113,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
.cmaps = cmaps,
.kern_dsc = NULL,
.kern_scale = 0,
.cmap_num = 1,
.cmap_num = 2,
.bpp = 2,
.kern_classes = 0,
.bitmap_format = 0,

View File

@ -59,6 +59,7 @@ typedef struct {
lv_obj_t* close_button_image;
lv_obj_t* action_container;
uint8_t action_count;
lv_event_cb_t nav_action_callback;
} Toolbar;
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
@ -105,6 +106,7 @@ lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) {
lv_obj_set_height(obj, toolbar_height);
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
toolbar->nav_action_callback = nullptr;
lv_obj_set_width(obj, LV_PCT(100));
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(obj, 0, LV_STATE_DEFAULT);
@ -176,7 +178,13 @@ void lvgl_toolbar_set_title(lv_obj_t* obj, const char* title) {
void lvgl_toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) {
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
if (toolbar->nav_action_callback != nullptr) {
lv_obj_remove_event_cb(toolbar->close_button, toolbar->nav_action_callback);
}
toolbar->nav_action_callback = callback;
if (callback != nullptr) {
lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
}
lv_image_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE
}

View File

@ -0,0 +1,34 @@
#pragma once
#include <cstdint>
#include <tactility/drivers/led_strip.h>
namespace tt::settings::ledstrip {
enum class Pattern : uint8_t {
Solid,
Alternating,
Gradient,
};
struct LedStripSettings {
bool enabled = true;
Pattern pattern = Pattern::Alternating;
uint8_t brightness = 77;
uint8_t primaryPreset = 6;
uint8_t secondaryPreset = 7;
struct LedRgb primaryCustom { 255, 165, 0 }; //Orange
struct LedRgb secondaryCustom { 0, 0, 255 }; //Blue
};
bool load(const char* deviceName, LedStripSettings& settings);
LedStripSettings loadOrGetDefault(const char* deviceName);
LedStripSettings getDefault();
bool save(const char* deviceName, const LedStripSettings& settings);
error_t apply(struct Device* device, const LedStripSettings& settings);
}

View File

@ -21,6 +21,12 @@ public:
/** @return true if onKey returned true for any key read */
bool pump(KeyHandler onKey);
/**
* While exclusive, LVGL's own indevs for these keyboards are disabled, since they would
* otherwise consume keys from the same devices. No effect while LVGL is not running.
*/
void setExclusive(bool exclusive);
private:
static constexpr uint32_t REFRESH_INTERVAL_MS = 1000;
@ -28,7 +34,9 @@ private:
void refresh();
void rescan();
void applyExclusive();
std::vector<Device*> devices_;
TickType_t lastRefresh_ = 0;
bool exclusive_ = false;
};

View File

@ -1,9 +1,13 @@
#pragma once
struct Device;
class TerminalRenderer;
/**
* Runs the terminal until the user exits. Must be called after module_stop(&lvgl_module), since it
* drives the display and keyboards directly. Tracks keyboards and the touch-to-exit gesture itself.
* Runs the terminal until the user exits. Unless the renderer is a TerminalRendererLvgl, must be
* called after module_stop(&lvgl_module), since it then drives the display directly. Tracks
* keyboards itself.
* @param[in] renderer not yet begun, begun and ended by this call
* @param[in] touchToExit whether touching the screen exits a keyboard-less terminal
*/
void runTerminal(struct Device* display);
void runTerminal(struct Device* display, TerminalRenderer& renderer, bool touchToExit);

View File

@ -3,6 +3,8 @@
#include <cstddef>
#include <cstdint>
#include <tactility/drivers/display.h>
struct Device;
struct PixelBuffer;
@ -18,7 +20,8 @@ struct PixelBuffer;
* per-row push would show a mix of stale and fresh rows, since a hw double buffer's zero-copy
* path flips on any write regardless of the sub-rect given.
*
* Must be used only while LVGL is stopped, since it writes to the display directly.
* Must be used only while LVGL is stopped, since it writes to the display directly. The exception
* is TerminalRendererLvgl, which draws into an LVGL canvas instead.
*/
class TerminalRenderer {
public:
@ -40,6 +43,9 @@ public:
int columns() const { return cols; }
int rows() const { return rowCount; }
/** False while the terminal is not on screen, e.g. when another app's window covers it. */
virtual bool isShown() const { return true; }
protected:
void paintCell(int row, int col, char ch, uint8_t attr);
void paintCursor(int row, int col);
@ -57,6 +63,9 @@ protected:
*/
bool allocateCommon(Device* display);
/** Like allocateCommon(Device*), but with frameBuffer in the given format instead of the display's. */
bool allocateCommon(Device* display, enum DisplayColorFormat frameColorFormat);
/** Frees frameBuffer, fullFrameBuffer, shadow, and the borrowed hw double buffer, if any. */
void freeCommon();

View File

@ -0,0 +1,41 @@
#pragma once
#include <Tactility/app/terminal/TerminalRenderer.h>
#include <lvgl.h>
#include <atomic>
/**
* Draws the terminal into an LVGL canvas, so it can live in a regular window while LVGL keeps
* running. The frame is kept in a canvas-sized buffer that survives the canvas itself being deleted
* and recreated by the window manager.
*/
class TerminalRendererLvgl : public TerminalRenderer {
public:
~TerminalRendererLvgl() override;
/** Sizes the grid to the canvas given to attachCanvas(), which must happen first. */
bool begin(Device* display) override;
/** Releases all resources and deletes the attached canvas, since it would still show the freed buffer. */
void end() override;
bool isShown() const override { return canvas_ != nullptr; }
/**
* Shows the frame on the given canvas, or detaches from it when nullptr.
* @warning Caller must hold the LVGL lock.
*/
void attachCanvas(lv_obj_t* canvas);
protected:
void present(int yStart, int yEnd) override;
private:
// Owned by the LVGL lock, but read without it by isShown().
std::atomic<lv_obj_t*> canvas_ = nullptr;
// The whole frame, as shown by the canvas.
PixelBuffer* canvasBuffer_ = nullptr;
};

View File

@ -91,6 +91,7 @@
extern "C" Module tactility_audio_module;
#include <tactility/drivers/grove.h>
#include <tactility/drivers/imu.h>
#include <tactility/drivers/led_strip.h>
#include <tactility/drivers/power_supply.h>
#include <tactility/drivers/rtc.h>
#include <tactility/drivers/trackball.h>
@ -176,6 +177,7 @@ namespace app {
namespace imageviewer { extern const ::AppManifest manifest; }
namespace inputdialog { extern const ::AppManifest manifest; }
namespace launcher { extern const ::AppManifest manifest; }
namespace ledstripsettings { extern const ::AppManifest manifest; }
namespace localesettings { extern const ::AppManifest manifest; }
namespace notes { extern const ::AppManifest manifest; }
namespace power { extern const ::AppManifest manifest; }
@ -237,6 +239,9 @@ static void registerInternalApps() {
if (device_exists_of_type(&DISPLAY_TYPE)) {
app_manager_add(&app::display::manifest);
}
if (device_exists_of_type(&LED_STRIP_TYPE)) {
app_manager_add(&app::ledstripsettings::manifest);
}
app_manager_add(&app::files::manifest);
app_manager_add(&app::fileselection::manifest);
app_manager_add(&app::i2cscanner::manifest);

View File

@ -0,0 +1,474 @@
#include <lvgl/lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/led_strip.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <Tactility/Tactility.h>
#include <Tactility/settings/LedStripSettings.h>
#include <app/event.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <lvgl_window_manager/window_manager.h>
#include <lvgl/widgets/sliderbox.h>
#include <lvgl/widgets/toolbar.h>
#include <cstdint>
#include <string>
namespace tt::app::ledstripsettings {
extern const ::AppManifest manifest;
constexpr auto* TAG = "LedStripSettings";
constexpr uint8_t CUSTOM_PRESET = 12;
constexpr const char* COLOR_NAMES[] = {
"White", "Green", "Red", "Blue",
"Yellow", "Orange", "Cyan", "Magenta",
"Purple", "Pink", "Teal", "Black"
};
namespace {
struct Context {
uint32_t appInstanceId = 0;
Device* device = nullptr;
settings::ledstrip::LedStripSettings settings;
bool updated = false;
enum class Page : uint8_t { Main, Choices, CustomColor } page = Page::Main;
enum class ChoiceKind : uint8_t { None, Pattern, PrimaryColor, SecondaryColor } choiceKind = ChoiceKind::None;
bool editingSecondary = false;
lv_obj_t* toolbar = nullptr;
lv_obj_t* mainPanel = nullptr;
lv_obj_t* choicePanel = nullptr;
lv_obj_t* choiceList = nullptr;
lv_obj_t* customPanel = nullptr;
lv_obj_t* returnFocus = nullptr;
lv_obj_t* modeValue = nullptr;
lv_obj_t* primaryValue = nullptr;
lv_obj_t* secondaryRow = nullptr;
lv_obj_t* secondaryValue = nullptr;
lv_obj_t* primaryEditor = nullptr;
lv_obj_t* secondaryEditor = nullptr;
lv_obj_t* primarySliderBoxes[3] = {};
lv_obj_t* secondarySliderBoxes[3] = {};
lv_obj_t* brightnessSlider = nullptr;
lv_obj_t* brightnessValue = nullptr;
};
const char* patternName(settings::ledstrip::Pattern pattern) {
switch (pattern) {
case settings::ledstrip::Pattern::Solid: return "Solid";
case settings::ledstrip::Pattern::Alternating: return "Alternating";
case settings::ledstrip::Pattern::Gradient: return "Gradient";
}
return "Solid";
}
const char* presetName(uint8_t preset) {
if (preset == CUSTOM_PRESET) return "Custom";
return COLOR_NAMES[preset < CUSTOM_PRESET ? preset : 0];
}
void applyAndLog(Context& ctx) {
const error_t result = settings::ledstrip::apply(ctx.device, ctx.settings);
if (result != ERROR_NONE) {
LOG_W(TAG, "Failed to apply LED strip settings: %d", result);
}
}
void updateVisibility(Context& ctx) {
const bool hasSecondary = ctx.settings.pattern != settings::ledstrip::Pattern::Solid;
if (hasSecondary) {
lv_obj_remove_flag(ctx.secondaryRow, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(ctx.secondaryRow, LV_OBJ_FLAG_HIDDEN);
}
}
void onNavigationPressed(lv_event_t* event);
void returnToMain(Context& ctx) {
ctx.page = Context::Page::Main;
ctx.choiceKind = Context::ChoiceKind::None;
lv_obj_add_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(ctx.customPanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_update_layout(ctx.mainPanel);
lvgl_toolbar_set_title(ctx.toolbar, "LED Strip");
lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, &ctx);
if (ctx.returnFocus != nullptr) {
lv_group_focus_obj(ctx.returnFocus);
}
}
void onNavigationPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
if (ctx->page != Context::Page::Main) {
returnToMain(*ctx);
} else {
app_event_emit_close(ctx->appInstanceId);
}
}
void openCustomEditor(Context& ctx, bool secondary) {
ctx.page = Context::Page::CustomColor;
ctx.choiceKind = Context::ChoiceKind::None;
ctx.editingSecondary = secondary;
lv_obj_add_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(ctx.customPanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(secondary ? ctx.primaryEditor : ctx.secondaryEditor, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(secondary ? ctx.secondaryEditor : ctx.primaryEditor, LV_OBJ_FLAG_HIDDEN);
lvgl_toolbar_set_title(ctx.toolbar, secondary ? "Custom second color" : "Custom color");
lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_LEFT, onNavigationPressed, &ctx);
lv_obj_update_layout(ctx.customPanel);
auto* sliderBox = secondary ? ctx.secondarySliderBoxes[0] : ctx.primarySliderBoxes[0];
lv_group_focus_obj(sliderBox);
}
size_t choiceCount(Context::ChoiceKind kind) {
return kind == Context::ChoiceKind::Pattern ? 3 : CUSTOM_PRESET + 1;
}
uint32_t currentChoice(const Context& ctx) {
switch (ctx.choiceKind) {
case Context::ChoiceKind::Pattern: return static_cast<uint32_t>(ctx.settings.pattern);
case Context::ChoiceKind::PrimaryColor: return ctx.settings.primaryPreset;
case Context::ChoiceKind::SecondaryColor: return ctx.settings.secondaryPreset;
case Context::ChoiceKind::None: return 0;
}
return 0;
}
const char* choiceTitle(Context::ChoiceKind kind) {
switch (kind) {
case Context::ChoiceKind::Pattern: return "Choose mode";
case Context::ChoiceKind::PrimaryColor: return "Choose color";
case Context::ChoiceKind::SecondaryColor: return "Choose second color";
case Context::ChoiceKind::None: return "LED Strip";
}
return "LED Strip";
}
const char* choiceName(Context::ChoiceKind kind, size_t index) {
if (kind == Context::ChoiceKind::Pattern) {
return patternName(static_cast<settings::ledstrip::Pattern>(index));
}
return index == CUSTOM_PRESET ? "Custom" : COLOR_NAMES[index];
}
void onChoiceSelected(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
const size_t selected = lv_obj_get_index(static_cast<lv_obj_t*>(lv_event_get_target(event)));
if (selected >= choiceCount(ctx->choiceKind)) return;
const auto choiceKind = ctx->choiceKind;
switch (ctx->choiceKind) {
case Context::ChoiceKind::Pattern:
ctx->settings.pattern = static_cast<settings::ledstrip::Pattern>(selected);
break;
case Context::ChoiceKind::PrimaryColor:
ctx->settings.primaryPreset = static_cast<uint8_t>(selected);
break;
case Context::ChoiceKind::SecondaryColor:
ctx->settings.secondaryPreset = static_cast<uint8_t>(selected);
break;
case Context::ChoiceKind::None:
return;
}
ctx->updated = true;
lv_label_set_text(ctx->modeValue, patternName(ctx->settings.pattern));
lv_label_set_text(ctx->primaryValue, presetName(ctx->settings.primaryPreset));
lv_label_set_text(ctx->secondaryValue, presetName(ctx->settings.secondaryPreset));
updateVisibility(*ctx);
if (selected == CUSTOM_PRESET && choiceKind != Context::ChoiceKind::Pattern) {
openCustomEditor(*ctx, choiceKind == Context::ChoiceKind::SecondaryColor);
applyAndLog(*ctx);
return;
}
returnToMain(*ctx);
applyAndLog(*ctx);
}
void openChoice(Context& ctx, Context::ChoiceKind kind, lv_obj_t* returnFocus) {
ctx.page = Context::Page::Choices;
ctx.choiceKind = kind;
ctx.returnFocus = returnFocus;
lv_obj_clean(ctx.choiceList);
const size_t selected = currentChoice(ctx);
lv_obj_t* selectedButton = nullptr;
for (size_t index = 0; index < choiceCount(kind); index++) {
auto* button = lv_list_add_button(ctx.choiceList, index == selected ? LV_SYMBOL_OK : nullptr, choiceName(kind, index));
lv_obj_add_event_cb(button, onChoiceSelected, LV_EVENT_SHORT_CLICKED, &ctx);
if (index == selected) selectedButton = button;
}
lvgl_toolbar_set_title(ctx.toolbar, choiceTitle(kind));
lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_LEFT, onNavigationPressed, &ctx);
lv_obj_add_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
if (selectedButton != nullptr) {
lv_group_focus_obj(selectedButton);
}
}
void onOutputChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
ctx->settings.enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
ctx->updated = true;
applyAndLog(*ctx);
}
void onPatternPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
openChoice(*ctx, Context::ChoiceKind::Pattern, static_cast<lv_obj_t*>(lv_event_get_target(event)));
}
void onPrimaryColorPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
openChoice(*ctx, Context::ChoiceKind::PrimaryColor, static_cast<lv_obj_t*>(lv_event_get_target(event)));
}
void onSecondaryColorPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
openChoice(*ctx, Context::ChoiceKind::SecondaryColor, static_cast<lv_obj_t*>(lv_event_get_target(event)));
}
void setChannel(LedRgb& color, size_t channel, uint8_t value) {
switch (channel) {
case 0: color.r = value; break;
case 1: color.g = value; break;
case 2: color.b = value; break;
default: break;
}
}
void onColorSliderChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* target = static_cast<lv_obj_t*>(lv_event_get_target(event));
const uint8_t value = static_cast<uint8_t>(lvgl_sliderbox_get_value(target));
for (size_t channel = 0; channel < 3; channel++) {
if (target == ctx->primarySliderBoxes[channel]) {
setChannel(ctx->settings.primaryCustom, channel, value);
ctx->updated = true;
applyAndLog(*ctx);
return;
}
if (target == ctx->secondarySliderBoxes[channel]) {
setChannel(ctx->settings.secondaryCustom, channel, value);
ctx->updated = true;
applyAndLog(*ctx);
return;
}
}
}
void onBrightnessChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
const auto value = static_cast<uint8_t>(lvgl_sliderbox_get_value(static_cast<lv_obj_t*>(lv_event_get_target(event))));
ctx->settings.brightness = value;
lv_label_set_text_fmt(ctx->brightnessValue, "%u%%", (static_cast<unsigned>(value) * 100 + 127) / 255);
ctx->updated = true;
applyAndLog(*ctx);
}
lv_obj_t* createRow(lv_obj_t* parent, const char* title) {
auto* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 2, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
auto* label = lv_label_create(row);
lv_label_set_text(label, title);
lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
return row;
}
void createRgbEditor(lv_obj_t* parent, const char* title, const LedRgb& color, lv_obj_t** sliderBoxes, Context* ctx, bool primary) {
auto* editor = lv_obj_create(parent);
lv_obj_set_width(editor, LV_PCT(100));
lv_obj_set_flex_flow(editor, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(editor, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(editor, 0, LV_STATE_DEFAULT);
auto* heading = lv_label_create(editor);
lv_label_set_text(heading, title);
constexpr const char* CHANNELS[] = { "R", "G", "B" };
const uint8_t channels[] = { color.r, color.g, color.b };
for (size_t i = 0; i < 3; i++) {
auto* row = lv_obj_create(editor);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 1, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
auto* label = lv_label_create(row);
lv_label_set_text(label, CHANNELS[i]);
lv_obj_set_width(label, LV_PCT(12));
lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
sliderBoxes[i] = lvgl_sliderbox_create(row, 0, UINT8_MAX, 8, channels[i]);
lv_obj_set_width(sliderBoxes[i], LV_PCT(84));
lv_obj_align(sliderBoxes[i], LV_ALIGN_RIGHT_MID, 0, 0);
lvgl_sliderbox_add_value_changed_cb(sliderBoxes[i], onColorSliderChanged, ctx);
}
if (primary) {
ctx->primaryEditor = editor;
} else {
ctx->secondaryEditor = editor;
}
}
void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
ctx->toolbar = lvgl_toolbar_create(parent, "LED Strip");
lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, ctx);
ctx->mainPanel = lv_obj_create(parent);
lv_obj_set_width(ctx->mainPanel, LV_PCT(100));
lv_obj_set_flex_grow(ctx->mainPanel, 1);
lv_obj_set_flex_flow(ctx->mainPanel, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(ctx->mainPanel, 2, LV_STATE_DEFAULT);
lv_obj_add_flag(ctx->mainPanel, LV_OBJ_FLAG_SCROLLABLE);
ctx->customPanel = lv_obj_create(parent);
lv_obj_set_width(ctx->customPanel, LV_PCT(100));
lv_obj_set_flex_grow(ctx->customPanel, 1);
lv_obj_set_flex_flow(ctx->customPanel, LV_FLEX_FLOW_COLUMN);
lv_obj_add_flag(ctx->customPanel, LV_OBJ_FLAG_HIDDEN);
auto* outputRow = createRow(ctx->mainPanel, "Output");
auto* outputSwitch = lv_switch_create(outputRow);
if (ctx->settings.enabled) lv_obj_add_state(outputSwitch, LV_STATE_CHECKED);
lv_obj_align(outputSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(outputSwitch, onOutputChanged, LV_EVENT_VALUE_CHANGED, ctx);
auto* modeRow = createRow(ctx->mainPanel, "Mode");
auto* modeButton = lv_button_create(modeRow);
lv_obj_set_width(modeButton, LV_PCT(62));
lv_obj_align(modeButton, LV_ALIGN_RIGHT_MID, 0, 0);
ctx->modeValue = lv_label_create(modeButton);
lv_label_set_text(ctx->modeValue, patternName(ctx->settings.pattern));
lv_obj_center(ctx->modeValue);
lv_obj_add_event_cb(modeButton, onPatternPressed, LV_EVENT_SHORT_CLICKED, ctx);
auto* primaryRow = createRow(ctx->mainPanel, "Color");
auto* primaryButton = lv_button_create(primaryRow);
lv_obj_set_width(primaryButton, LV_PCT(62));
lv_obj_align(primaryButton, LV_ALIGN_RIGHT_MID, 0, 0);
ctx->primaryValue = lv_label_create(primaryButton);
lv_label_set_text(ctx->primaryValue, presetName(ctx->settings.primaryPreset));
lv_obj_center(ctx->primaryValue);
lv_obj_add_event_cb(primaryButton, onPrimaryColorPressed, LV_EVENT_SHORT_CLICKED, ctx);
createRgbEditor(ctx->customPanel, "Red / Green / Blue", ctx->settings.primaryCustom, ctx->primarySliderBoxes, ctx, true);
lv_obj_add_flag(ctx->primaryEditor, LV_OBJ_FLAG_HIDDEN);
ctx->secondaryRow = createRow(ctx->mainPanel, "Second color");
auto* secondaryButton = lv_button_create(ctx->secondaryRow);
lv_obj_set_width(secondaryButton, LV_PCT(62));
lv_obj_align(secondaryButton, LV_ALIGN_RIGHT_MID, 0, 0);
ctx->secondaryValue = lv_label_create(secondaryButton);
lv_label_set_text(ctx->secondaryValue, presetName(ctx->settings.secondaryPreset));
lv_obj_center(ctx->secondaryValue);
lv_obj_add_event_cb(secondaryButton, onSecondaryColorPressed, LV_EVENT_SHORT_CLICKED, ctx);
createRgbEditor(ctx->customPanel, "Red / Green / Blue", ctx->settings.secondaryCustom, ctx->secondarySliderBoxes, ctx, false);
lv_obj_add_flag(ctx->secondaryEditor, LV_OBJ_FLAG_HIDDEN);
auto* brightnessRow = createRow(ctx->mainPanel, "Brightness");
ctx->brightnessValue = lv_label_create(brightnessRow);
lv_label_set_text_fmt(ctx->brightnessValue, "%u%%", (static_cast<unsigned>(ctx->settings.brightness) * 100 + 127) / 255);
lv_obj_align(ctx->brightnessValue, LV_ALIGN_RIGHT_MID, 0, 0);
ctx->brightnessSlider = lvgl_sliderbox_create(ctx->mainPanel, 0, UINT8_MAX, 17, ctx->settings.brightness);
lv_obj_set_width(ctx->brightnessSlider, LV_PCT(100));
lvgl_sliderbox_add_value_changed_cb(ctx->brightnessSlider, onBrightnessChanged, ctx);
ctx->choicePanel = lv_obj_create(parent);
lv_obj_set_width(ctx->choicePanel, LV_PCT(100));
lv_obj_set_flex_grow(ctx->choicePanel, 1);
lv_obj_set_flex_flow(ctx->choicePanel, LV_FLEX_FLOW_COLUMN);
lv_obj_add_flag(ctx->choicePanel, LV_OBJ_FLAG_HIDDEN);
ctx->choiceList = lv_list_create(ctx->choicePanel);
lv_obj_set_width(ctx->choiceList, LV_PCT(100));
lv_obj_set_flex_grow(ctx->choiceList, 1);
updateVisibility(*ctx);
}
void persistIfUpdated(const Context& ctx) {
if (!ctx.updated) return;
const auto settingsToSave = ctx.settings;
const std::string deviceName = ctx.device->name;
getMainDispatcher().dispatch([settingsToSave, deviceName] {
if (!settings::ledstrip::save(deviceName.c_str(), settingsToSave)) {
LOG_W(TAG, "Failed to save LED strip settings");
}
});
}
int32_t appMain(int argc, char* argv[]) {
Device* device = nullptr;
if (device_get_first_by_type(&LED_STRIP_TYPE, &device) != ERROR_NONE) {
return 0;
}
Context ctx {};
ctx.appInstanceId = app_scheduler_current_app_id();
ctx.device = device;
ctx.settings = settings::ledstrip::loadOrGetDefault(device->name);
uint16_t ledCount = 0;
if (led_strip_get_length(device, &ledCount) != ERROR_NONE || ledCount == 0) {
device_put(device);
return 0;
}
applyAndLog(ctx);
TaskEventGroup eventGroup {};
task_event_group_construct(&eventGroup);
AppEventSubscription subscription {};
if (app_event_subscribe(&subscription, &eventGroup) != ERROR_NONE) {
task_event_group_destruct(&eventGroup);
device_put(device);
return 0;
}
const WindowId window = window_manager_create(ctx.appInstanceId, createWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
task_event_group_wait_any(&eventGroup, nullptr, portMAX_DELAY);
AppEvent event {};
while (app_event_poll(&subscription, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
persistIfUpdated(ctx);
shouldClose = true;
break;
}
}
}
window_manager_remove(window);
app_event_unsubscribe(&subscription);
task_event_group_destruct(&eventGroup);
device_put(device);
return 0;
}
}
extern const ::AppManifest manifest = {
.id = "tactility.ledstripsettings",
.name = "LED Strip",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = 0,
.stack = {}
};
}

View File

@ -43,6 +43,7 @@ constexpr IconEntry ICONS[] = {
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},

View File

@ -9,6 +9,7 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/scheduler.h>
#include <app/signal.h>
#include <app/start.h>
#include <app/stream.h>
@ -19,6 +20,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@ -215,6 +217,8 @@ int runApp(AppStartContext& context) {
} else {
// Our own stdin hung up (touch-to-exit): propagate to the child the same way.
app_stream_close(&stdinStream);
// A child that doesn't stop at the end of its input, like an editor, stops at the hangup
app_signal_send(childId, SIGHUP);
ownStdinClosed = true;
}
}

View File

@ -1,5 +1,8 @@
#include <Tactility/app/terminal/KeyboardInput.h>
#include <lvgl/devices/keyboard.h>
#include <lvgl/lvgl.h>
#include <algorithm>
constexpr auto* TAG = "terminal-keyb";
@ -9,6 +12,7 @@ KeyboardInput::KeyboardInput() {
}
KeyboardInput::~KeyboardInput() {
setExclusive(false);
for (Device* device : devices_) {
device_put(device);
}
@ -39,6 +43,28 @@ bool KeyboardInput::pump(KeyHandler onKey) {
return handled;
}
void KeyboardInput::setExclusive(bool exclusive) {
if (exclusive == exclusive_) {
return;
}
exclusive_ = exclusive;
applyExclusive();
}
void KeyboardInput::applyExclusive() {
if (!lvgl_is_running()) {
return;
}
lvgl_lock();
for (Device* device : devices_) {
lv_indev_t* indev = lvgl_keyboard_find_by_device(device);
if (indev != nullptr) {
lv_indev_enable(indev, !exclusive_);
}
}
lvgl_unlock();
}
bool KeyboardInput::collect(Device* device, void* context) {
static_cast<std::vector<Device*>*>(context)->push_back(device);
return true;
@ -76,4 +102,9 @@ void KeyboardInput::rescan() {
++it;
}
}
// A keyboard connected since the last scan gets an enabled LVGL indev of its own.
if (exclusive_) {
applyExclusive();
}
}

View File

@ -119,6 +119,8 @@ void runShell(int columns, int rows, volatile bool* stopRequested, TaskHandle_t
while (app_event_poll(&eventSub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_RESULT && event.result.launch_id == shellId) {
shellDone = true;
} else if (event.type == APP_EVENT_CLOSE) {
*stopRequested = true;
}
}
}

View File

@ -3,8 +3,6 @@
#include <Tactility/app/terminal/Shell.h>
#include <Tactility/app/terminal/Terminal.h>
#include <Tactility/app/terminal/TerminalRenderer.h>
#include <Tactility/app/terminal/TerminalRendererGeneric.h>
#include <Tactility/app/terminal/TerminalRendererPpa.h>
#include <Tactility/app/terminal/TouchInput.h>
#include <tactility/device.h>
@ -46,7 +44,7 @@ constexpr int SCROLL_STEP_LINES = 5;
namespace {
/** Set once the shell should wind down (touch-to-exit; see ioTask()). Read by runTerminal()'s own
/** Set once the shell should wind down (touch-to-exit in ioTask(), or APP_EVENT_CLOSE in runShell()). Read by runTerminal()'s own
* pump loop, which closes the shell app's stdin to unstick it (see that loop's own comment). */
volatile bool stopRequested = false;
@ -160,12 +158,16 @@ void ioTask(void* arg) {
auto* params = static_cast<IoTaskParams*>(arg);
while (!shellFinished) {
// Keys belong to whichever window is on top, so they are left alone while that is not this one.
const bool shown = params->renderer->isShown();
params->keyboards->setExclusive(shown);
// A scroll replaces every row at once, so the renderer is told to repaint rather than rely
// on its per-cell comparison.
const bool viewMoved = params->keyboards->pump(handleKey);
const bool viewMoved = shown && params->keyboards->pump(handleKey);
params->renderer->render(viewMoved);
if (!stopRequested && params->touch->touched(!params->keyboards->empty())) {
if (!stopRequested && params->touch != nullptr && params->touch->touched(!params->keyboards->empty())) {
LOG_I(TAG, "Touch detected - stopping");
stopRequested = true;
}
@ -183,7 +185,7 @@ void ioTask(void* arg) {
} // namespace
void runTerminal(Device* display) {
void runTerminal(Device* display, TerminalRenderer& renderer, bool touchToExit) {
stopRequested = false;
shellFinished = false;
@ -195,11 +197,6 @@ void runTerminal(Device* display) {
return;
}
TerminalRendererPpa ppaRenderer;
TerminalRendererGeneric genericRenderer;
TerminalRenderer& renderer = TerminalRendererPpa::isSupported()
? static_cast<TerminalRenderer&>(ppaRenderer)
: static_cast<TerminalRenderer&>(genericRenderer);
if (!renderer.begin(display)) {
LOG_E(TAG, "Renderer failed to start");
vterm_deinit();
@ -221,7 +218,7 @@ void runTerminal(Device* display) {
// working while this one is blocked draining the shell app. See ioTask().
IoTaskParams ioParams {
.keyboards = &keyboards,
.touch = &touch,
.touch = touchToExit ? &touch : nullptr,
.renderer = &renderer,
.doneSem = xSemaphoreCreateBinary(),
};

View File

@ -41,6 +41,10 @@ inline uint16_t paletteColour(uint8_t index) {
} // namespace
bool TerminalRenderer::allocateCommon(Device* displayDevice) {
return allocateCommon(displayDevice, display_get_color_format(displayDevice));
}
bool TerminalRenderer::allocateCommon(Device* displayDevice, enum DisplayColorFormat frameColorFormat) {
display = displayDevice;
monochrome = display_get_color_format(display) == DISPLAY_COLOR_FORMAT_MONOCHROME;
@ -65,8 +69,7 @@ bool TerminalRenderer::allocateCommon(Device* displayDevice) {
originX = (frameWidth - cols * cellWidth) / 2;
originY = (frameHeight - rowCount * cellHeight) / 2;
const enum DisplayColorFormat colorFormat = display_get_color_format(display);
frameBuffer = pixel_buffer_create(colorFormat, frameWidth, cellHeight);
frameBuffer = pixel_buffer_create(frameColorFormat, frameWidth, cellHeight);
if (frameBuffer == nullptr) {
LOG_E(TAG, "Failed to allocate frame buffer");
return false;

View File

@ -0,0 +1,91 @@
#include <Tactility/app/terminal/TerminalRendererLvgl.h>
#include <graphics/pixel_buffer.h>
#include <lvgl/lvgl.h>
#include <tactility/log.h>
constexpr auto* TAG = "TermRenderLvgl";
TerminalRendererLvgl::~TerminalRendererLvgl() {
end();
}
bool TerminalRendererLvgl::begin(Device* displayDevice) {
lvgl_lock();
lv_obj_t* canvas = canvas_;
if (canvas == nullptr) {
lvgl_unlock();
LOG_E(TAG, "No canvas attached");
return false;
}
lv_obj_update_layout(canvas);
frameWidth = lv_obj_get_width(canvas);
frameHeight = lv_obj_get_height(canvas);
lvgl_unlock();
panelWidth = frameWidth;
panelHeight = frameHeight;
// LVGL converts to the display's own format when flushing.
if (!allocateCommon(displayDevice, DISPLAY_COLOR_FORMAT_RGB565)) {
return false;
}
canvasBuffer_ = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, frameWidth, frameHeight);
if (canvasBuffer_ == nullptr) {
LOG_E(TAG, "Failed to allocate canvas buffer");
freeCommon();
return false;
}
clearPanelOnce();
lvgl_lock();
attachCanvas(canvas_);
lvgl_unlock();
LOG_I(TAG, "Terminal %dx%d cells (%dx%d px) on %dx%d canvas",
cols, rowCount, cellWidth, cellHeight, frameWidth, frameHeight);
return true;
}
void TerminalRendererLvgl::end() {
if (canvasBuffer_ != nullptr) {
lvgl_lock();
lv_obj_t* canvas = canvas_;
if (canvas != nullptr) {
lv_obj_delete(canvas);
canvas_ = nullptr;
}
lvgl_unlock();
pixel_buffer_free(canvasBuffer_);
canvasBuffer_ = nullptr;
}
freeCommon();
}
void TerminalRendererLvgl::attachCanvas(lv_obj_t* canvas) {
canvas_ = canvas;
if (canvas != nullptr && canvasBuffer_ != nullptr) {
lv_canvas_set_buffer(canvas, pixel_buffer_get_data(canvasBuffer_), frameWidth, frameHeight, LV_COLOR_FORMAT_RGB565);
}
}
void TerminalRendererLvgl::present(int yStart, int yEnd) {
// Locked during the blit too, as LVGL may be drawing the canvas from this buffer
lvgl_lock();
pixel_buffer_blit(canvasBuffer_, 0, yStart, frameBuffer, 0, 0, frameWidth, yEnd - yStart, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
lv_obj_t* canvas = canvas_;
if (canvas != nullptr) {
lv_area_t area;
lv_obj_get_coords(canvas, &area);
area.y2 = area.y1 + yEnd - 1;
area.y1 += yStart;
lv_obj_invalidate_area(canvas, &area);
}
lvgl_unlock();
}

View File

@ -2,6 +2,9 @@
#include <Tactility/app/terminal/Terminal.h>
#include <Tactility/app/terminal/TerminalRendererGeneric.h>
#include <Tactility/app/terminal/TerminalRendererLvgl.h>
#include <Tactility/app/terminal/TerminalRendererPpa.h>
#include <tactility/log.h>
@ -14,11 +17,88 @@
#include <tactility/drivers/display.h>
#include <tactility/module.h>
#include <lvgl/module.h>
#include <lvgl/widgets/toolbar.h>
#include <lvgl_window_manager/window_manager.h>
#include <cstring>
constexpr auto* TAG = "terminal";
namespace tt::app::terminal {
namespace {
struct WindowContext {
TerminalRendererLvgl renderer;
AppInstanceId appInstanceId;
};
// The toolbar's default action closes the topmost app instance, which is the shell app rather than this one.
void onClosePressed(lv_event_t* event) {
auto* ctx = static_cast<WindowContext*>(lv_event_get_user_data(event));
app_event_emit_close(ctx->appInstanceId);
}
void createWidgets(lv_obj_t* root, void* userData) {
auto* ctx = static_cast<WindowContext*>(userData);
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(root, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(root, "Terminal");
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx);
auto* canvas = lv_canvas_create(root);
lv_obj_set_width(canvas, LV_PCT(100));
lv_obj_set_flex_grow(canvas, 1);
ctx->renderer.attachCanvas(canvas);
}
void destroyWidgets(void* userData) {
static_cast<WindowContext*>(userData)->renderer.attachCanvas(nullptr);
}
bool hasArgument(int argc, char* argv[], const char* argument) {
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], argument) == 0) {
return true;
}
}
return false;
}
void runInWindow(Device* display) {
WindowContext ctx { .appInstanceId = app_scheduler_current_app_id() };
WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx);
runTerminal(display, ctx.renderer, false);
window_manager_remove(window);
}
void runFullscreen(Device* display) {
// Stop LVGL, because terminal has custom rendering
auto lvgl_active = module_is_started(&lvgl_module);
if (lvgl_active) {
module_stop(&lvgl_module);
}
TerminalRendererPpa ppaRenderer;
TerminalRendererGeneric genericRenderer;
TerminalRenderer& renderer = TerminalRendererPpa::isSupported()
? static_cast<TerminalRenderer&>(ppaRenderer)
: static_cast<TerminalRenderer&>(genericRenderer);
runTerminal(display, renderer, true);
// If needed, restart LVGL
if (lvgl_active && !module_is_started(&lvgl_module)) {
LOG_I(TAG, "Restarting LVGL");
module_start(&lvgl_module);
}
}
} // namespace
int main(int argc, char* argv[]) {
Device* display_device = nullptr;
if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
@ -26,22 +106,19 @@ int main(int argc, char* argv[]) {
return 0;
}
// Stop LVGL, because terminal has custom rendering
auto lvgl_active = module_is_started(&lvgl_module);
if (lvgl_active) {
module_stop(&lvgl_module);
}
// The canvas holds a full frame, which needs external RAM on larger displays.
const bool useLvgl = module_is_started(&lvgl_module) &&
memory_external_total() > 0 &&
!hasArgument(argc, argv, "--no-lvgl");
runTerminal(display_device);
if (useLvgl) {
runInWindow(display_device);
} else {
runFullscreen(display_device);
}
device_put(display_device);
// If needed, restart LVGL
if (lvgl_active && !module_is_started(&lvgl_module)) {
LOG_I(TAG, "Restarting LVGL");
module_start(&lvgl_module);
}
return 0;
}

View File

@ -0,0 +1,194 @@
#include <Tactility/settings/LedStripSettings.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <app/paths.h>
#include <charconv>
#include <algorithm>
#include <iterator>
#include <map>
#include <string>
namespace tt::settings::ledstrip {
namespace {
struct ColorPreset {
const char* name;
LedRgb color;
};
const ColorPreset COLOR_PRESETS[] = {
{"White", LED_RGB_COLOR_WHITE},
{"Green", LED_RGB_COLOR_GREEN},
{"Red", LED_RGB_COLOR_RED},
{"Blue", LED_RGB_COLOR_BLUE},
{"Yellow", LED_RGB_COLOR_YELLOW},
{"Orange", LED_RGB_COLOR_ORANGE},
{"Cyan", LED_RGB_COLOR_CYAN},
{"Magenta", LED_RGB_COLOR_MAGENTA},
{"Purple", LED_RGB_COLOR_PURPLE},
{"Pink", LED_RGB_COLOR_PINK},
{"Teal", LED_RGB_COLOR_TEAL},
{"Black", LED_RGB_COLOR_BLACK},
};
LedRgb getColor(uint8_t preset, const LedRgb& custom) {
if (preset == 12) return custom;
return COLOR_PRESETS[std::min<size_t>(preset, std::size(COLOR_PRESETS) - 1)].color;
}
uint8_t interpolate(uint8_t first, uint8_t second, size_t position, size_t count) {
if (count <= 1) return first;
const size_t denominator = count - 1;
return static_cast<uint8_t>((first * (denominator - position) + second * position) / denominator);
}
}
static std::string getSettingsFilePath(const char* deviceName) {
char path[256];
const auto fileName = std::string(deviceName) + ".properties";
if (app_paths_get_user_data_path("tactility.ledstripsettings", fileName.c_str(), path, sizeof(path)) != ERROR_NONE) {
return "";
}
return path;
}
static bool readNumber(const std::map<std::string, std::string>& properties, const char* key, unsigned maximum, unsigned& value) {
const auto it = properties.find(key);
if (it == properties.end()) {
return false;
}
unsigned parsed = 0;
const auto result = std::from_chars(it->second.data(), it->second.data() + it->second.size(), parsed);
if (result.ec != std::errc() || result.ptr != it->second.data() + it->second.size() || parsed > maximum) {
return false;
}
value = parsed;
return true;
}
static bool readBool(const std::map<std::string, std::string>& properties, const char* key, bool& value) {
const auto it = properties.find(key);
if (it == properties.end()) {
return false;
}
if (it->second == "1" || it->second == "true") {
value = true;
return true;
}
if (it->second == "0" || it->second == "false") {
value = false;
return true;
}
return false;
}
LedStripSettings getDefault() {
return {};
}
bool load(const char* deviceName, LedStripSettings& settings) {
const auto path = getSettingsFilePath(deviceName);
if (!file::isFile(path)) {
return false;
}
std::map<std::string, std::string> properties;
if (!file::loadPropertiesFile(path, properties)) {
return false;
}
auto loaded = getDefault();
unsigned value = 0;
readBool(properties, "enabled", loaded.enabled);
if (readNumber(properties, "pattern", static_cast<unsigned>(Pattern::Gradient), value)) {
loaded.pattern = static_cast<Pattern>(value);
}
if (readNumber(properties, "brightness", UINT8_MAX, value)) loaded.brightness = static_cast<uint8_t>(value);
if (readNumber(properties, "primaryPreset", 12, value)) loaded.primaryPreset = static_cast<uint8_t>(value);
if (readNumber(properties, "secondaryPreset", 12, value)) loaded.secondaryPreset = static_cast<uint8_t>(value);
if (readNumber(properties, "primaryRed", UINT8_MAX, value)) loaded.primaryCustom.r = static_cast<uint8_t>(value);
if (readNumber(properties, "primaryGreen", UINT8_MAX, value)) loaded.primaryCustom.g = static_cast<uint8_t>(value);
if (readNumber(properties, "primaryBlue", UINT8_MAX, value)) loaded.primaryCustom.b = static_cast<uint8_t>(value);
if (readNumber(properties, "secondaryRed", UINT8_MAX, value)) loaded.secondaryCustom.r = static_cast<uint8_t>(value);
if (readNumber(properties, "secondaryGreen", UINT8_MAX, value)) loaded.secondaryCustom.g = static_cast<uint8_t>(value);
if (readNumber(properties, "secondaryBlue", UINT8_MAX, value)) loaded.secondaryCustom.b = static_cast<uint8_t>(value);
settings = loaded;
return true;
}
LedStripSettings loadOrGetDefault(const char* deviceName) {
auto settings = getDefault();
load(deviceName, settings);
return settings;
}
bool save(const char* deviceName, const LedStripSettings& settings) {
std::map<std::string, std::string> properties;
properties["enabled"] = settings.enabled ? "1" : "0";
properties["pattern"] = std::to_string(static_cast<unsigned>(settings.pattern));
properties["brightness"] = std::to_string(settings.brightness);
properties["primaryPreset"] = std::to_string(settings.primaryPreset);
properties["secondaryPreset"] = std::to_string(settings.secondaryPreset);
properties["primaryRed"] = std::to_string(settings.primaryCustom.r);
properties["primaryGreen"] = std::to_string(settings.primaryCustom.g);
properties["primaryBlue"] = std::to_string(settings.primaryCustom.b);
properties["secondaryRed"] = std::to_string(settings.secondaryCustom.r);
properties["secondaryGreen"] = std::to_string(settings.secondaryCustom.g);
properties["secondaryBlue"] = std::to_string(settings.secondaryCustom.b);
auto settings_path = getSettingsFilePath(deviceName);
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
return false;
}
return file::savePropertiesFile(settings_path, properties);
}
error_t apply(struct Device* device, const LedStripSettings& settings) {
if (!settings.enabled) {
return led_strip_clear(device);
}
uint16_t ledCount = 0;
auto result = led_strip_get_length(device, &ledCount);
if (result != ERROR_NONE) return result;
if (ledCount == 0) return ERROR_INVALID_ARGUMENT;
result = led_strip_set_brightness(device, settings.brightness);
if (result != ERROR_NONE) return result;
const LedRgb primary = getColor(settings.primaryPreset, settings.primaryCustom);
const LedRgb secondary = getColor(settings.secondaryPreset, settings.secondaryCustom);
if (settings.pattern == Pattern::Solid) {
result = led_strip_fill_led_range_color(device, 0, ledCount, primary);
if (result != ERROR_NONE) return result;
} else {
for (size_t index = 0; index < ledCount; index++) {
LedRgb color = primary;
if (settings.pattern == Pattern::Alternating) {
color = (index % 2 == 0) ? primary : secondary;
} else if (settings.pattern == Pattern::Gradient) {
color = {
interpolate(primary.r, secondary.r, index, ledCount),
interpolate(primary.g, secondary.g, index, ledCount),
interpolate(primary.b, secondary.b, index, ledCount),
};
}
result = led_strip_set_single_led_color(device, index, color);
if (result != ERROR_NONE) return result;
}
}
return led_strip_show(device);
}
}

View File

@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/device.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief An RGB color value.
*/
struct LedRgb {
uint8_t r;
uint8_t g;
uint8_t b;
};
#define LED_RGB_COLOR_WHITE ((struct LedRgb){255, 255, 255})
#define LED_RGB_COLOR_GREEN ((struct LedRgb){0, 255, 0})
#define LED_RGB_COLOR_RED ((struct LedRgb){255, 0, 0})
#define LED_RGB_COLOR_BLUE ((struct LedRgb){0, 0, 255})
#define LED_RGB_COLOR_YELLOW ((struct LedRgb){255, 255, 0})
#define LED_RGB_COLOR_ORANGE ((struct LedRgb){255, 165, 0})
#define LED_RGB_COLOR_CYAN ((struct LedRgb){0, 255, 255})
#define LED_RGB_COLOR_MAGENTA ((struct LedRgb){255, 0, 255})
#define LED_RGB_COLOR_PURPLE ((struct LedRgb){191, 0, 255})
#define LED_RGB_COLOR_PINK ((struct LedRgb){255, 192, 203})
#define LED_RGB_COLOR_TEAL ((struct LedRgb){0, 128, 128})
#define LED_RGB_COLOR_BLACK ((struct LedRgb){0, 0, 0})
#define LED_COLOR_RGB_CUSTOM(r, g, b) ((struct LedRgb){(r), (g), (b)})
struct LedStripApi {
error_t (*set_single_led_color)(struct Device* device, size_t index, const struct LedRgb color);
error_t (*set_single_led_color_and_brightness)(struct Device* device, size_t index, const struct LedRgb color, uint8_t brightness);
error_t (*set_led_range_color)(struct Device* device, size_t first, size_t count, const struct LedRgb* color);
error_t (*set_led_range_color_and_brightness)(struct Device* device, size_t first, size_t count, const struct LedRgb* color, uint8_t brightness);
error_t (*fill_led_range_color)(struct Device* device, size_t first, size_t count, const struct LedRgb color);
error_t (*fill_led_range_color_and_brightness)(struct Device* device, size_t first, size_t count, const struct LedRgb color, uint8_t brightness);
error_t (*set_brightness)(struct Device* device, uint8_t brightness);
error_t (*show)(struct Device* device);
error_t (*clear)(struct Device* device);
error_t (*get_length)(struct Device* device, uint16_t* out_length);
};
extern const struct DeviceType LED_STRIP_TYPE;
/**
* @brief Sets the color of a single LED using the global brightness.
*/
error_t led_strip_set_single_led_color(struct Device* device, size_t index, const struct LedRgb color);
/**
* @brief Sets the color of a single LED with an explicit brightness value.
*/
error_t led_strip_set_single_led_color_and_brightness(struct Device* device, size_t index, const struct LedRgb color, uint8_t brightness);
/**
* @brief Sets colors for a range of LEDs using the global brightness.
*/
error_t led_strip_set_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb* color);
/**
* @brief Sets colors for a range of LEDs with an explicit brightness value.
*/
error_t led_strip_set_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb* color, uint8_t brightness);
/**
* @brief Fills a range of LEDs with a single color using the global brightness.
*/
error_t led_strip_fill_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb color);
/**
* @brief Fills a range of LEDs with a single color and an explicit brightness value.
*/
error_t led_strip_fill_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb color, uint8_t brightness);
/**
* @brief Set global brightness for the LED strip.
*/
error_t led_strip_set_brightness(struct Device* device, uint8_t brightness);
/**
* @brief Refreshes the strip to update the displayed colors.
*/
error_t led_strip_show(struct Device* device);
/**
* @brief Clears the strip to black.
*/
error_t led_strip_clear(struct Device* device);
/**
* @brief Get the LED strip length.
*/
error_t led_strip_get_length(struct Device* device, uint16_t* out_length);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/led_strip.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
#define TAG "LedStrip"
#define LED_STRIP_DRIVER_API(driver) ((struct LedStripApi*)driver->api)
extern "C" {
error_t led_strip_set_single_led_color(struct Device* device, size_t index, const struct LedRgb color) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->set_single_led_color(device, index, color);
}
error_t led_strip_set_single_led_color_and_brightness(struct Device* device, size_t index, const struct LedRgb color, uint8_t brightness) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->set_single_led_color_and_brightness(device, index, color, brightness);
}
error_t led_strip_set_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb* color) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->set_led_range_color(device, first, count, color);
}
error_t led_strip_set_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb* color, uint8_t brightness) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->set_led_range_color_and_brightness(device, first, count, color, brightness);
}
error_t led_strip_fill_led_range_color(struct Device* device, size_t first, size_t count, const struct LedRgb color) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->fill_led_range_color(device, first, count, color);
}
error_t led_strip_fill_led_range_color_and_brightness(struct Device* device, size_t first, size_t count, const struct LedRgb color, uint8_t brightness) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->fill_led_range_color_and_brightness(device, first, count, color, brightness);
}
error_t led_strip_set_brightness(struct Device* device, uint8_t brightness) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->set_brightness(device, brightness);
}
error_t led_strip_show(struct Device* device) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->show(device);
}
error_t led_strip_clear(struct Device* device) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->clear(device);
}
error_t led_strip_get_length(struct Device* device, uint16_t* out_length) {
const auto* driver = device_get_driver(device);
return LED_STRIP_DRIVER_API(driver)->get_length(device, out_length);
}
const struct DeviceType LED_STRIP_TYPE {
.name = "led_strip"
};
}

View File

@ -25,6 +25,7 @@
#include <tactility/drivers/i8080_controller.h>
#include <tactility/drivers/imu.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/drivers/led_strip.h>
#include <tactility/drivers/lora.h>
#include <tactility/drivers/pointer.h>
#include <tactility/drivers/power_supply.h>
@ -257,6 +258,18 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
// drivers/keyboard
DEFINE_MODULE_SYMBOL(keyboard_read_key),
DEFINE_MODULE_SYMBOL(KEYBOARD_TYPE),
// drivers/led_strip
DEFINE_MODULE_SYMBOL(led_strip_set_single_led_color),
DEFINE_MODULE_SYMBOL(led_strip_set_single_led_color_and_brightness),
DEFINE_MODULE_SYMBOL(led_strip_set_led_range_color),
DEFINE_MODULE_SYMBOL(led_strip_set_led_range_color_and_brightness),
DEFINE_MODULE_SYMBOL(led_strip_fill_led_range_color),
DEFINE_MODULE_SYMBOL(led_strip_fill_led_range_color_and_brightness),
DEFINE_MODULE_SYMBOL(led_strip_set_brightness),
DEFINE_MODULE_SYMBOL(led_strip_show),
DEFINE_MODULE_SYMBOL(led_strip_clear),
DEFINE_MODULE_SYMBOL(led_strip_get_length),
DEFINE_MODULE_SYMBOL(LED_STRIP_TYPE),
// drivers/paths
DEFINE_MODULE_SYMBOL(paths_get_data_path),
DEFINE_MODULE_SYMBOL(paths_get_temp_path),