mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-20 12:25:06 +00:00
Compare commits
2 Commits
5bac1ada82
...
34ca63ec3c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34ca63ec3c | ||
|
|
ffab897fe4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,5 +26,6 @@ sdkconfig.board.*.dev
|
|||||||
|
|
||||||
.caveman.json
|
.caveman.json
|
||||||
.ai/mcp
|
.ai/mcp
|
||||||
|
.clinerules/
|
||||||
|
|
||||||
__pycache__
|
__pycache__
|
||||||
6
Devices/lilygo-tembed-cc1101-plus/CMakeLists.txt
Normal file
6
Devices/lilygo-tembed-cc1101-plus/CMakeLists.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES source/*.c*)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
REQUIRES TactilityKernel Tactility
|
||||||
|
)
|
||||||
195
Devices/lilygo-tembed-cc1101-plus/LICENSE-Apache-2.0.md
Normal file
195
Devices/lilygo-tembed-cc1101-plus/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
Apache License
|
||||||
|
==============
|
||||||
|
|
||||||
|
_Version 2.0, January 2004_
|
||||||
|
_<<http://www.apache.org/licenses/>>_
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
28
Devices/lilygo-tembed-cc1101-plus/device.properties
Normal file
28
Devices/lilygo-tembed-cc1101-plus/device.properties
Normal 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
|
||||||
|
|
||||||
|
|
||||||
186
Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts
Normal file
186
Devices/lilygo-tembed-cc1101-plus/lilygo,tembed-cc1101-plus.dts
Normal 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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
11
Devices/lilygo-tembed-cc1101-plus/module.yaml
Normal file
11
Devices/lilygo-tembed-cc1101-plus/module.yaml
Normal 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
|
||||||
49
Devices/lilygo-tembed-cc1101-plus/source/module.cpp
Normal file
49
Devices/lilygo-tembed-cc1101-plus/source/module.cpp
Normal 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
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## 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
|
- CrashDiagnostics shouldn't show a QR when there's no callstack
|
||||||
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
|
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
|
||||||
- Add bold fonts for e-ink readability improvement
|
- Add bold fonts for e-ink readability improvement
|
||||||
@ -18,7 +18,7 @@
|
|||||||
- TCA9534 keyboards should use interrupts
|
- TCA9534 keyboards should use interrupts
|
||||||
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
|
- 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)
|
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")
|
- minitar/untarFile(): "entry->metadata.path" can escape its confined path (e.g. "../something")
|
||||||
|
|
||||||
## Medium Priority
|
## Medium Priority
|
||||||
|
|||||||
11
Drivers/esp32-led-strip-module/CMakeLists.txt
Normal file
11
Drivers/esp32-led-strip-module/CMakeLists.txt
Normal 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
|
||||||
|
)
|
||||||
195
Drivers/esp32-led-strip-module/LICENSE-Apache-2.0.md
Normal file
195
Drivers/esp32-led-strip-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
Apache License
|
||||||
|
==============
|
||||||
|
|
||||||
|
_Version 2.0, January 2004_
|
||||||
|
_<<http://www.apache.org/licenses/>>_
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
88
Drivers/esp32-led-strip-module/README.md
Normal file
88
Drivers/esp32-led-strip-module/README.md
Normal 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`.
|
||||||
@ -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
|
||||||
@ -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
|
||||||
122
Drivers/esp32-led-strip-module/include/drivers/esp32_led_strip.h
Normal file
122
Drivers/esp32-led-strip-module/include/drivers/esp32_led_strip.h
Normal 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
|
||||||
@ -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
|
||||||
99
Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt.h
Normal file
99
Drivers/esp32-led-strip-module/include/esp32_led_strip_rmt.h
Normal 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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
3
Drivers/esp32-led-strip-module/module.yaml
Normal file
3
Drivers/esp32-led-strip-module/module.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dependencies:
|
||||||
|
- TactilityKernel
|
||||||
|
bindings: bindings
|
||||||
258
Drivers/esp32-led-strip-module/source/esp32_led_strip.cpp
Normal file
258
Drivers/esp32-led-strip-module/source/esp32_led_strip.cpp
Normal 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"
|
||||||
170
Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_dev.c
Normal file
170
Drivers/esp32-led-strip-module/source/esp32_led_strip_rmt_dev.c
Normal 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;
|
||||||
|
}
|
||||||
@ -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(©_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;
|
||||||
|
}
|
||||||
23
Drivers/esp32-led-strip-module/source/module.cpp
Normal file
23
Drivers/esp32-led-strip-module/source/module.cpp
Normal 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"
|
||||||
@ -66,6 +66,10 @@ static error_t tdeck_keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
|||||||
data->key = internal->last_key;
|
data->key = internal->last_key;
|
||||||
data->pressed = false;
|
data->pressed = false;
|
||||||
} else if (read_buffer != 0) {
|
} 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->key = read_buffer;
|
||||||
data->pressed = true;
|
data->pressed = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -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
|
// esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure
|
||||||
free(runtime->file_data);
|
free(runtime->file_data);
|
||||||
delete runtime;
|
delete runtime;
|
||||||
LOG_E(TAG, "Failed to map elf");
|
LOG_E(TAG, "Failed to map elf. Possibly out of memory.");
|
||||||
return ERROR_RESOURCE;
|
return ERROR_RESOURCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
192
Modules/app-esp32-module/source/app_symbols.cpp
Normal file
192
Modules/app-esp32-module/source/app_symbols.cpp
Normal 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
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -11,12 +11,16 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
extern ServiceManifest loader_service_manifest;
|
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
|
// 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 app_esp32_symbol_resolver(const char* symbolName) {
|
||||||
uintptr_t symbol_address;
|
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 symbol_address;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@ -37,7 +41,7 @@ Module app_esp32_module = {
|
|||||||
.start = start,
|
.start = start,
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.drivers = nullptr,
|
.drivers = nullptr,
|
||||||
.symbols = nullptr,
|
.symbols = app_esp32_symbols,
|
||||||
.internal = nullptr,
|
.internal = nullptr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ extern "C" {
|
|||||||
enum AppEventType {
|
enum AppEventType {
|
||||||
APP_EVENT_RESULT, // struct AppResultEventData
|
APP_EVENT_RESULT, // struct AppResultEventData
|
||||||
APP_EVENT_CLOSE, // no data - terminate now, permanently
|
APP_EVENT_CLOSE, // no data - terminate now, permanently
|
||||||
|
APP_EVENT_SIGNAL, // struct AppSignalEventData, see app/signal.h
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Data for APP_EVENT_RESULT. */
|
/** Data for APP_EVENT_RESULT. */
|
||||||
@ -29,12 +30,22 @@ struct AppResultEventData {
|
|||||||
int32_t result;
|
int32_t result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Data for APP_EVENT_SIGNAL. */
|
||||||
|
struct AppSignalEventData {
|
||||||
|
/** A POSIX signal number, e.g. SIGHUP */
|
||||||
|
int sig;
|
||||||
|
};
|
||||||
|
|
||||||
struct AppEvent {
|
struct AppEvent {
|
||||||
enum AppEventType type;
|
enum AppEventType type;
|
||||||
/** Stamped by app_event_emit(); any value passed in by the caller is ignored. */
|
/** Stamped by app_event_emit(); any value passed in by the caller is ignored. */
|
||||||
uint64_t timestamp;
|
uint64_t timestamp;
|
||||||
|
union {
|
||||||
/** Valid only when type == APP_EVENT_RESULT. */
|
/** Valid only when type == APP_EVENT_RESULT. */
|
||||||
struct AppResultEventData result;
|
struct AppResultEventData result;
|
||||||
|
/** Valid only when type == APP_EVENT_SIGNAL. */
|
||||||
|
struct AppSignalEventData signal;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -48,20 +48,40 @@ bool app_libc_try_tcsetattr(int fd, const struct termios* t, int* out_result);
|
|||||||
typedef void (*AppLibcSignalHandler)(int sig);
|
typedef void (*AppLibcSignalHandler)(int sig);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records @a handler for @a sig in the calling app instance. Signals aren't delivered to apps yet,
|
* Records @a handler for @a sig in the calling app instance, called when a signal is delivered (see
|
||||||
* so the handler is never called. SIGKILL, SIGSTOP and out-of-range signals fail with EINVAL.
|
* 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
|
* @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);
|
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);
|
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);
|
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.
|
* 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);
|
bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result);
|
||||||
|
|
||||||
|
|||||||
25
Modules/app-module/include/app/memory.h
Normal file
25
Modules/app-module/include/app/memory.h
Normal 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
|
||||||
46
Modules/app-module/include/app/signal.h
Normal file
46
Modules/app-module/include/app/signal.h
Normal 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
|
||||||
@ -77,6 +77,9 @@ struct AppInstanceRecord {
|
|||||||
/** Handlers registered via signal() (app/libc.h), indexed by signal number. Null means SIG_DFL. */
|
/** Handlers registered via signal() (app/libc.h), indexed by signal number. Null means SIG_DFL. */
|
||||||
AppLibcSignalHandler signal_handlers[APP_LIBC_SIGNAL_COUNT] {};
|
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. */
|
/** Terminal input flags set via tcsetattr() (app/libc.h). ICRNL is applied when reading stdin. */
|
||||||
tcflag_t termios_iflag = ICRNL;
|
tcflag_t termios_iflag = ICRNL;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
#include <app/dir.h>
|
#include <app/dir.h>
|
||||||
#include <app/io.h>
|
#include <app/io.h>
|
||||||
|
#include <app/manager.h>
|
||||||
#include <app/private/ledger.h>
|
#include <app/private/ledger.h>
|
||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
|
#include <app/signal.h>
|
||||||
|
|
||||||
#include <tactility/delay.h>
|
#include <tactility/delay.h>
|
||||||
#include <tactility/paths.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.
|
// 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);
|
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
|
} // namespace
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@ -232,10 +256,81 @@ bool app_libc_try_kill(int pid, int sig, int* out_result) {
|
|||||||
if (app_scheduler_current_app_id() == 0) {
|
if (app_scheduler_current_app_id() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
errno = ENOSYS;
|
if (sig < 0 || sig >= APP_LIBC_SIGNAL_COUNT || sig == SIGKILL || sig == SIGSTOP) {
|
||||||
|
errno = EINVAL;
|
||||||
*out_result = -1;
|
*out_result = -1;
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Non-app fds in @a fds are checked with a zero timeout real poll() on every iteration.
|
// Non-app fds in @a fds are checked with a zero timeout real poll() on every iteration.
|
||||||
bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result) {
|
bool app_libc_try_poll(struct pollfd* fds, nfds_t nfds, int timeout, AppLibcPollFunction real_poll, int* out_result) {
|
||||||
@ -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 start = get_ticks();
|
||||||
const TickType_t timeout_ticks = (timeout < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout);
|
const TickType_t timeout_ticks = (timeout < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout);
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (app_signal_is_pending()) {
|
||||||
|
errno = EINTR;
|
||||||
|
*out_result = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
int ready = 0;
|
int ready = 0;
|
||||||
for (nfds_t i = 0; i < nfds; i++) {
|
for (nfds_t i = 0; i < nfds; i++) {
|
||||||
fds[i].revents = 0;
|
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;
|
remaining = POLL_INTERVAL_TICKS;
|
||||||
}
|
}
|
||||||
const struct pollfd& first = fds[first_app_index];
|
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) {
|
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) {
|
} 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 {
|
} else {
|
||||||
delay_ticks(remaining < POLL_INTERVAL_TICKS ? remaining : POLL_INTERVAL_TICKS);
|
delay_ticks(remaining < POLL_INTERVAL_TICKS ? remaining : POLL_INTERVAL_TICKS);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,11 @@
|
|||||||
#include <app/io.h>
|
#include <app/io.h>
|
||||||
#include <app/manager.h>
|
#include <app/manager.h>
|
||||||
#include <app/manifest.h>
|
#include <app/manifest.h>
|
||||||
|
#include <app/memory.h>
|
||||||
#include <app/package_manifest.h>
|
#include <app/package_manifest.h>
|
||||||
#include <app/paths.h>
|
#include <app/paths.h>
|
||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
|
#include <app/signal.h>
|
||||||
#include <app/start.h>
|
#include <app/start.h>
|
||||||
#include <app/stream.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_id_is_valid),
|
||||||
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
|
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
|
||||||
DEFINE_MODULE_SYMBOL(app_manifest_stack_size_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
|
// app/package_manifest
|
||||||
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
|
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
|
||||||
DEFINE_MODULE_SYMBOL(app_package_manifest_is_compatible),
|
DEFINE_MODULE_SYMBOL(app_package_manifest_is_compatible),
|
||||||
@ -86,6 +91,8 @@ static const ModuleSymbol SYMBOLS[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
|
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
|
||||||
// app/scheduler
|
// app/scheduler
|
||||||
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
|
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
|
||||||
|
// app/signal
|
||||||
|
DEFINE_MODULE_SYMBOL(app_signal_send),
|
||||||
// app/start
|
// app/start
|
||||||
DEFINE_MODULE_SYMBOL(app_start_context_for_manifest),
|
DEFINE_MODULE_SYMBOL(app_start_context_for_manifest),
|
||||||
DEFINE_MODULE_SYMBOL(app_start_context_for_location),
|
DEFINE_MODULE_SYMBOL(app_start_context_for_location),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
#include <app/instance.h>
|
#include <app/instance.h>
|
||||||
#include <app/loader.h>
|
#include <app/loader.h>
|
||||||
|
#include <app/memory.h>
|
||||||
#include <app/private/arguments.h>
|
#include <app/private/arguments.h>
|
||||||
#include <app/private/event.h>
|
#include <app/private/event.h>
|
||||||
#include <app/private/fd_table.h>
|
#include <app/private/fd_table.h>
|
||||||
@ -8,6 +9,7 @@
|
|||||||
#include <app/private/scheduler.h>
|
#include <app/private/scheduler.h>
|
||||||
#include <app/private/stream_internal.h>
|
#include <app/private/stream_internal.h>
|
||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
|
#include <app/signal.h>
|
||||||
#include <app/stream.h>
|
#include <app/stream.h>
|
||||||
|
|
||||||
#include <service/instance.h>
|
#include <service/instance.h>
|
||||||
@ -20,6 +22,7 @@
|
|||||||
#include <tactility/time.h>
|
#include <tactility/time.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <csignal>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <new>
|
#include <new>
|
||||||
@ -82,6 +85,9 @@ struct TaskContext {
|
|||||||
AppCompletionSignal* completion;
|
AppCompletionSignal* completion;
|
||||||
StackType_t* stackBuffer;
|
StackType_t* stackBuffer;
|
||||||
StaticTask_t* taskTcb;
|
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 {
|
struct ReaperContext {
|
||||||
@ -306,6 +312,10 @@ void finish_app_task(TaskContext* ctx, int32_t result, bool exiting) {
|
|||||||
ctx->loader->unload(ctx->runtime);
|
ctx->loader->unload(ctx->runtime);
|
||||||
#endif
|
#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);
|
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
|
// 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,
|
.completion = completion,
|
||||||
.stackBuffer = stack_buffer,
|
.stackBuffer = stack_buffer,
|
||||||
.taskTcb = task_tcb,
|
.taskTcb = task_tcb,
|
||||||
|
.allocCount = 0,
|
||||||
|
.allocBytes = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (context == nullptr) {
|
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);
|
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
|
||||||
if (completion != nullptr) {
|
if (completion != nullptr) {
|
||||||
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
|
// An app with an event subscription receives this as APP_EVENT_CLOSE, see app/signal.h
|
||||||
app_event_emit(app_instance_id, &event);
|
app_signal_send(app_instance_id, SIGTERM);
|
||||||
|
|
||||||
// Marked as soon as the app has been told to close, not once its task has actually
|
// 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
|
// 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();
|
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) {
|
void app_scheduler_exit_current(int32_t status) {
|
||||||
// Checked first: thread_local can't be read before the scheduler starts on ESP32
|
// Checked first: thread_local can't be read before the scheduler starts on ESP32
|
||||||
if (get_current_app_id() == 0) {
|
if (get_current_app_id() == 0) {
|
||||||
|
|||||||
125
Modules/app-module/source/signal.cpp
Normal file
125
Modules/app-module/source/signal.cpp
Normal 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"
|
||||||
@ -4,12 +4,18 @@
|
|||||||
#include <app/private/fd_table.h>
|
#include <app/private/fd_table.h>
|
||||||
#include <app/private/ledger.h>
|
#include <app/private/ledger.h>
|
||||||
#include <app/private/stream_internal.h>
|
#include <app/private/stream_internal.h>
|
||||||
|
#include <app/signal.h>
|
||||||
|
|
||||||
#include <tactility/concurrent/mutex.h>
|
#include <tactility/concurrent/mutex.h>
|
||||||
#include <tactility/error.h>
|
#include <tactility/error.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
|
||||||
namespace {
|
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.
|
// Caller must hold stream->mutex.
|
||||||
bool is_readable_locked(AppStream* stream) {
|
bool is_readable_locked(AppStream* stream) {
|
||||||
return stream->buffer.count > 0 || stream->closed;
|
return stream->buffer.count > 0 || stream->closed;
|
||||||
@ -44,11 +50,28 @@ private:
|
|||||||
AppStream* stream_;
|
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) {
|
ssize_t stream_file_read(void* object, void* buffer, size_t size) {
|
||||||
auto* stream = static_cast<AppStream*>(object);
|
auto* stream = static_cast<AppStream*>(object);
|
||||||
StreamOperationGuard guard(stream);
|
StreamOperationGuard guard(stream);
|
||||||
while (true) {
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
size_t read = app_stream_read(stream, buffer, size);
|
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) {
|
ssize_t stream_file_write(void* object, const void* buffer, size_t size) {
|
||||||
auto* stream = static_cast<AppStream*>(object);
|
auto* stream = static_cast<AppStream*>(object);
|
||||||
StreamOperationGuard guard(stream);
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
// app_stream_write() itself refuses to copy anything once closed (checked under the same
|
// app_stream_write() itself refuses to copy anything once closed (checked under the same
|
||||||
|
|||||||
19
Modules/app-posix-module/private/app_posix/malloc_wrap.h
Normal file
19
Modules/app-posix-module/private/app_posix/malloc_wrap.h
Normal 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
|
||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <termios.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.
|
// 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);
|
int __wrap_tcsetattr(int fd, int optional_actions, const struct termios* p);
|
||||||
AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler);
|
AppLibcSignalHandler __wrap_signal(int sig, AppLibcSignalHandler handler);
|
||||||
int __wrap_kill(pid_t pid, int sig);
|
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);
|
[[noreturn]] void __wrap_exit(int status);
|
||||||
|
|
||||||
int __wrap_vprintf(const char* format, va_list args);
|
int __wrap_vprintf(const char* format, va_list args);
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include <app_posix/malloc_wrap.h>
|
||||||
|
|
||||||
#include <app/elf_check.h>
|
#include <app/elf_check.h>
|
||||||
#include <app/loader.h>
|
#include <app/loader.h>
|
||||||
#include <app/location.h>
|
#include <app/location.h>
|
||||||
@ -11,6 +13,10 @@
|
|||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
#include <link.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <new>
|
#include <new>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@ -21,8 +27,52 @@ namespace {
|
|||||||
/** load()-allocated state, passed back through run()/unload(). */
|
/** load()-allocated state, passed back through run()/unload(). */
|
||||||
struct PosixAppRuntime {
|
struct PosixAppRuntime {
|
||||||
void* handle = nullptr;
|
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) {
|
bool is_regular_file(const std::string& path) {
|
||||||
struct stat path_stat {};
|
struct stat path_stat {};
|
||||||
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
|
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;
|
return ERROR_OUT_OF_MEMORY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
resolve_image_range(runtime);
|
||||||
|
#endif
|
||||||
|
|
||||||
*out_runtime = runtime;
|
*out_runtime = runtime;
|
||||||
return ERROR_NONE;
|
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);
|
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) {
|
void api_unload(AppRuntime runtime_ptr) {
|
||||||
|
|||||||
176
Modules/app-posix-module/source/malloc_wrap.cpp
Normal file
176
Modules/app-posix-module/source/malloc_wrap.cpp
Normal 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
|
||||||
@ -10,6 +10,7 @@
|
|||||||
#include <app/io.h>
|
#include <app/io.h>
|
||||||
#include <app/libc.h>
|
#include <app/libc.h>
|
||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
|
#include <app/signal.h>
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -83,18 +84,51 @@ int __real_kill(pid_t pid, int sig) {
|
|||||||
return real(pid, 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) {
|
[[noreturn]] void __real_exit(int status) {
|
||||||
static auto real = reinterpret_cast<void (*)(int)>(dlsym(RTLD_NEXT, "exit"));
|
static auto real = reinterpret_cast<void (*)(int)>(dlsym(RTLD_NEXT, "exit"));
|
||||||
real(status);
|
real(status);
|
||||||
__builtin_unreachable();
|
__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) {
|
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) {
|
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) {
|
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) {
|
int __wrap_poll(struct pollfd* fds, nfds_t nfds, int timeout) {
|
||||||
|
app_signal_deliver_pending();
|
||||||
int result;
|
int result;
|
||||||
if (app_libc_try_poll(fds, nfds, timeout, __real_poll, &result)) {
|
if (app_libc_try_poll(fds, nfds, timeout, __real_poll, &result)) {
|
||||||
|
if (result < 0 && errno == EINTR) {
|
||||||
|
app_signal_deliver_pending();
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return __real_poll(fds, nfds, timeout);
|
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 __wrap_kill(pid_t pid, int sig) {
|
||||||
int result;
|
int result;
|
||||||
if (app_libc_try_kill(pid, sig, &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 result;
|
||||||
}
|
}
|
||||||
return __real_kill(pid, sig);
|
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
|
// Called by an app, the real exit() would end the whole simulator
|
||||||
void __wrap_exit(int status) {
|
void __wrap_exit(int status) {
|
||||||
app_scheduler_exit_current(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 */
|
/** @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) {
|
size_t writeAllTo(int fd, const void* data, size_t size) {
|
||||||
|
app_signal_deliver_pending();
|
||||||
const auto* bytes = static_cast<const char*>(data);
|
const auto* bytes = static_cast<const char*>(data);
|
||||||
size_t remaining = size;
|
size_t remaining = size;
|
||||||
while (remaining > 0) {
|
while (remaining > 0) {
|
||||||
ssize_t written = app_io_write(fd, bytes, remaining);
|
ssize_t written = app_io_write(fd, bytes, remaining);
|
||||||
if (written <= 0) {
|
if (written <= 0) {
|
||||||
|
if (written < 0 && errno == EINTR) {
|
||||||
|
app_signal_deliver_pending();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
bytes += written;
|
bytes += written;
|
||||||
@ -274,7 +354,7 @@ int formatTo(int fd, const char* format, va_list args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int readOneFromStdin(char& out) {
|
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
|
// The process' own streams, captured before anything can reassign stdin/stdout/stderr. A caller
|
||||||
|
|||||||
@ -27,6 +27,10 @@ TT_DYLD_INTERPOSE(__wrap_tcsetattr, tcsetattr)
|
|||||||
TT_DYLD_INTERPOSE(__wrap_exit, exit)
|
TT_DYLD_INTERPOSE(__wrap_exit, exit)
|
||||||
TT_DYLD_INTERPOSE(__wrap_signal, signal)
|
TT_DYLD_INTERPOSE(__wrap_signal, signal)
|
||||||
TT_DYLD_INTERPOSE(__wrap_kill, kill)
|
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_vprintf, vprintf)
|
||||||
TT_DYLD_INTERPOSE(__wrap_printf, printf)
|
TT_DYLD_INTERPOSE(__wrap_printf, printf)
|
||||||
|
|||||||
@ -59,6 +59,22 @@ int kill(pid_t pid, int sig) {
|
|||||||
return __wrap_kill(pid, 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) {
|
void exit(int status) {
|
||||||
__wrap_exit(status);
|
__wrap_exit(status);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <app/loader.h>
|
#include <app/loader.h>
|
||||||
#include <app/manager.h>
|
#include <app/manager.h>
|
||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
|
#include <app/signal.h>
|
||||||
#include <app/start.h>
|
#include <app/start.h>
|
||||||
#include <app/stream.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_first_default { false };
|
||||||
std::atomic<bool> g_signal_returns_previous { false };
|
std::atomic<bool> g_signal_returns_previous { false };
|
||||||
std::atomic<bool> g_signal_sigkill_rejected { 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*[]) {
|
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_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_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);
|
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()
|
// 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;
|
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_first_default.store(false, std::memory_order_relaxed);
|
||||||
g_signal_returns_previous.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_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) } };
|
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);
|
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_first_default.load(std::memory_order_acquire));
|
||||||
CHECK(g_signal_returns_previous.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_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");
|
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_stop(parent_id);
|
||||||
app_manager_remove("test.app.execute.printf_parent");
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -114,6 +114,7 @@ shared_symbol_code_point_names = [
|
|||||||
"terminal",
|
"terminal",
|
||||||
"logo_dev",
|
"logo_dev",
|
||||||
"camera",
|
"camera",
|
||||||
|
"lightstrip",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
|
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
|
||||||
|
|||||||
@ -48,3 +48,4 @@
|
|||||||
#define LVGL_ICON_SHARED_TERMINAL "\xEE\xAE\x8E"
|
#define LVGL_ICON_SHARED_TERMINAL "\xEE\xAE\x8E"
|
||||||
#define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96"
|
#define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96"
|
||||||
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
|
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
|
||||||
|
#define LVGL_ICON_SHARED_LIGHTSTRIP "\xF3\xBF\xBD\xB5"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Size: 12 px
|
* Size: 12 px
|
||||||
* Bpp: 2
|
* 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
|
#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,
|
0x0, 0x80, 0x0, 0x9, 0x0, 0x70, 0x91, 0xcd,
|
||||||
0x9, 0xd, 0xc0, 0x90, 0xac, 0x4, 0x6, 0xd0,
|
0x9, 0xd, 0xc0, 0x90, 0xac, 0x4, 0x6, 0xd0,
|
||||||
0x0, 0x97, 0x0, 0x1c, 0x2c, 0x7, 0x0, 0x7f,
|
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 = 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 = 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 = 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,
|
.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
|
.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,
|
.cmaps = cmaps,
|
||||||
.kern_dsc = NULL,
|
.kern_dsc = NULL,
|
||||||
.kern_scale = 0,
|
.kern_scale = 0,
|
||||||
.cmap_num = 1,
|
.cmap_num = 2,
|
||||||
.bpp = 2,
|
.bpp = 2,
|
||||||
.kern_classes = 0,
|
.kern_classes = 0,
|
||||||
.bitmap_format = 0,
|
.bitmap_format = 0,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Size: 16 px
|
* Size: 16 px
|
||||||
* Bpp: 2
|
* 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
|
#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,
|
0xca, 0x0, 0x80, 0xc, 0xa0, 0x0, 0x0, 0xc7,
|
||||||
0x0, 0x0, 0x2c, 0x38, 0x0, 0x3, 0x40, 0xe0,
|
0x0, 0x0, 0x2c, 0x38, 0x0, 0x3, 0x40, 0xe0,
|
||||||
0x1, 0xe0, 0x3, 0xea, 0xf8, 0x0, 0x6, 0xf8,
|
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 = 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 = 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 = 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,
|
.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
|
.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,
|
.cmaps = cmaps,
|
||||||
.kern_dsc = NULL,
|
.kern_dsc = NULL,
|
||||||
.kern_scale = 0,
|
.kern_scale = 0,
|
||||||
.cmap_num = 1,
|
.cmap_num = 2,
|
||||||
.bpp = 2,
|
.bpp = 2,
|
||||||
.kern_classes = 0,
|
.kern_classes = 0,
|
||||||
.bitmap_format = 0,
|
.bitmap_format = 0,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Size: 20 px
|
* Size: 20 px
|
||||||
* Bpp: 2
|
* 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
|
#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,
|
0x0, 0x7, 0x80, 0xf4, 0x0, 0x0, 0xf0, 0x3,
|
||||||
0xe0, 0x0, 0x7c, 0x0, 0xb, 0xea, 0xbf, 0x0,
|
0xe0, 0x0, 0x7c, 0x0, 0xb, 0xea, 0xbf, 0x0,
|
||||||
0x0, 0x1b, 0xff, 0x40, 0x0, 0x0, 0x0, 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
|
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 = 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 = 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 = 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,
|
.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
|
.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,
|
.cmaps = cmaps,
|
||||||
.kern_dsc = NULL,
|
.kern_dsc = NULL,
|
||||||
.kern_scale = 0,
|
.kern_scale = 0,
|
||||||
.cmap_num = 1,
|
.cmap_num = 2,
|
||||||
.bpp = 2,
|
.bpp = 2,
|
||||||
.kern_classes = 0,
|
.kern_classes = 0,
|
||||||
.bitmap_format = 0,
|
.bitmap_format = 0,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Size: 24 px
|
* Size: 24 px
|
||||||
* Bpp: 2
|
* 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
|
#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,
|
0x0, 0x0, 0x7c, 0x1f, 0x0, 0x0, 0x0, 0xf4,
|
||||||
0xb, 0xd0, 0x0, 0x7, 0xe0, 0x2, 0xfd, 0x0,
|
0xb, 0xd0, 0x0, 0x7, 0xe0, 0x2, 0xfd, 0x0,
|
||||||
0x7f, 0x80, 0x0, 0x7f, 0xff, 0xfd, 0x0, 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 = 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 = 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 = 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,
|
.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
|
.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,
|
.cmaps = cmaps,
|
||||||
.kern_dsc = NULL,
|
.kern_dsc = NULL,
|
||||||
.kern_scale = 0,
|
.kern_scale = 0,
|
||||||
.cmap_num = 1,
|
.cmap_num = 2,
|
||||||
.bpp = 2,
|
.bpp = 2,
|
||||||
.kern_classes = 0,
|
.kern_classes = 0,
|
||||||
.bitmap_format = 0,
|
.bitmap_format = 0,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* Size: 32 px
|
* Size: 32 px
|
||||||
* Bpp: 2
|
* 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
|
#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,
|
0x0, 0xb, 0xff, 0xaa, 0xbf, 0xf4, 0x0, 0x0,
|
||||||
0x1, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
|
0x1, 0xff, 0xff, 0xff, 0x80, 0x0, 0x0, 0x0,
|
||||||
0x1b, 0xff, 0xe4, 0x0, 0x0, 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
|
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 = 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 = 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 = 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,
|
.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
|
.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,
|
.cmaps = cmaps,
|
||||||
.kern_dsc = NULL,
|
.kern_dsc = NULL,
|
||||||
.kern_scale = 0,
|
.kern_scale = 0,
|
||||||
.cmap_num = 1,
|
.cmap_num = 2,
|
||||||
.bpp = 2,
|
.bpp = 2,
|
||||||
.kern_classes = 0,
|
.kern_classes = 0,
|
||||||
.bitmap_format = 0,
|
.bitmap_format = 0,
|
||||||
|
|||||||
@ -59,6 +59,7 @@ typedef struct {
|
|||||||
lv_obj_t* close_button_image;
|
lv_obj_t* close_button_image;
|
||||||
lv_obj_t* action_container;
|
lv_obj_t* action_container;
|
||||||
uint8_t action_count;
|
uint8_t action_count;
|
||||||
|
lv_event_cb_t nav_action_callback;
|
||||||
} Toolbar;
|
} Toolbar;
|
||||||
|
|
||||||
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
|
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);
|
lv_obj_set_height(obj, toolbar_height);
|
||||||
|
|
||||||
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
|
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
|
||||||
|
toolbar->nav_action_callback = nullptr;
|
||||||
lv_obj_set_width(obj, LV_PCT(100));
|
lv_obj_set_width(obj, LV_PCT(100));
|
||||||
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
|
||||||
lv_obj_set_style_pad_column(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) {
|
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);
|
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
|
||||||
|
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_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
|
lv_image_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
Tactility/Include/Tactility/settings/LedStripSettings.h
Normal file
34
Tactility/Include/Tactility/settings/LedStripSettings.h
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
@ -21,6 +21,12 @@ public:
|
|||||||
/** @return true if onKey returned true for any key read */
|
/** @return true if onKey returned true for any key read */
|
||||||
bool pump(KeyHandler onKey);
|
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:
|
private:
|
||||||
static constexpr uint32_t REFRESH_INTERVAL_MS = 1000;
|
static constexpr uint32_t REFRESH_INTERVAL_MS = 1000;
|
||||||
|
|
||||||
@ -28,7 +34,9 @@ private:
|
|||||||
|
|
||||||
void refresh();
|
void refresh();
|
||||||
void rescan();
|
void rescan();
|
||||||
|
void applyExclusive();
|
||||||
|
|
||||||
std::vector<Device*> devices_;
|
std::vector<Device*> devices_;
|
||||||
TickType_t lastRefresh_ = 0;
|
TickType_t lastRefresh_ = 0;
|
||||||
|
bool exclusive_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
struct Device;
|
struct Device;
|
||||||
|
class TerminalRenderer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the terminal until the user exits. Must be called after module_stop(&lvgl_module), since it
|
* Runs the terminal until the user exits. Unless the renderer is a TerminalRendererLvgl, must be
|
||||||
* drives the display and keyboards directly. Tracks keyboards and the touch-to-exit gesture itself.
|
* 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);
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <tactility/drivers/display.h>
|
||||||
|
|
||||||
struct Device;
|
struct Device;
|
||||||
struct PixelBuffer;
|
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
|
* 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.
|
* 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 {
|
class TerminalRenderer {
|
||||||
public:
|
public:
|
||||||
@ -40,6 +43,9 @@ public:
|
|||||||
int columns() const { return cols; }
|
int columns() const { return cols; }
|
||||||
int rows() const { return rowCount; }
|
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:
|
protected:
|
||||||
void paintCell(int row, int col, char ch, uint8_t attr);
|
void paintCell(int row, int col, char ch, uint8_t attr);
|
||||||
void paintCursor(int row, int col);
|
void paintCursor(int row, int col);
|
||||||
@ -57,6 +63,9 @@ protected:
|
|||||||
*/
|
*/
|
||||||
bool allocateCommon(Device* display);
|
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. */
|
/** Frees frameBuffer, fullFrameBuffer, shadow, and the borrowed hw double buffer, if any. */
|
||||||
void freeCommon();
|
void freeCommon();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -91,6 +91,7 @@
|
|||||||
extern "C" Module tactility_audio_module;
|
extern "C" Module tactility_audio_module;
|
||||||
#include <tactility/drivers/grove.h>
|
#include <tactility/drivers/grove.h>
|
||||||
#include <tactility/drivers/imu.h>
|
#include <tactility/drivers/imu.h>
|
||||||
|
#include <tactility/drivers/led_strip.h>
|
||||||
#include <tactility/drivers/power_supply.h>
|
#include <tactility/drivers/power_supply.h>
|
||||||
#include <tactility/drivers/rtc.h>
|
#include <tactility/drivers/rtc.h>
|
||||||
#include <tactility/drivers/trackball.h>
|
#include <tactility/drivers/trackball.h>
|
||||||
@ -176,6 +177,7 @@ namespace app {
|
|||||||
namespace imageviewer { extern const ::AppManifest manifest; }
|
namespace imageviewer { extern const ::AppManifest manifest; }
|
||||||
namespace inputdialog { extern const ::AppManifest manifest; }
|
namespace inputdialog { extern const ::AppManifest manifest; }
|
||||||
namespace launcher { extern const ::AppManifest manifest; }
|
namespace launcher { extern const ::AppManifest manifest; }
|
||||||
|
namespace ledstripsettings { extern const ::AppManifest manifest; }
|
||||||
namespace localesettings { extern const ::AppManifest manifest; }
|
namespace localesettings { extern const ::AppManifest manifest; }
|
||||||
namespace notes { extern const ::AppManifest manifest; }
|
namespace notes { extern const ::AppManifest manifest; }
|
||||||
namespace power { extern const ::AppManifest manifest; }
|
namespace power { extern const ::AppManifest manifest; }
|
||||||
@ -237,6 +239,9 @@ static void registerInternalApps() {
|
|||||||
if (device_exists_of_type(&DISPLAY_TYPE)) {
|
if (device_exists_of_type(&DISPLAY_TYPE)) {
|
||||||
app_manager_add(&app::display::manifest);
|
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::files::manifest);
|
||||||
app_manager_add(&app::fileselection::manifest);
|
app_manager_add(&app::fileselection::manifest);
|
||||||
app_manager_add(&app::i2cscanner::manifest);
|
app_manager_add(&app::i2cscanner::manifest);
|
||||||
|
|||||||
474
Tactility/Source/app/ledstripsettings/LedStripSettings.cpp
Normal file
474
Tactility/Source/app/ledstripsettings/LedStripSettings.cpp
Normal 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 = {}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -43,6 +43,7 @@ constexpr IconEntry ICONS[] = {
|
|||||||
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
|
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
|
||||||
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
|
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
|
||||||
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
||||||
|
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
||||||
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
||||||
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
||||||
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include <app/io.h>
|
#include <app/io.h>
|
||||||
#include <app/manager.h>
|
#include <app/manager.h>
|
||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
|
#include <app/signal.h>
|
||||||
#include <app/start.h>
|
#include <app/start.h>
|
||||||
#include <app/stream.h>
|
#include <app/stream.h>
|
||||||
|
|
||||||
@ -19,6 +20,7 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <csignal>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -215,6 +217,8 @@ int runApp(AppStartContext& context) {
|
|||||||
} else {
|
} else {
|
||||||
// Our own stdin hung up (touch-to-exit): propagate to the child the same way.
|
// Our own stdin hung up (touch-to-exit): propagate to the child the same way.
|
||||||
app_stream_close(&stdinStream);
|
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;
|
ownStdinClosed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
#include <Tactility/app/terminal/KeyboardInput.h>
|
#include <Tactility/app/terminal/KeyboardInput.h>
|
||||||
|
|
||||||
|
#include <lvgl/devices/keyboard.h>
|
||||||
|
#include <lvgl/lvgl.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
constexpr auto* TAG = "terminal-keyb";
|
constexpr auto* TAG = "terminal-keyb";
|
||||||
@ -9,6 +12,7 @@ KeyboardInput::KeyboardInput() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyboardInput::~KeyboardInput() {
|
KeyboardInput::~KeyboardInput() {
|
||||||
|
setExclusive(false);
|
||||||
for (Device* device : devices_) {
|
for (Device* device : devices_) {
|
||||||
device_put(device);
|
device_put(device);
|
||||||
}
|
}
|
||||||
@ -39,6 +43,28 @@ bool KeyboardInput::pump(KeyHandler onKey) {
|
|||||||
return handled;
|
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) {
|
bool KeyboardInput::collect(Device* device, void* context) {
|
||||||
static_cast<std::vector<Device*>*>(context)->push_back(device);
|
static_cast<std::vector<Device*>*>(context)->push_back(device);
|
||||||
return true;
|
return true;
|
||||||
@ -76,4 +102,9 @@ void KeyboardInput::rescan() {
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A keyboard connected since the last scan gets an enabled LVGL indev of its own.
|
||||||
|
if (exclusive_) {
|
||||||
|
applyExclusive();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,6 +119,8 @@ void runShell(int columns, int rows, volatile bool* stopRequested, TaskHandle_t
|
|||||||
while (app_event_poll(&eventSub, &event) == ERROR_NONE) {
|
while (app_event_poll(&eventSub, &event) == ERROR_NONE) {
|
||||||
if (event.type == APP_EVENT_RESULT && event.result.launch_id == shellId) {
|
if (event.type == APP_EVENT_RESULT && event.result.launch_id == shellId) {
|
||||||
shellDone = true;
|
shellDone = true;
|
||||||
|
} else if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
*stopRequested = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@
|
|||||||
#include <Tactility/app/terminal/Shell.h>
|
#include <Tactility/app/terminal/Shell.h>
|
||||||
#include <Tactility/app/terminal/Terminal.h>
|
#include <Tactility/app/terminal/Terminal.h>
|
||||||
#include <Tactility/app/terminal/TerminalRenderer.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/app/terminal/TouchInput.h>
|
||||||
|
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
@ -46,7 +44,7 @@ constexpr int SCROLL_STEP_LINES = 5;
|
|||||||
|
|
||||||
namespace {
|
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). */
|
* pump loop, which closes the shell app's stdin to unstick it (see that loop's own comment). */
|
||||||
volatile bool stopRequested = false;
|
volatile bool stopRequested = false;
|
||||||
|
|
||||||
@ -160,12 +158,16 @@ void ioTask(void* arg) {
|
|||||||
auto* params = static_cast<IoTaskParams*>(arg);
|
auto* params = static_cast<IoTaskParams*>(arg);
|
||||||
|
|
||||||
while (!shellFinished) {
|
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
|
// A scroll replaces every row at once, so the renderer is told to repaint rather than rely
|
||||||
// on its per-cell comparison.
|
// on its per-cell comparison.
|
||||||
const bool viewMoved = params->keyboards->pump(handleKey);
|
const bool viewMoved = shown && params->keyboards->pump(handleKey);
|
||||||
params->renderer->render(viewMoved);
|
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");
|
LOG_I(TAG, "Touch detected - stopping");
|
||||||
stopRequested = true;
|
stopRequested = true;
|
||||||
}
|
}
|
||||||
@ -183,7 +185,7 @@ void ioTask(void* arg) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void runTerminal(Device* display) {
|
void runTerminal(Device* display, TerminalRenderer& renderer, bool touchToExit) {
|
||||||
stopRequested = false;
|
stopRequested = false;
|
||||||
shellFinished = false;
|
shellFinished = false;
|
||||||
|
|
||||||
@ -195,11 +197,6 @@ void runTerminal(Device* display) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalRendererPpa ppaRenderer;
|
|
||||||
TerminalRendererGeneric genericRenderer;
|
|
||||||
TerminalRenderer& renderer = TerminalRendererPpa::isSupported()
|
|
||||||
? static_cast<TerminalRenderer&>(ppaRenderer)
|
|
||||||
: static_cast<TerminalRenderer&>(genericRenderer);
|
|
||||||
if (!renderer.begin(display)) {
|
if (!renderer.begin(display)) {
|
||||||
LOG_E(TAG, "Renderer failed to start");
|
LOG_E(TAG, "Renderer failed to start");
|
||||||
vterm_deinit();
|
vterm_deinit();
|
||||||
@ -221,7 +218,7 @@ void runTerminal(Device* display) {
|
|||||||
// working while this one is blocked draining the shell app. See ioTask().
|
// working while this one is blocked draining the shell app. See ioTask().
|
||||||
IoTaskParams ioParams {
|
IoTaskParams ioParams {
|
||||||
.keyboards = &keyboards,
|
.keyboards = &keyboards,
|
||||||
.touch = &touch,
|
.touch = touchToExit ? &touch : nullptr,
|
||||||
.renderer = &renderer,
|
.renderer = &renderer,
|
||||||
.doneSem = xSemaphoreCreateBinary(),
|
.doneSem = xSemaphoreCreateBinary(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -41,6 +41,10 @@ inline uint16_t paletteColour(uint8_t index) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool TerminalRenderer::allocateCommon(Device* displayDevice) {
|
bool TerminalRenderer::allocateCommon(Device* displayDevice) {
|
||||||
|
return allocateCommon(displayDevice, display_get_color_format(displayDevice));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TerminalRenderer::allocateCommon(Device* displayDevice, enum DisplayColorFormat frameColorFormat) {
|
||||||
display = displayDevice;
|
display = displayDevice;
|
||||||
monochrome = display_get_color_format(display) == DISPLAY_COLOR_FORMAT_MONOCHROME;
|
monochrome = display_get_color_format(display) == DISPLAY_COLOR_FORMAT_MONOCHROME;
|
||||||
|
|
||||||
@ -65,8 +69,7 @@ bool TerminalRenderer::allocateCommon(Device* displayDevice) {
|
|||||||
originX = (frameWidth - cols * cellWidth) / 2;
|
originX = (frameWidth - cols * cellWidth) / 2;
|
||||||
originY = (frameHeight - rowCount * cellHeight) / 2;
|
originY = (frameHeight - rowCount * cellHeight) / 2;
|
||||||
|
|
||||||
const enum DisplayColorFormat colorFormat = display_get_color_format(display);
|
frameBuffer = pixel_buffer_create(frameColorFormat, frameWidth, cellHeight);
|
||||||
frameBuffer = pixel_buffer_create(colorFormat, frameWidth, cellHeight);
|
|
||||||
if (frameBuffer == nullptr) {
|
if (frameBuffer == nullptr) {
|
||||||
LOG_E(TAG, "Failed to allocate frame buffer");
|
LOG_E(TAG, "Failed to allocate frame buffer");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
91
Tactility/Source/app/terminal/TerminalRendererLvgl.cpp
Normal file
91
Tactility/Source/app/terminal/TerminalRendererLvgl.cpp
Normal 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();
|
||||||
|
}
|
||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
|
|
||||||
#include <Tactility/app/terminal/Terminal.h>
|
#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>
|
#include <tactility/log.h>
|
||||||
|
|
||||||
@ -14,11 +17,88 @@
|
|||||||
#include <tactility/drivers/display.h>
|
#include <tactility/drivers/display.h>
|
||||||
#include <tactility/module.h>
|
#include <tactility/module.h>
|
||||||
#include <lvgl/module.h>
|
#include <lvgl/module.h>
|
||||||
|
#include <lvgl/widgets/toolbar.h>
|
||||||
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
constexpr auto* TAG = "terminal";
|
constexpr auto* TAG = "terminal";
|
||||||
|
|
||||||
namespace tt::app::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[]) {
|
int main(int argc, char* argv[]) {
|
||||||
Device* display_device = nullptr;
|
Device* display_device = nullptr;
|
||||||
if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop LVGL, because terminal has custom rendering
|
// The canvas holds a full frame, which needs external RAM on larger displays.
|
||||||
auto lvgl_active = module_is_started(&lvgl_module);
|
const bool useLvgl = module_is_started(&lvgl_module) &&
|
||||||
if (lvgl_active) {
|
memory_external_total() > 0 &&
|
||||||
module_stop(&lvgl_module);
|
!hasArgument(argc, argv, "--no-lvgl");
|
||||||
}
|
|
||||||
|
|
||||||
runTerminal(display_device);
|
if (useLvgl) {
|
||||||
|
runInWindow(display_device);
|
||||||
|
} else {
|
||||||
|
runFullscreen(display_device);
|
||||||
|
}
|
||||||
|
|
||||||
device_put(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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
194
Tactility/Source/settings/LedStripSettings.cpp
Normal file
194
Tactility/Source/settings/LedStripSettings.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
110
TactilityKernel/include/tactility/drivers/led_strip.h
Normal file
110
TactilityKernel/include/tactility/drivers/led_strip.h
Normal 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
|
||||||
67
TactilityKernel/source/drivers/led_strip.cpp
Normal file
67
TactilityKernel/source/drivers/led_strip.cpp
Normal 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"
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -25,6 +25,7 @@
|
|||||||
#include <tactility/drivers/i8080_controller.h>
|
#include <tactility/drivers/i8080_controller.h>
|
||||||
#include <tactility/drivers/imu.h>
|
#include <tactility/drivers/imu.h>
|
||||||
#include <tactility/drivers/keyboard.h>
|
#include <tactility/drivers/keyboard.h>
|
||||||
|
#include <tactility/drivers/led_strip.h>
|
||||||
#include <tactility/drivers/lora.h>
|
#include <tactility/drivers/lora.h>
|
||||||
#include <tactility/drivers/pointer.h>
|
#include <tactility/drivers/pointer.h>
|
||||||
#include <tactility/drivers/power_supply.h>
|
#include <tactility/drivers/power_supply.h>
|
||||||
@ -257,6 +258,18 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
|||||||
// drivers/keyboard
|
// drivers/keyboard
|
||||||
DEFINE_MODULE_SYMBOL(keyboard_read_key),
|
DEFINE_MODULE_SYMBOL(keyboard_read_key),
|
||||||
DEFINE_MODULE_SYMBOL(KEYBOARD_TYPE),
|
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
|
// drivers/paths
|
||||||
DEFINE_MODULE_SYMBOL(paths_get_data_path),
|
DEFINE_MODULE_SYMBOL(paths_get_data_path),
|
||||||
DEFINE_MODULE_SYMBOL(paths_get_temp_path),
|
DEFINE_MODULE_SYMBOL(paths_get_temp_path),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user