├── .github └── workflows │ ├── configure_test_env.sh │ └── makefile_test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── dev_scripts ├── debug-release-in-host.sh ├── make-all-package.sh ├── run-container.sh ├── setup-env.sh ├── setup-host.sh └── setup-toolchain.sh ├── github ├── led_controller_main.gif ├── readme.txt └── start_menu.png ├── makefile └── workspace ├── assets ├── images │ ├── background.png │ ├── brick_sprite_sheet.png │ ├── icon.png │ └── main_menu.png └── retro_gaming.ttf ├── config_files ├── config.json └── settings.ini ├── include ├── led_controller.h ├── led_controller_common.h └── sdl_base.h ├── scripts ├── launch │ └── launch.sh └── runtime │ ├── install.sh │ ├── turn_off_all_leds.sh │ ├── turn_on_all_leds.sh │ └── uninstall.sh ├── service ├── led-settings-daemon ├── led_controller_low_battery_led.sh └── settings-daemon.sh └── src ├── led_controller.c ├── led_controller_common.c └── sdl_base.c /.github/workflows/configure_test_env.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/.github/workflows/configure_test_env.sh -------------------------------------------------------------------------------- /.github/workflows/makefile_test.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ "**" ] 6 | pull_request: 7 | branches: [ "**" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | matrix: 15 | arch: [amd64, arm64] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: configure test environment 21 | run: sudo make deps 22 | 23 | - name: Install dependencies 24 | run: make 25 | 26 | - name: Make project 27 | run: make 28 | 29 | - name: Package project 30 | run: make package -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | release 2 | build 3 | toolchains/* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "toolchains/arm64-tg3040-toolchain"] 2 | path = toolchains/arm64-tg3040-toolchain 3 | url = https://github.com/pierceTee/arm64-tg3040-toolchain.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TrimUI LED Controller 2 | 3 | ### An application to configure the LEDs on the TrimUI Brick (smart pro support WIP). 4 | #### *Configure all LED brightness, color, and effect!* 5 | 6 | ![Configure all LED brightness, color, and effect!](./github/led_controller_main.gif) 7 | 8 | ## Installation 9 | 10 | This application is designed for the TrimUI smart Brick emulation handheld. There has been no testing of operational safety on any other hardware. 11 | 12 | - **Stock OS:** Place the `LedController.pak` folder in the `Apps` folder of your SDCARD and launch from the `Apps` menu. 13 | - **MinUI:** Place the `LedController.pak` folder in the `Tools/tg3040` folder of your SDCARD and launch from the `Tools` menu. 14 | 15 | ## Usage Guide 16 | 17 | ### Controls 18 | 19 | **From the configuration menu:** 20 | 21 | - **DPAD UP/DOWN:** Change the selected setting. 22 | - **DPAD L/R:** Change the value of the selected setting. 23 | - **START:** Access the start menu. 24 | - **B/POWER:** Exit the application. 25 | - **L/R:** Switch the active LED. 26 | 27 | **From the start menu:** 28 | 29 | - **DPAD UP/DOWN:** Change the selected setting. 30 | - **B/START:** Return to config page. 31 | - **A:** Execute selected setting. 32 | 33 | ### Menu Options 34 | 35 | - **Sync LED colors:** Match all LED colors to the currently selected LED. 36 | - **Turn ON all LEDs:** Enable all LEDs by setting their brightness to the maximum value. 37 | - **Turn OFF all LEDs:** Disable all LEDs by setting their brightness to 0. 38 | - **Enable/Disable extended colors:** Toggle the ability to select colors from an extended range of RGB values. 39 | - **Enable/Disable low battery warning:** Toggle the default OS behavior of flashing LEDs when the battery falls below 15%. 40 | - **Uninstall:** Uninstall all services and system files, turning off the LEDs on exit. 41 | 42 | 43 | ## Development Guide 44 | 45 | All development work has been performed in a Docker container from [arm64-tg3040-toolchain](https://github.com/pierceTee/arm64-tg3040-toolchain.git) running on a Windows host machine. Theoretically, this should be cross-platform, but practical applicability is left to the reader. 46 | 47 | If you want everything handled for you, run `dev_scripts/make-all-packages` from the root of the project directory, then navigate to the corresponding release folder. 48 | 49 | ### To Build Manually: 50 | 51 | 1. Pull submodules and build the `toolchains/arm64-tg3040-toolchain` Docker container. 52 | 2. Run `dev_scripts/run-container.sh` to enter into the `arm64-tg3040-toolchain` image. 53 | 3. Run `make` to build the project, then `make package` to package it into the `release/aarch64` folder. 54 | 4. To test the application: 55 | - **On your host machine:** Select a project from `release//` (e.g., `release//LedController`) and run the application from the command line. 56 | - **On your device:** 57 | - **MinUI:** 58 | 1. Copy the project release `release/aarch64/LedControl.pak` to `/Tools/tg3040/`. 59 | 2. On the device, navigate to `Tools`, find your `LedController` entry, and launch. 60 | - **Stock OS:** 61 | 1. Copy the project release `release/aarch64/LedController` to your `/Apps/`. 62 | 2. On the device, navigate to your `Led Controller` entry in the "apps" section and launch. 63 | 64 | Although alternatives are easily attainable, the project is structured with the idea that the user will compile in the Docker container found in toolchains. From this container, you can run the application as if it were on the TrimUI device given you've connected your host display to the container (see `dev_scripts/run-container.sh`). You can use a tool like `gdb` to debug the application from your host machine by navigating to your host machine's architecture release directory and manually launching with your desired debugger. Just be aware that while all versions of the app *should* behave the same, this isn't guaranteed to be the same behavior you see on your TrimUI device. 65 | 66 | ## Troubleshooting 67 | 68 | - **Dependency issues:** Try running `make deps`. 69 | - **Compiler issues:** Try running `dev_scripts/setup-toolchain.sh` followed by `dev_scripts/setup-env.sh`. 70 | - **Docker issues:** Try running `./toolchains/arm64-tg3040-toolchain/make shell` then `./dev_scripts/run-container.sh`. 71 | 72 | ## References/Findings 73 | 74 | All logic is derived from the help file found in `/sys/class/led_anim/` 75 | 76 | ``` 77 | [TRIMUI LED Animation driver] 78 | max_scale : maximum LED brightness in dec [0 ~ tg5040 limit brightness 60] 79 | frame : raw frames for total 23 XRGB 32bpp data 80 | frame_hex : frames for total 23 XRGB 32bpp data in hex format "RRGGBB RRGGBB RRGGBB ... RRGGBB " end with space. 81 | 82 | [usage of anim to function] 83 | effect_lr: Left and right joystick LEDS effect type. 84 | effect_l: Left joystick LEDS effect type. 85 | effect_r: Right joystick LEDS effect type. 86 | effect_m : middle LED effect type. 87 | (effect_x for a trigger of effect start) 88 | effect_names : show the effect types description. 89 | effect_duration_lr: Left and right joystick LEDS animation durations. 90 | effect_duration_l: Left joystick LEDS animation durations. 91 | effect_duration_r: Right joystick LEDS animation durations. 92 | effect_duration_m : middle LED effect duration. 93 | effect_rgb_hex_lr: Left and right LED all target color in format "RRGGBB " end with space. 94 | effect_rgb_hex_l: Left LED all target color in format "RRGGBB " end with space. 95 | effect_rgb_hex_r: Right LED all target color in format "RRGGBB " end with space. 96 | effect_rgb_hex_m: Middle LED target color in format "RRGGBB " end with space. 97 | effect_cycles_lr: Left and right joystick LEDS animation loops. 98 | effect_cycles_l: Left joystick LEDS animation loops. 99 | effect_cycles_r: Right joystick LEDS animation loops. 100 | effect_cycles_m : middle LED effect loops. 101 | (cycles value: 0 for stop, -1 for endless loop, > 0 for loop times) 102 | effect_enable : toggle of anim to function 103 | 104 | [usage of framebuffer animation function !!Did not finish yet!!] 105 | anim_frames: raw frames for total XRGB 32bpp data, buffer length 10 sec@60fps, 23 data per frame. 106 | anim_frames_hex: same as anim_frames and use hex format "RRGGBB RRGGBB RRGGBB ... RRGGBB " end with space. 107 | anim_frames_cycle: animation loops count 108 | anim_frames_enable : toggle of frames anim function 109 | anim_frames_override_m_enable: toggle of middle LED in frames anim function. 110 | ``` 111 | 112 | The tricky part was getting a daemon running on boot. First, `systemctl` isn't installed on the device so it's not as easy as starting/stopping with that. Daemons in `/etc/init.d` also use a different format than I've seen before, they're very primitive and seem to order themselves by a `START` variable (with 0 being first and running in order from there). I've done my best to run the daemon at the right time to make the changes feel seamless (i.e., the OS doesn't control the LEDs at any point) by placing the order just before the `runtrimui` service. Run `grep -H "START=" /etc/init.d/* | awk -F 'START=' '{print $2, $0}' | sort -n | cut -d' ' -f2-` on the device to see the order of boot services for more context. 113 | 114 | Getting a functioning daemon was a bit of a pain but here are a few things that helped me see where I was going wrong: 115 | - The brick uses an OpenWrt `/etc/rc.common` script to 'link' 'start', 'stop', 'restart', 'reload', 'boot', 'shutdown', and 'enable' services. So using this script will ensure compatibility with whatever the OS is expecting. 116 | - Check the examples in `/etc/init.d` a few times and cross-reference with what `/etc/rc.common` things are a little fast and loose (`printf` and `echo` both work? so do `start`, `start_service`, `start_service_remote`, and `start_service_daemon` [see `/etc/init.d/log`]). I should probably look into this standard to better understand what's going on under the hood. 117 | 118 | 1/12/2025: Turns out there's a directory `usr/trimui/bin` (should have looked at the minUI launch scripts a bit more thoroughly). In there is a script `usr/trimui/bin/init_leds.sh`. I'd bet we can just override this for an easy install method. There's a `usr/trimui/bin/low_battery_led.sh` that I should also overwrite to lock/unlock permission in toggling the LEDs. 119 | 120 | P.s: there's loads more scripts in here, this might be the key to unlocking more functionality. 121 | UPDATE: overwriting this script didn't seem to do anything :( 122 | 123 | Looks like MinUI actually does exactly what I'm thinking of doing here, they move `usr/trimui/bin/runtrimui.sh` `usr/trimui/bin/runtrimui-original.sh` and replace it with `skeleton/BOOT/trimui/app/runtrimui.sh` which then launches MinUI's `.tmp_update/updater` (or `runtrimui-original.sh` if the SDCARD isn't found). 124 | 125 | `usr/trimui/bin/trimui_inputd` is the input daemon written in C so maybe this is where runtime daemons go? 126 | 127 | Tons of more interesting things in `usr/trimui`, all the stock apps are in `usr/trimui/apps` and can easily be moved to the SDCARD for use in other OSs, alternatively we can move our app in here for a "permanent" install. `usr/trimui/gamecontrollerdb.txt` seemingly has the mapping of controllers to SDL inputs, if we wanted to add support for a certain controller, it would go there. 128 | 129 | `usr/trimui/lib` has all the installed libraries listed here: 130 | 131 | ``` 132 | libSDL-1.2.so.0 libSDL_image-1.2.so.0 133 | libSDL-1.2.so.0.11.4 libSDL_image-1.2.so.0.8.4 134 | libSDL2-2.0.so.0 libSDL_mixer-1.2.so.0 135 | libSDL2-2.0.so.0.3000.8 libSDL_mixer-1.2.so.0.12.0 136 | libSDL2_image-2.0.so.0 libSDL_ttf-2.0.so.0 137 | libSDL2_image-2.0.so.0.2.3 libSDL_ttf-2.0.so.0.10.1 138 | libSDL2_mixer-2.0.so.0 libgamename.so 139 | libSDL2_mixer-2.0.so.0.0.1 libshmvar.so 140 | libSDL2_ttf-2.0.so.0 libtmenu.so 141 | libSDL2_ttf-2.0.so.0.14.1 142 | ``` 143 | 144 | I wonder if we could swap in some binaries to support more applications? 145 | 146 | ## Final Thoughts 147 | 148 | Yes, this project is totally overkill. This can be done MUCH simpler as controlling the LEDs is as simple as `echo`-ing an integer into a few text files. But that's no fun... I was on winter break and had been meaning to dip my toes back into firmware/OS development, this project helped scratch that itch. 149 | 150 | Much of the project time was spent building a strong framework for myself and others to continue dev work on ARM-based Linux handhelds in the future. Those tools include but aren't limited to: a plug-and-play Docker container with scripts to easily build/deploy/test applications from your host machine and a base SDL library that handles all the display setup and input processing. 151 | 152 | This project was a ton of fun, it helped me better understand the wild world of custom operating systems on retro handhelds, sprites and sprite sheet rendering with SDL2, device-specific processes of the TrimUI Brick, the current state of generative-AI SWE tools, and much more. 153 | 154 | I hope being able to control your LEDs brings you even a fraction of the joy making this project has brought to me. 155 | 156 | Best, 157 | Pierce 158 | 159 | ## Special Thanks 160 | 161 | - [Shaun Inman](https://github.com/shauninman): Their dev toolchains, and MinUI as a whole were instrumental in building this project. 162 | - [ro8inmorgan](https://github.com/ro8inmorgan): Their [LedControl](https://github.com/ro8inmorgan/LedControl) application helped me fix some bugs/functionality in this application. 163 | - Retro Handhelds discord users: `Heef`, `Richard(RJNY)`, and `seki`. The app wouldn't be half as polished without their testing and feedback. 164 | - [skiselkov](https://github.com/skiselkov): For their contribution to the application. 165 | 166 | ## TODO 167 | 168 | - Smart pro support. 169 | - Separate core SDL functionality to its own repo to import as a submodule for future apps? 170 | 171 | -------------------------------------------------------------------------------- /dev_scripts/debug-release-in-host.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run from the root of the projects directory 3 | 4 | make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4 -------------------------------------------------------------------------------- /dev_scripts/make-all-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run from the root of the projects 3 | 4 | # Function to check if a Docker container exists 5 | container_exists() { 6 | docker ps -a --format '{{.Names}}' | grep -Eq "^$1\$" 7 | } 8 | 9 | # Container name 10 | CONTAINER_NAME="arm64-tg3040-toolchain" 11 | 12 | # Check if the container exists and build it if it doesn't 13 | if ! container_exists "$CONTAINER_NAME"; then 14 | echo "Container $CONTAINER_NAME does not exist. Building the toolchain..." 15 | cd toolchains/arm64-tg3040-toolchain 16 | make .build 17 | cd - 18 | fi 19 | 20 | # Check for the "release" parameter 21 | if [ "$1" = "release" ]; then 22 | MAKE_COMMAND="make && make release" 23 | else 24 | MAKE_COMMAND="make && make package" 25 | fi 26 | 27 | # Run the make commands 28 | eval $MAKE_COMMAND 29 | 30 | # Run the Docker container and execute the make commands 31 | docker run --rm --privileged \ 32 | --platform linux/arm64 \ 33 | -v $(pwd)/:/root/workspace \ 34 | --device /dev/dri \ 35 | $CONTAINER_NAME sh -c "cd /root/workspace && $MAKE_COMMAND" 36 | -------------------------------------------------------------------------------- /dev_scripts/run-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run from the root of the projects 3 | 4 | # Function to check if a Docker container exists 5 | container_exists() { 6 | docker ps -a --format '{{.Names}}' | grep -Eq "^$1\$" 7 | } 8 | 9 | # Container name 10 | CONTAINER_NAME="arm64-tg3040-toolchain" 11 | 12 | # Check if the container exists and build it if it doesn't 13 | if ! container_exists "$CONTAINER_NAME"; then 14 | echo "Container $CONTAINER_NAME does not exist. Building the toolchain..." 15 | cd toolchains/arm64-tg3040-toolchain 16 | make .build 17 | cd - 18 | fi 19 | 20 | # Run the QEMU emulator and allows it to handle ARM64 binaries 21 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 22 | 23 | # Allow local connections to X server 24 | xhost +local:root 25 | 26 | # Run the Docker container 27 | docker run -it --privileged \ 28 | --platform linux/arm64 \ 29 | -e DISPLAY=$DISPLAY \ 30 | -e XDG_RUNTIME_DIR=/tmp \ 31 | -v /tmp/.X11-unix:/tmp/.X11-unix:rw \ 32 | -v $XAUTHORITY:/root/.Xauthority:rw \ 33 | -v $(pwd)/:/root/workspace \ 34 | --device /dev/dri \ 35 | $CONTAINER_NAME 36 | 37 | # Revoke local connections to X server 38 | xhost -local:root -------------------------------------------------------------------------------- /dev_scripts/setup-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ###################################################################################################### 3 | # Taken from shauninman's union-tg3040-toolchain: https://github.com/shauninman/union-tg3040-toolchain 4 | ###################################################################################################### 5 | 6 | # This is meant to be run within the container. 7 | 8 | # If we're already on an aarch64 system, we set our compiler flags to defaults and exit. 9 | TOOLCHAIN_ARCH=`uname -m` 10 | if [ "$TOOLCHAIN_ARCH" = "aarch64" ]; then 11 | export CROSS_COMPILE=/usr/bin/aarch64-linux-gnu- 12 | export PREFIX=/usr 13 | else 14 | # Otherwise, we need to explicitly path to the target compiler. 15 | export PATH="/opt/aarch64-linux-gnu/aarch64-linux-gnu/bin:${PATH}:/opt/aarch64-linux-gnu/aarch64-linux-gnu/libc/bin" 16 | export CROSS_COMPILE=/opt/aarch64-linux-gnu/bin/aarch64-linux-gnu- 17 | export PREFIX=/opt/aarch64-linux-gnu/aarch64-linux-gnu/libc/usr 18 | fi 19 | export UNION_PLATFORM=tg3040 20 | -------------------------------------------------------------------------------- /dev_scripts/setup-host.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run from the root of the projects 3 | 4 | # Stuff to install on your host machine to make building/debugging easier 5 | # Assumes we're running in an Ubuntu environment 6 | 7 | sudo apt-get install gdb-multiarch -------------------------------------------------------------------------------- /dev_scripts/setup-toolchain.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ###################################################################################################### 3 | # Taken from shauninman's union-tg3040-toolchain: https://github.com/shauninman/union-tg3040-toolchain 4 | ###################################################################################################### 5 | 6 | # This is meant to be run within the container. 7 | 8 | # If we're already on an aarch64 system, we don't need to do anything. 9 | TOOLCHAIN_ARCH=`uname -m` 10 | if [ "$TOOLCHAIN_ARCH" = "aarch64" ]; then 11 | exit 12 | fi 13 | 14 | # Otherwise, we need to set up the toolchain. 15 | SYSROOT_TAR="SDK_usr_tg5040_a133p" 16 | TOOLCHAIN_NAME="aarch64-linux-gnu" 17 | TOOLCHAIN_TAR="gcc-arm-8.3-2019.02-x86_64-aarch64-linux-gnu" 18 | 19 | TOOLCHAIN_URL="https://developer.arm.com/-/media/Files/downloads/gnu-a/8.3-2019.02/$TOOLCHAIN_TAR.tar.xz" 20 | SYSROOT_URL="https://github.com/trimui/toolchain_sdk_smartpro/releases/download/20231018/$SYSROOT_TAR.tgz" 21 | 22 | cd ~ 23 | 24 | wget $TOOLCHAIN_URL 25 | wget $SYSROOT_URL 26 | 27 | tar xf $TOOLCHAIN_TAR.tar.xz -C /opt 28 | mv /opt/$TOOLCHAIN_TAR /opt/$TOOLCHAIN_NAME 29 | rm $TOOLCHAIN_TAR.tar.xz 30 | 31 | tar xf $SYSROOT_TAR.tgz 32 | rsync -a --ignore-existing ./usr/ /opt/$TOOLCHAIN_NAME/$TOOLCHAIN_NAME/libc/usr/ 33 | rm -rf ./usr $SYSROOT_TAR.tgz 34 | -------------------------------------------------------------------------------- /github/led_controller_main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/github/led_controller_main.gif -------------------------------------------------------------------------------- /github/readme.txt: -------------------------------------------------------------------------------- 1 | This application is designed for the TrimUI smart Brick emulation handheld. 2 | There has been no testing of operational safety on any other hardware. 3 | 4 | 5 | Stock OS: Place the "LedController.pak" folder in the "Apps" folder of your SDCARD and launch from the "Apps" menu. 6 | 7 | MinUI: Place the "LedController.pak" folder in the "Tools/tg3040" folder of your SDCARD and launch from the "Tools" menu. 8 | 9 | 10 | Controls: 11 | 12 | From the configuration menu... 13 | 14 | DPAD UP/DOWN: Change the selected setting. 15 | DPAD L/R: Change the value of the selected setting. 16 | START: Access the start menu. 17 | B/POWER: Exit the application. 18 | L/R: Switch the active LED. 19 | 20 | 21 | From the start menu... 22 | 23 | DPAD UP/DOWN: Change the selected setting. 24 | B/START: Return to config page. 25 | A: Execute selected setting. 26 | 27 | 28 | Menu Options: 29 | Sync LED colors - Matches all LEDs colors to the currently selected LED. 30 | Turn ON all LEDs - Enables all the LEDs by setting their brightness to the max value. 31 | Turn OFF all LEDs - Disabled all the LEDs by setting their brightness to 0. 32 | Enable/Disable extended colors - Toggles the ability to select colors from an extended range of RGB color values. 33 | Enable/Disable low battery warning - Toggles the default OS behavior of flashing your LEDs when the battery falls below 15%. 34 | Uninstall - Uninstalls all services and installed system files. Turns the LEDs off on exit. 35 | 36 | 37 | 38 | Notes: 39 | This is a free app made for fun over christmas break, hopefully you get some joy from messing with your LEDs. 40 | If you got this from anywhere other than https://github.com/pierceTee/TrimuiLEDController, 41 | you should follow that link and download the latest release. -------------------------------------------------------------------------------- /github/start_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/github/start_menu.png -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Compiler and flags for host 2 | CC = gcc 3 | ARCH := $(shell uname -m) 4 | BUILD_DIR = build/$(ARCH) 5 | RELEASE_DIR = release/$(ARCH) 6 | CFLAGS = -I/usr/include/SDL2 -Iworkspace/include -Wall 7 | LDFLAGS = -L/usr/lib -lSDL2 -lSDL2_ttf -lSDL2_image -lm -g 8 | 9 | # General flags 10 | PROJECT_NAME=LedController 11 | 12 | .PHONY: all clean deps 13 | 14 | all: led_controller 15 | 16 | led_controller: 17 | mkdir -p $(BUILD_DIR) 18 | $(CC) $(CFLAGS) -o $(BUILD_DIR)/led_controller workspace/src/led_controller_common.c workspace/src/led_controller.c workspace/src/sdl_base.c $(LDFLAGS) 19 | chmod -R a+rwx $(BUILD_DIR) 20 | 21 | package: all 22 | mkdir -p $(RELEASE_DIR) 23 | 24 | # Create general project package 25 | mkdir -p $(RELEASE_DIR)/$(PROJECT_NAME).pak/scripts 26 | 27 | # Copy the launch script to the release directory 28 | cp -r workspace/scripts/launch/launch.sh $(RELEASE_DIR)/$(PROJECT_NAME).pak/launch.sh 29 | 30 | # Copy the service scripts to the release directory 31 | cp workspace/scripts/runtime/* $(RELEASE_DIR)/$(PROJECT_NAME).pak/scripts 32 | 33 | # Copy the assets, service files, config files, and build files to the release directory 34 | cp -r workspace/assets/ workspace/service/ $(BUILD_DIR)/* workspace/config_files/* $(RELEASE_DIR)/$(PROJECT_NAME).pak 35 | 36 | # Make the release directory executable by all users 37 | chmod -R a+rwx $(RELEASE_DIR) 38 | 39 | release: package 40 | # Create the final release package 41 | mkdir -p $(RELEASE_DIR)/github 42 | cp github/readme.txt $(RELEASE_DIR) 43 | 44 | # zip up the project 45 | cd $(RELEASE_DIR) && zip -r github/$(PROJECT_NAME).zip . -x "github/*" 46 | cd - 47 | # zip up all the source code 48 | zip -r $(RELEASE_DIR)/github/source_code.zip . -x "release/*" "build/*" ".git/*" ".github/*" 49 | chmod -R a+rwx $(RELEASE_DIR)/github 50 | 51 | clean: 52 | rm -rf build release 53 | 54 | deps: 55 | # Ensure both target architecture's package libaries are available 56 | sudo apt-get update 57 | 58 | # Install SDL libraries for both architectures 59 | sudo apt install -y libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev 60 | 61 | # Install the cross-compilation tools 62 | sudo apt-get install -y gcc g++ zip 63 | -------------------------------------------------------------------------------- /workspace/assets/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/workspace/assets/images/background.png -------------------------------------------------------------------------------- /workspace/assets/images/brick_sprite_sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/workspace/assets/images/brick_sprite_sheet.png -------------------------------------------------------------------------------- /workspace/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/workspace/assets/images/icon.png -------------------------------------------------------------------------------- /workspace/assets/images/main_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/workspace/assets/images/main_menu.png -------------------------------------------------------------------------------- /workspace/assets/retro_gaming.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierceTee/TrimuiLEDController/4e2c115e2840104770fc9f9d4abd7d67f6409780/workspace/assets/retro_gaming.ttf -------------------------------------------------------------------------------- /workspace/config_files/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "LED Controller", 3 | "icon": "assets/images/icon.png", 4 | "iconsel": "assets/images/icon.png", 5 | "launch": "launch.sh", 6 | "description": "Configure the LEDs on your triumUI Brick" 7 | } -------------------------------------------------------------------------------- /workspace/config_files/settings.ini: -------------------------------------------------------------------------------- 1 | [global] 2 | should_enable_low_battery_indication=1 3 | 4 | [f1f2] 5 | brightness=60 6 | color=0xFF8000 7 | duration=500 8 | effect=4 9 | 10 | [m] 11 | brightness=60 12 | color=0xFF8000 13 | duration=500 14 | effect=4 15 | 16 | [lr] 17 | brightness=60 18 | color=0xFF8000 19 | duration=500 20 | effect=4 -------------------------------------------------------------------------------- /workspace/include/led_controller.h: -------------------------------------------------------------------------------- 1 | #ifndef LED_CONTROLLER_H 2 | #define LED_CONTROLLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "sdl_base.h" 8 | #include "led_controller_common.h" 9 | 10 | /* Eggshell white color for main text */ 11 | SDL_Color text_color = {255, 239, 186, 255}; 12 | 13 | /* Semi-transparent black for shadow */ 14 | SDL_Color text_shadow_color = {0, 0, 0, 128}; 15 | 16 | /* The list of colors we support */ 17 | const uint32_t colors[] = { 18 | 19 | // White 20 | 0xFFFFFF, // White 21 | // Greens 22 | 0x00FF00, // Green 23 | 0x00FF80, // Aqua 24 | 25 | // Yellows 26 | 0xFFFF00, // Yellow 27 | 0x808000, // Olive 28 | 29 | // Blues 30 | 0x0000FF, // Blue 31 | 0x0080FF, // Sky Blue 32 | 33 | // Cyans 34 | 0x00FFFF, // Cyan 35 | 0x008080, // Teal 36 | 37 | // Oranges 38 | 0xFF8000, // Orange 39 | // Reds 40 | 0xFF0000, // Red 41 | // Magentas 42 | 0xFF00FF, // Magenta 43 | 0xFF80C0, // Light Magenta 44 | // Pinks 45 | 0xFF0080, // Hot Pink 46 | 47 | 0xFF8080, // Light Red 48 | }; 49 | 50 | const int num_color = sizeof(colors) / sizeof(colors[0]); 51 | 52 | /* Array of the split suffixs that sont front led files end in*/ 53 | const char *front_led_suffix[2] = {"f1", "f2"}; 54 | 55 | /** 56 | * Manage what to do with user input. 57 | * 58 | * Shoulder buttons change which LED is selected. 59 | * B button quits the application. 60 | * DPAD UP/DOWN selects the setting to change 61 | * DPAD LEFT/RIGHT changes the setting value 62 | * START saves the configuration file. 63 | * Parameters: 64 | * user_input - pre-processed user input converted from SDL_Event 65 | * should_quit - loop control flag to exit the main loop. 66 | * app_state - state object with user information we're updating. 67 | * controller - SDL controller object to read dpad hold inputs from. 68 | * Returns: 69 | * void 70 | */ 71 | void handle_user_input(InputType user_input, AppState *app_state, SDL_GameController *controller); 72 | 73 | /** 74 | * Control what happens when a LED setting is changed. 75 | * 76 | * Parameters: 77 | * app_state - state object with user information we're updating. 78 | * change - the amount to change the setting by. 79 | * Returns: 80 | * void 81 | */ 82 | void handle_change_setting(AppState *app_state, int change); 83 | 84 | /** 85 | * Control what happens when a menu option is selected. 86 | * 87 | * Parameters: 88 | * app_state - state object with user information we're updating. 89 | * selected_menu_option - the menu option selected. 90 | * Returns: 91 | * void 92 | */ 93 | void handle_menu_select(AppState *app_state, MenuOption selected_menu_option); 94 | 95 | /** 96 | * Handle SDL events and update the application state. 97 | * This is the core updater for the main application loop 98 | * 99 | * Parameters: 100 | * app_state - state object with user information we're updating. 101 | * core_components - core SDL components 102 | * components - SDL components specific to this application 103 | * config_page_ui - user interface object for the config page 104 | * menu_page_ui - user interface object for the menu page 105 | * user_input - pre-processed user input converted from SDL_Event 106 | * event - the SDL event to process 107 | */ 108 | void handle_event_updates(AppState *app_state, CoreSDLComponents *core_components, AdditionalSDLComponents *components, 109 | SelectableMenuItems *config_page_ui, SelectableMenuItems *menu_page_ui, 110 | InputType user_input, SDL_Event event); 111 | 112 | /** 113 | * Initialize any SDL components needed by the application that aren't 114 | * already initialized by initialize_sdl_core. 115 | * 116 | * Parameters: 117 | * core_components - core SDL components 118 | * components - SDL components specific to this application 119 | * 120 | * Returns: 121 | * 0 on success, 1 on failures 122 | */ 123 | int initialize_additional_sdl_components(CoreSDLComponents *core_components, AdditionalSDLComponents *components); 124 | 125 | /** 126 | * Initialize the user state object. 127 | * 128 | * Parameters: 129 | * app_state - state object to initialize 130 | * 131 | * Returns: 132 | * 0 on success, 1 on failure 133 | */ 134 | int initialize_app_state(AppState *app_state); 135 | 136 | /** 137 | * Initialize the user interface. 138 | * 139 | * Parameters: 140 | * menu_items - user interface object to initialize 141 | * core_components - core SDL components 142 | * components - SDL components specific to this application 143 | * 144 | * Returns: 145 | * void 146 | */ 147 | void initialize_config_page_ui(SelectableMenuItems *menu_items, CoreSDLComponents *core_components, AdditionalSDLComponents *components); 148 | 149 | /** 150 | * Initialize the main menu (start screen) user interface. 151 | * 152 | * Parameters: 153 | * menu_items - user interface object to initialize 154 | * core_components - core SDL components 155 | * components - SDL components specific to this application 156 | * app_state - AppState object to reference for conditional logic 157 | * 158 | * Returns: 159 | * void 160 | */ 161 | void initialize_menu_ui(SelectableMenuItems *menu_items, CoreSDLComponents *core_components, AdditionalSDLComponents *components, AppState *app_state); 162 | 163 | /** 164 | * Frees all sub-objects of the menu_items 165 | * 166 | * Can be used for cleanup and to clear the text off the screen in 167 | * preparation for the next frame. 168 | */ 169 | void free_menu_items(SelectableMenuItems *menu_items); 170 | 171 | /** 172 | * Update the user interface text. 173 | * 174 | * Parameters: 175 | * menu_items - user interface object to update 176 | * core_components - core SDL components 177 | * components - SDL components specific to this application 178 | * app_state - state object with user information we're updating. 179 | * 180 | * Returns: 181 | * void 182 | */ 183 | void update_menu_ui_text(SelectableMenuItems *menu_items, const CoreSDLComponents *core_components, const AdditionalSDLComponents *components, const AppState *app_state); 184 | 185 | /** 186 | * Update the config page (start menu) user interface text. 187 | * 188 | * Parameters: 189 | * menu_items - user interface object to update 190 | * core_components - core SDL components 191 | * components - SDL components specific to this application 192 | * app_state - state object with user information we're updating. 193 | * 194 | * Returns: 195 | * void 196 | */ 197 | void update_config_page_ui_text(SelectableMenuItems *menu_items, const CoreSDLComponents *core_components, const AdditionalSDLComponents *components, const AppState *app_state); 198 | 199 | /** 200 | * Read settings from a file. 201 | * 202 | * Parameters: 203 | * app_state - state object to read settings into 204 | * 205 | * Returns: 206 | * 0 on success, 1 on failure 207 | */ 208 | int read_settings(AppState *app_state); 209 | 210 | /** 211 | * Save settings to a file. 212 | * 213 | * Parameters: 214 | * app_state - state object to save settings from 215 | * 216 | * Returns: 217 | * 0 on success, 1 on failure 218 | */ 219 | int save_settings(AppState *app_state); 220 | 221 | /** 222 | * Initialize the Sprite object responsible for rendering the brick animations. 223 | * 224 | * Parameters: 225 | * brick_sprite - sprite object to initialize 226 | * renderer - SDL renderer to draw to 227 | * 228 | * Returns: 229 | * 0 on success, 1 on failure 230 | */ 231 | int initialize_brick_sprite(Sprite *brick_sprite, SDL_Renderer *renderer); 232 | 233 | /** 234 | * Render the main application frame to the screen. 235 | * This is a bit of a monolith function to house all the final rendering logic 236 | * for the main application loop. As such it takes several inputs. 237 | * 238 | * Parameters: 239 | * app_state - state object with user information we're updating. 240 | * core_components - core SDL components 241 | * components - SDL components specific to this application 242 | * brick_sprite - sprite object to render 243 | * config_page_ui - user interface object for the config page 244 | * menu_page_ui - user interface object for the menu page 245 | * verbose_logging_enabled - flag to enable verbose logging 246 | * 247 | */ 248 | void render_frame(AppState *app_state, CoreSDLComponents *core_components, AdditionalSDLComponents *components, Sprite *brick_sprite, SelectableMenuItems *config_page_ui, SelectableMenuItems *menu_page_ui, bool verbose_logging_enabled); 249 | 250 | /** 251 | * Render a colored square to the screen. 252 | * 253 | * Parameters: 254 | * app_state - state object with user information we're updating. 255 | * core_components - core SDL components 256 | * 257 | * Returns: 258 | * void 259 | */ 260 | void render_colored_square(AppState *app_state, CoreSDLComponents *core_components); 261 | 262 | /** 263 | * Render a text texture to the screen. 264 | * 265 | * Parameters: 266 | * renderer - SDL renderer to draw to. 267 | * texture - SDL texture containing the text 268 | * x - x position to render the text 269 | * y - y position to render the text 270 | * 271 | * Returns: 272 | * void 273 | */ 274 | void render_text_texture(SDL_Renderer *renderer, SDL_Texture *texture, int x, int y); 275 | 276 | /** 277 | * Render the user interface to the screen. 278 | * 279 | * Parameters: 280 | * renderer - SDL renderer to draw to. 281 | * menu_items - user interface object to render 282 | * start_x - x position to start rendering 283 | * start_y - y position to start rendering 284 | * 285 | * Returns: 286 | * void 287 | */ 288 | void render_menu_items(SDL_Renderer *renderer, SelectableMenuItems *menu_items, int start_x, int start_y); 289 | 290 | /** 291 | * Load an image from a file. 292 | * 293 | * Parameters: 294 | * image_name - name of the image file to load 295 | * 296 | * Returns: 297 | * SDL_Surface containing the image 298 | */ 299 | SDL_Surface *load_image(const char *image_name); 300 | 301 | /** 302 | * Update the LED system files. 303 | * 304 | * Returns: 305 | * void 306 | */ 307 | void update_led_sys_files(); 308 | 309 | /** 310 | * Teardown the application. 311 | * 312 | * Parameters: 313 | * core_components - core SDL components 314 | * components - SDL components specific to this application 315 | * config_menu_items - user interface object to teardown 316 | * main_menu_items - user interface object to teardown 317 | * brick_sprite - sprite object to teardown 318 | * app_state - state object used to determine return code 319 | * 320 | * Returns: 321 | * 0 on success, a numeric error code on failure 322 | */ 323 | int teardown(CoreSDLComponents *core_components, AdditionalSDLComponents *components, 324 | SelectableMenuItems *config_menu_items, SelectableMenuItems *main_menu_items, Sprite *brick_sprite, AppState *app_state); 325 | /** 326 | * Write the max scale data to a file. 327 | * 328 | * Parameters: 329 | * file - file to write to 330 | * app_state - state object with user information we're updating. 331 | * led - LED to write the data for 332 | * filepath - path to the file 333 | * 334 | * Returns: 335 | * void 336 | */ 337 | void write_max_scale_data(FILE *file, const AppState *app_state, const Led led, char *filepath); 338 | 339 | /** 340 | * Write the effect data to a file. 341 | * 342 | * Parameters: 343 | * file - file to write to 344 | * app_state - state object with user information we're updating. 345 | * led - LED to write the data for 346 | * filepath - path to the file 347 | * 348 | * Returns: 349 | * void 350 | */ 351 | void write_effect_data(FILE *file, const AppState *app_state, const Led led, char *filepath); 352 | 353 | /** 354 | * Write the effect duration data to a file. 355 | * 356 | * Parameters: 357 | * file - file to write to 358 | * app_state - state object with user information we're updating. 359 | * led - LED to write the data for 360 | * filepath - path to the file 361 | * 362 | * Returns: 363 | * void 364 | */ 365 | void write_effect_duration_data(FILE *file, const AppState *app_state, const Led led, char *filepath); 366 | 367 | /** 368 | * Write the color data to a file. 369 | * 370 | * Parameters: 371 | * file - file to write to 372 | * app_state - state object with user information we're updating. 373 | * led - LED to write the data for 374 | * filepath - path to the file 375 | * 376 | * Returns: 377 | * void 378 | */ 379 | void write_color_data(FILE *file, const AppState *app_state, const Led led, char *filepath); 380 | 381 | /** 382 | * Write user selection data to the associated LED files. 383 | * 384 | * Opens several files in the /sys/class/led_anim directory and writes 385 | * the user selected values to the files. The firmware continually 386 | * checks the values in this directory and updates the LEDs accordingly. 387 | * 388 | * Parameters: 389 | * app_state - state object with user information we're updating. 390 | * 391 | * Returns: 392 | * void 393 | */ 394 | void update_leds(AppState *app_state); 395 | 396 | /** 397 | * Install the daemon. 398 | * 399 | * Runs the install.sh script to install the daemon. 400 | * 401 | * Returns: 402 | * void 403 | */ 404 | void install_daemon(); 405 | 406 | /** 407 | * Uninstall the daemon. 408 | * 409 | * Runs the uninstall.sh script to uninstall the daemon. 410 | * 411 | * Returns: 412 | * void 413 | */ 414 | void uninstall_daemon(); 415 | 416 | /** 417 | * Quick action that matches all LED colors to the current 418 | * 419 | * Parameters: 420 | * app_state - state object with user information we're updating. 421 | * 422 | * Returns: 423 | * void 424 | */ 425 | void color_match_leds(AppState *app_state); 426 | 427 | /** 428 | * Turn off all LEDs. 429 | * 430 | * Parameters: 431 | * app_state - state object with led information that must be updated. 432 | * 433 | * Returns: 434 | * void 435 | */ 436 | void turn_off_all_leds(AppState *app_state); 437 | 438 | /** 439 | * Turn on all LEDs. 440 | * 441 | * Parameters: 442 | * app_state - state object with led information that must be updated. 443 | * 444 | * Returns: 445 | * void 446 | */ 447 | void turn_on_all_leds(AppState *app_state); 448 | 449 | #endif 450 | -------------------------------------------------------------------------------- /workspace/include/led_controller_common.h: -------------------------------------------------------------------------------- 1 | #ifndef LED_CONTROLLER_COMMON_H 2 | #define LED_CONTROLLER_COMMON_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // ** File locations ** 10 | #define IMAGE_DIR "assets/images" 11 | #define BACKGROUND_IMAGE_PATH "assets/images/background.png" 12 | #define MENU_IMAGE_PATH "assets/images/main_menu.png" 13 | #define BRICK_SPRITE_SHEET_PATH "assets/images/brick_sprite_sheet.png" 14 | #define FONT_PATH "assets/retro_gaming.ttf" 15 | #define SETTINGS_FILE "settings.ini" 16 | #define UPDATE_LED_SYS_FILES_SCRIPT "./update_led_sys_files.sh" 17 | /* Location of system files we need to edit to change LEDs */ 18 | #define SYS_FILE_PATH "/sys/class/led_anim" 19 | 20 | // ** SDL/Animation Consts ** 21 | /* TrimUI Brick WxH */ 22 | #define WINDOW_WIDTH 1024 23 | #define WINDOW_HEIGHT 768 24 | #define FONT_SIZE 34 25 | #define WINDOW_TITLE "Led Controller" 26 | #define BRICK_SPRITE_HEIGHT 512 27 | #define BRICK_SPRITE_WIDTH 512 28 | 29 | /* Indicator to render current user selection (i.e '>>> Brightness: 100') */ 30 | #define MENU_CARRET_LEFT "< " 31 | #define MENU_CARRET_RIGHT " >" 32 | 33 | // ** Settings Consts ** 34 | /* FRONT, TOP, BACK */ 35 | #define LED_COUNT 3 36 | /* brightness, effect, color, duration */ 37 | #define LED_SETTINGS_COUNT 6 38 | 39 | /* enable all, disable all, uninstall, quit*/ 40 | #define MENU_OPTION_COUNT 6 41 | 42 | /* DISABLE, LINEAR, BREATH, SNIFF, STATIC, BLINK1, BLINK2, BLINK3 */ 43 | #define ANIMATION_EFFECT_COUNT 8 44 | /* 0xRRGGBBAA */ 45 | #define COLOR_HEX_LENGTH 8 46 | /* Maximum brightness value allowed by trimui firmware */ 47 | #define MAX_BRIGHTNESS 100 48 | /* Maximum duration to cycle a lighting effect */ 49 | #define MAX_DURATION 5000 50 | /* How much to increment duration by when the user increases/decreases */ 51 | #define DURATION_INCREMENT 500 52 | /* How much to increment the brightness by when increasing/decreasing */ 53 | #define BRIGHTNESS_INCREMENT 10 54 | /* How many colors we support */ 55 | #define NUM_COLORS 17 56 | /* Application Consts */ 57 | #define STRING_LENGTH 256 58 | /*How much to increment/decrement the color when the user changes the value in extened color mode*/ 59 | #define COLOR_CYLCE_INCREMENT 16 60 | 61 | /* The different Led clusters we support */ 62 | typedef enum 63 | { 64 | LED_FRONT, 65 | LED_TOP, 66 | LED_BACK, 67 | } Led; 68 | 69 | /* The different Animation effect options recognized by the TrimUI firmware */ 70 | /* Reference: "/sys/class/led_anim/help" */ 71 | typedef enum 72 | { 73 | DISABLE, 74 | LINEAR, 75 | BREATH, 76 | SNIFF, 77 | STATIC, 78 | BLINK1, 79 | BLINK2, 80 | BLINK3 81 | } AnimationEffect; 82 | 83 | /* The different LED functions we support modifying */ 84 | typedef enum 85 | { 86 | SELECTED_LED, 87 | BRIGHTNESS, 88 | EFFECT, 89 | DURATION, 90 | COLOR, 91 | MATCH_SETTINGS 92 | } LedSettingOption; 93 | 94 | /* The different menu options we provide */ 95 | typedef enum 96 | { 97 | ENABLE_ALL, 98 | DISABLE_ALL, 99 | TOGGLE_EXTENDED_COLORS, 100 | TOGGLE_LOW_BATTERY_INDICATION, 101 | UNINSTALL, 102 | QUIT, 103 | } MenuOption; 104 | 105 | /* SDL Extended set of components required by this application not covered in sdl_base::CoreSDLComponents */ 106 | typedef struct 107 | { 108 | SDL_Texture *backgroundTexture; 109 | SDL_Texture *menuTexture; 110 | TTF_Font *font; 111 | } AdditionalSDLComponents; 112 | 113 | /* Cluster of all mutable user-interface related objects */ 114 | typedef struct 115 | { 116 | SDL_Color text_shadow_color; 117 | SDL_Color text_color; 118 | SDL_Color text_highlight_color; 119 | SDL_Texture **menu_text_textures; 120 | char **menu_text; 121 | int item_count; 122 | int string_length; 123 | 124 | } SelectableMenuItems; 125 | 126 | /* Collection of values for each supported LED setting. */ 127 | typedef struct 128 | { 129 | int brightness; 130 | AnimationEffect effect; 131 | uint32_t color; 132 | int duration; 133 | } LedSettings; 134 | 135 | typedef enum 136 | { 137 | CONFIG_PAGE, 138 | MENU_PAGE, 139 | } ApplicationPage; 140 | 141 | typedef struct 142 | { 143 | bool should_save_settings; 144 | bool should_update_leds; 145 | bool should_quit; 146 | bool should_install_daemon; 147 | bool are_extended_colors_enabled; 148 | bool should_enable_low_battery_indication; 149 | ApplicationPage current_page; 150 | Led selected_led; 151 | LedSettingOption selected_setting; 152 | MenuOption selected_menu_option; 153 | LedSettings led_settings[LED_COUNT]; 154 | } AppState; 155 | 156 | /** 157 | * Convert a color to a string. 158 | * 159 | * Parameters: 160 | * color - 0x8 RGB hex value of the color to convert 161 | * 162 | * Returns: 163 | * string containing the color 164 | */ 165 | const char *color_to_string(uint32_t color); 166 | 167 | /** 168 | * Cycles to the next color in the color wheel. 169 | * 170 | * This is used to cycle colors in the extended color mode. 171 | * Parameters: 172 | * color - the current 0xRRGGBBAA color to cycle from 173 | * sign - direction to cycle the color 174 | */ 175 | uint32_t next_color(uint32_t color, int sign); 176 | /** 177 | * Convert an animation effect to a string. 178 | * 179 | * Parameters: 180 | * effect - animation effect to convert 181 | * 182 | * Returns: 183 | * string containing the animation effect 184 | */ 185 | const char *animation_effect_to_string(AnimationEffect effect); 186 | 187 | /** 188 | * Convert a LED setting option to a string. 189 | * 190 | * Parameters: 191 | * setting - LED setting option to convert 192 | * 193 | * Returns: 194 | * string containing the LED setting option 195 | */ 196 | const char *led_setting_option_to_string(LedSettingOption setting); 197 | 198 | /** 199 | * Convert a menu option to a string. 200 | * 201 | * Parameters: 202 | * option - menu option to convert 203 | * app_state - application state to reference for conditional strings 204 | * 205 | * Returns: 206 | * string containing the menu option 207 | */ 208 | 209 | const char *menu_option_to_string(MenuOption option, const AppState *app_state); 210 | 211 | /** 212 | * Convert a LED to a string. 213 | * 214 | * Parameters: 215 | * led - LED to convert 216 | * 217 | * Returns: 218 | * string containing the LED 219 | */ 220 | const char *led_to_string(Led led); 221 | 222 | /** 223 | * Get the internal name of a LED. 224 | * 225 | * Parameters: 226 | * led - LED to get the internal name of 227 | * 228 | * Returns: 229 | * string containing the internal name of the LED 230 | */ 231 | const char *led_internal_name(Led led); 232 | 233 | /** 234 | * Convert an internal LED name to an index. 235 | * 236 | * Parameters: 237 | * led_name - internal name of the LED 238 | * 239 | * Returns: 240 | * Led object 241 | */ 242 | Led internal_led_name_to_led(const char *led_name); 243 | 244 | /** 245 | 246 | * Log a debug message. 247 | * 248 | * Parameters: 249 | * message - message to log 250 | * verbose_logging_enabled - flag to enable verbose logging 251 | * 252 | * Returns: 253 | * void 254 | */ 255 | void debug_log(const char *message, bool verbose_logging_enabled); 256 | 257 | /** 258 | * Clamp a value between a minimum and maximum value. 259 | * 260 | * Parameters: 261 | * value - the value to clamp 262 | * min - the minimum value 263 | * max - the maximum value 264 | * 265 | * Returns: 266 | * the clamped value 267 | */ 268 | int clamp(int value, int min, int max); 269 | #endif 270 | -------------------------------------------------------------------------------- /workspace/include/sdl_base.h: -------------------------------------------------------------------------------- 1 | #ifndef SDL_BASE_H 2 | #define SDL_BASE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | /* Struct to hold core SDL components 9 | * 10 | * Helps keep all SDL components in one place. 11 | */ 12 | typedef struct 13 | { 14 | SDL_Window *window; 15 | SDL_Renderer *renderer; 16 | SDL_GameController *controller; 17 | int window_width; 18 | int window_height; 19 | } CoreSDLComponents; 20 | 21 | /* Abstraction of what frames relate to a particular animation. 22 | * 23 | * I.E if an animation uses frames 0, 3, 4 of the horizontal sprite sheet, 24 | * with durations of 1000, 500, 500 milliseconds respectively, 25 | * it would be stored as: 26 | * frame_indicies = [0, 3, 4] 27 | * frame_duration_millis = [1000, 500, 500] 28 | * frame_count = 3 29 | * current_frame_index = 0 30 | * last_frame_time_millis = 0 31 | */ 32 | typedef struct 33 | { 34 | int *frame_indicies; 35 | int *frame_duration_millis; 36 | int frame_count; 37 | int current_frame_index; 38 | Uint32 last_frame_time_millis; 39 | } AnimationInfo; 40 | 41 | /* Sprite object that contains everything necessary to render a sprite. */ 42 | typedef struct 43 | { 44 | SDL_Texture *sprite_texture; 45 | AnimationInfo *animations; 46 | int animation_count; 47 | int current_animation_index; 48 | int sprite_width; 49 | int sprite_height; 50 | } Sprite; 51 | 52 | /* Used to convert SDL inputs to a common input definition. 53 | * 54 | * Useful in the case of accepting both keyboard and controller inputs. 55 | */ 56 | typedef enum 57 | { 58 | UNKNOWN, 59 | START, 60 | SELECT, 61 | MENU, 62 | POWER, 63 | DPAD_LEFT, 64 | DPAD_RIGHT, 65 | DPAD_UP, 66 | DPAD_DOWN, 67 | A, 68 | B, 69 | X, 70 | Y, 71 | L1, 72 | R1, 73 | L2, 74 | R2, 75 | L3, 76 | R3, 77 | } InputType; 78 | 79 | /** 80 | * Handles the initialization of common SDL components necessary for every app 81 | * 82 | * Parameters: 83 | * core_components - A pre-allocated CoreSDLComponents struct 84 | * to encapsulate common SDL objects 85 | * window_title - The title displayed at the top of the window. 86 | * 87 | * Returns: 88 | * 0 on success, 1 on failures 89 | */ 90 | int initialize_sdl_core(CoreSDLComponents *core_components, char *window_title); 91 | 92 | /** 93 | * Frees the core SDL components. 94 | * 95 | * Parameters: 96 | * core_components - A pre-allocated CoreSDLComponents struct 97 | * to encapsulate common SDL objects 98 | */ 99 | void free_sdl_core(CoreSDLComponents *core_components); 100 | 101 | /** 102 | * Frees the sprite object. 103 | * 104 | * Parameters: 105 | * sprite - The sprite object to free 106 | */ 107 | void free_sprite(Sprite *sprite); 108 | 109 | /** 110 | * Handles all the logic for preparing the next frame of a sprite animation. 111 | * 112 | * Works by selecting the correct section of the sprite sheet to render and 113 | * applying that to a SDL_Rect which is copies to the renderer at the chosen 114 | * position. If enough time has elapsed, the frame index is updated to the next 115 | * frame in the animation. As with all Sprite Objects, we expect the sprite to be 116 | * a horizontal sprite sheet. 117 | * 118 | * Note: Animation index is expected to be set before calling this function. 119 | * 120 | * Parameters: 121 | * renderer - SDL renderer to draw to. 122 | * sprite - SDL surface containing the sprite sheet 123 | * position_x - x position to render the sprite 124 | * position_y - y position to render the sprite 125 | * 126 | * Returns: 127 | * void 128 | */ 129 | void update_sprite_render(SDL_Renderer *renderer, Sprite *sprite, int position_x, int position_y); 130 | 131 | /** 132 | * Converts an SDL event to an InputType to simplify input handling 133 | * 134 | * Parameters: 135 | * event - The SDL event to convert 136 | * verbose - If true, prints the input type to the console 137 | * 138 | * Returns: 139 | * InputType enum value if the event is supported, UNKNOWN otherwise 140 | */ 141 | InputType sdl_event_to_input_type(SDL_Event *event, bool verbose); 142 | 143 | /** 144 | * Create an SDL texture from an image file. 145 | * 146 | * Returned pointer must be freed by the caller 147 | * 148 | * Parameters: 149 | * renderer - The SDL renderer to create the texture with 150 | * full_image_path - The full path to the image file 151 | * 152 | * Returns: 153 | * SDL_Texture pointer containing the image. 154 | */ 155 | SDL_Texture *create_sdl_texture_from_image(SDL_Renderer *renderer, const char *full_image_path); 156 | 157 | /** 158 | * Create a text texture from a string. 159 | * 160 | * Parameters: 161 | * renderer - SDL renderer to draw to. 162 | * font - TTF font to use for the text 163 | * text_color - color of the text 164 | * shadow_color - color of the text drop-shadow (leave null disable drop shadow rendering) 165 | * text - string containing the text 166 | * 167 | * Returns: 168 | * SDL_Texture containing the text 169 | */ 170 | SDL_Texture *create_text_texture(SDL_Renderer *renderer, TTF_Font *font, const SDL_Color *text_color, const SDL_Color *shadow_color, const char *text); 171 | 172 | /** 173 | * Checks if an SDL event is supported by the input handling system 174 | * 175 | * Parameters: 176 | * event_type - The SDL event type to check 177 | * 178 | * Returns: 179 | * true if the event is supported by the input handling system, false otherwise 180 | */ 181 | bool is_supported_input_event(Uint32 event_type); 182 | 183 | /** 184 | * Gets the name of an SDL_Keycode 185 | * 186 | * Parameters: 187 | * key - The SDL_Keycode to get the name of 188 | * 189 | * Returns: 190 | * A string containing the name of the key 191 | */ 192 | const char *get_input_type_name(Uint32 inputType); 193 | 194 | /** 195 | * Gets the name of an SDL event type 196 | * 197 | * Parameters: 198 | * eventType - The SDL event type to get the name of 199 | * 200 | * Returns: 201 | * A string containing the name of the event type 202 | */ 203 | const char *get_event_name(Uint32 eventType); 204 | 205 | /** 206 | * Gets the name of an SDL_GameControllerButton 207 | * 208 | * Parameters: 209 | * button - The SDL_GameControllerButton to get the name of 210 | * 211 | * Returns: 212 | * A string containing the name of the button 213 | */ 214 | const char *get_button_name(SDL_GameControllerButton button); 215 | 216 | /** 217 | * Gets the name of an SDL_Keycode 218 | * 219 | * Parameters: 220 | * key - The SDL_Keycode to get the name of 221 | * 222 | * Returns: 223 | * A string containing the name of the key 224 | */ 225 | const char *get_key_name(SDL_Keycode key); 226 | 227 | #endif 228 | -------------------------------------------------------------------------------- /workspace/scripts/launch/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Becomes LedController.pak/launch.sh 3 | BASE_LED_PATH="/sys/class/led_anim" 4 | TOP_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale" 5 | FRONT_LED_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale_f1f2" 6 | BACK_LED_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale_lr" 7 | LOG_FILE="led_controller.log" 8 | UNINSTALL_EXIT_CODE=66 9 | EXIT_OK_CODE=0 10 | 11 | SCRIPT_NAME=$(basename "$0") 12 | 13 | echo $0 $* 14 | cd $(dirname "$0") 15 | 16 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Launching cd $(dirname "$0")/led_controller" >> $LOG_FILE 17 | 18 | # Allow the application to change the LEDs 19 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Enabling write permissions on $BASE_LED_PATH files" >> $LOG_FILE 20 | chmod a+w $BASE_LED_PATH/* >> $LOG_FILE 21 | 22 | # turn LEDS on 23 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Turning on all LEDs" >> $LOG_FILE 24 | ./scripts/turn_on_all_leds.sh >> $LOG_FILE 25 | 26 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Launching led_controller..." >> $LOG_FILE 27 | 28 | # Grab the exit code that the led_controller app script will return 29 | ./led_controller 2>>$LOG_FILE 30 | EXIT_CODE=$? 31 | 32 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: led_controller returned: $EXIT_CODE" >> $LOG_FILE 33 | 34 | # Prevent other applications from changing the LEDs 35 | if [ $EXIT_CODE -ne $UNINSTALL_EXIT_CODE ]; then 36 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Disabling write permissions on $BASE_LED_PATH files" >> $LOG_FILE 37 | chmod a-w $BASE_LED_PATH/* >> $LOG_FILE 38 | fi 39 | 40 | if [ $EXIT_CODE -eq $UNINSTALL_EXIT_CODE ]; then 41 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Running turn_off_leds.sh" >> $LOG_FILE 42 | ./scripts/turn_off_all_leds.sh >> $LOG_FILE 43 | fi 44 | chmod a-w $BASE_LED_PATH/* >> $LOG_FILE 45 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: exiting..." >> $LOG_FILE 46 | -------------------------------------------------------------------------------- /workspace/scripts/runtime/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SYS_SERVICE_PATH="/etc/led_controller" 4 | SERVICE_NAME="led-settings-daemon" 5 | 6 | SCRIPT_NAME=$(basename "$0") 7 | 8 | # Write settings to system path. 9 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Writing LED information to configuration files ..." 10 | mkdir -p $SYS_SERVICE_PATH 11 | cp settings.ini service/settings-daemon.sh $SYS_SERVICE_PATH 12 | cp service/led-settings-daemon /etc/init.d/ 13 | chmod +x /etc/init.d/led-settings-daemon 14 | 15 | # Start service 16 | /etc/init.d/led-settings-daemon enable 17 | 18 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Finished writing service information to $SYS_SERVICE_PATH ..." 19 | 20 | 21 | # Read the should_enable_low_battery_indication setting 22 | if [ ! -f "settings.ini" ]; then 23 | echo "Error: settings.ini not found" 24 | exit 1 25 | fi 26 | LOW_BATTERY_ENABLED=$(grep -m 1 "should_enable_low_battery_indication" settings.ini | cut -d'=' -f2 | tr -d '[:space:]' || echo "0") 27 | 28 | # If enabled, replace the stock script with our custom one 29 | if [ "$LOW_BATTERY_ENABLED" = "1" ]; then 30 | 31 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: copying low battery LED script to /etc/init.d/ ..." 32 | 33 | # Copy custom low battery LED script to service directory 34 | cp service/led_controller_low_battery_led.sh $SYS_SERVICE_PATH 35 | 36 | # Backup stock led script if not already backed up 37 | if [ -f "/usr/trimui/bin/low_battery_led.sh" ] && [ ! -f "/usr/trimui/bin/low_battery_led.sh.bak" ]; then 38 | 39 | # Copy stock OS low_battery_led.sh to service directory 40 | cp /usr/trimui/bin/low_battery_led.sh $SYS_SERVICE_PATH/trimui_low_battery_led.sh 41 | 42 | # Create backup 43 | cp /usr/trimui/bin/low_battery_led.sh /usr/trimui/bin/low_battery_led.sh.bak 44 | fi 45 | 46 | # Remove existing script if present 47 | rm -f /usr/trimui/bin/low_battery_led.sh 48 | 49 | # Symlink custom script to stock script location 50 | ln -sf $SYS_SERVICE_PATH/led_controller_low_battery_led.sh /usr/trimui/bin/low_battery_led.sh 51 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Enabled custom low battery LED script" 52 | fi 53 | 54 | # If disabled, restore the stock script 55 | if [ "$LOW_BATTERY_ENABLED" = "0" ]; then 56 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Restoring stock low battery LED script ..." 57 | 58 | # Remove symlink if it exists 59 | if [ -L "/usr/trimui/bin/low_battery_led.sh" ]; then 60 | rm /usr/trimui/bin/low_battery_led.sh 61 | fi 62 | 63 | # Restore backup if it exists 64 | if [ -f "/usr/trimui/bin/low_battery_led.sh.bak" ]; then 65 | mv /usr/trimui/bin/low_battery_led.sh.bak /usr/trimui/bin/low_battery_led.sh 66 | fi 67 | 68 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Restored stock low battery LED script" 69 | fi 70 | 71 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: exiting ..." -------------------------------------------------------------------------------- /workspace/scripts/runtime/turn_off_all_leds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | BASE_LED_PATH="/sys/class/led_anim" 3 | TOP_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale" 4 | FRONT_LED_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale_f1f2" 5 | BACK_LED_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale_lr" 6 | SCRIPT_NAME=$(basename "$0") 7 | 8 | chmod a+w $BASE_LED_PATH/* 9 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Turning off all LEDs" 10 | # Turn brightness off for all LEDs 11 | echo 0 | tee $TOP_BRIGHTNESS_PATH $FRONT_LED_BRIGHTNESS_PATH $BACK_LED_BRIGHTNESS_PATH 12 | 13 | # Change all LEDs to static (I think the OS looks for changes to the effect files 14 | # to know when to update the LEDs) 15 | echo 0 | tee $BASE_LED_PATH/effect_lr $BASE_LED_PATH/effect_m $BASE_LED_PATH/effect_f1 $BASE_LED_PATH/effect_f2 16 | -------------------------------------------------------------------------------- /workspace/scripts/runtime/turn_on_all_leds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | BASE_LED_PATH="/sys/class/led_anim" 3 | TOP_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale" 4 | FRONT_LED_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale_f1f2" 5 | BACK_LED_BRIGHTNESS_PATH="$BASE_LED_PATH/max_scale_lr" 6 | SCRIPT_NAME=$(basename "$0") 7 | 8 | chmod a+w $BASE_LED_PATH/* 9 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Turning on all LEDs" 10 | echo 100 | tee $TOP_BRIGHTNESS_PATH $FRONT_LED_BRIGHTNESS_PATH $BACK_LED_BRIGHTNESS_PATH -------------------------------------------------------------------------------- /workspace/scripts/runtime/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | BASE_LED_PATH="/sys/class/led_anim" 3 | SERVICE_PATH="/etc/led_controller" 4 | SCRIPT_NAME=$(basename "$0") 5 | 6 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Removing all led_controller files from system ..." 7 | # Stop our service 8 | /etc/init.d/led-settings-daemon stop 9 | 10 | # Remove our service files. 11 | rm /etc/rc.d/*led-settings-daemon 12 | rm /etc/init.d/led-settings-daemon 13 | rm -rf $SERVICE_PATH 14 | 15 | # Restore the stock low battery LED script 16 | if [ -f "/usr/trimui/bin/low_battery_led.sh.bak" ]; then 17 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: Restoring stock low battery LED script ..." 18 | rm -f /usr/trimui/bin/low_battery_led.sh 19 | mv /usr/trimui/bin/low_battery_led.sh.bak /usr/trimui/bin/low_battery_led.sh 20 | fi 21 | 22 | # Re-enabled led file access 23 | chmod a+w $BASE_LED_PATH/* 24 | echo "[`date '+%Y-%m-%d %H:%M:%S'`][$SCRIPT_NAME]: exiting ..." -------------------------------------------------------------------------------- /workspace/service/led-settings-daemon: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | # LED Settings Daemon 4 | # Run after system starts but before the trimUI OS 5 | # For more information on how daemons work on Trimui devices, 6 | # see /etc/rc.common usage, and /etc/init.d for examples. 7 | START=98 8 | STOP=99 9 | start() { 10 | printf "Starting LED SETTINGS DAEMON...\n" 11 | ./etc/led_controller/settings-daemon.sh 12 | echo "done" 13 | } 14 | 15 | stop_() { 16 | printf "Stopping LED SETTINGS DAEMON" 17 | killall led-settings-daemon 18 | } -------------------------------------------------------------------------------- /workspace/service/led_controller_low_battery_led.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # My custom version of trimUIs low battery LED script 3 | # This becomes /usr/trimui/bin/low_battery_led.sh and is called by the system when the battery is low 4 | # The only material difference at the time of writing it is that we enable/disable write 5 | # access to the LEDs before operating on them. 6 | 7 | 8 | # Enable write permissions on LED files 9 | chmod a+w /sys/class/led_anim/* 10 | 11 | case "$1" in 12 | 1 ) 13 | echo "enter low battery" 14 | echo "FF0000 " > /sys/class/led_anim/effect_rgb_hex_lr 15 | echo "30000" > /sys/class/led_anim/effect_cycles_lr 16 | echo "2000" > /sys/class/led_anim/effect_duration_lr 17 | echo "6" > /sys/class/led_anim/effect_lr 18 | 19 | echo "FF0000 " > /sys/class/led_anim/effect_rgb_hex_f1 20 | echo "30000" > /sys/class/led_anim/effect_cycles_f1 21 | echo "2000" > /sys/class/led_anim/effect_duration_f1 22 | echo "6" > /sys/class/led_anim/effect_f1 23 | 24 | echo "FF0000 " > /sys/class/led_anim/effect_rgb_hex_f2 25 | echo "30000" > /sys/class/led_anim/effect_cycles_f2 26 | echo "2000" > /sys/class/led_anim/effect_duration_f2 27 | echo "6" > /sys/class/led_anim/effect_f2 28 | ;; 29 | 0 ) 30 | echo "exit low battery" 31 | echo "0" > /sys/class/led_anim/effect_lr 32 | echo "0" > /sys/class/led_anim/effect_f1 33 | echo "0" > /sys/class/led_anim/effect_f2 34 | ;; 35 | *) 36 | ;; 37 | esac -------------------------------------------------------------------------------- /workspace/service/settings-daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | INSTALL_DIR="/etc/led_controller" 4 | SETTINGS_FILE="$INSTALL_DIR/settings.ini" 5 | SYS_FILE_PATH="/sys/class/led_anim" 6 | SERVICE_PATH="/etc/led_controller" 7 | BASE_LED_PATH="/sys/class/led_anim" 8 | SCRIPT_NAME=$(basename "$0") 9 | LOG_FILE="$INSTALL_DIR/settings_daemon.log" 10 | 11 | # Function to apply settings for a specific LED 12 | apply_led_settings() { 13 | local led=$1 14 | local brightness=$2 15 | local color=$3 16 | local duration=$4 17 | local effect=$5 18 | 19 | echo "[$SCRIPT_NAME]: Writing $led LED information to configuration files ..." | tee -a "$LOG_FILE" 20 | if [ "$led" = "f1f2" ]; then 21 | echo $brightness > "$SYS_FILE_PATH/max_scale_$led" 22 | echo $color > "$SYS_FILE_PATH/effect_rgb_hex_f1" 23 | echo $color > "$SYS_FILE_PATH/effect_rgb_hex_f2" 24 | echo $duration > "$SYS_FILE_PATH/effect_duration_f1" 25 | echo $duration > "$SYS_FILE_PATH/effect_duration_f2" 26 | echo $effect > "$SYS_FILE_PATH/effect_f1" 27 | echo $effect > "$SYS_FILE_PATH/effect_f2" 28 | else 29 | if [ "$led" = "m" ]; then 30 | # Why are you like this TrimUI? 31 | echo $brightness > "$SYS_FILE_PATH/max_scale" 32 | else 33 | echo $brightness > "$SYS_FILE_PATH/max_scale_$led" 34 | fi 35 | echo $color > "$SYS_FILE_PATH/effect_rgb_hex_$led" 36 | echo $duration > "$SYS_FILE_PATH/effect_duration_$led" 37 | echo $effect > "$SYS_FILE_PATH/effect_$led" 38 | fi 39 | } 40 | 41 | # == Start of the script == 42 | echo "[$SCRIPT_NAME]: LED settings daemon started ..." | tee -a "$LOG_FILE" 43 | 44 | # Enable write permissions on LED files 45 | chmod a+w $BASE_LED_PATH/* | tee -a "$LOG_FILE" 46 | 47 | echo "[$SCRIPT_NAME]: Writing LED information to configuration files ..." | tee -a "$LOG_FILE" 48 | # Read settings from the settings file and apply them 49 | while IFS= read -r line; do 50 | if echo "$line" | grep -qE '^\[([a-zA-Z0-9]+)\]$'; then 51 | led=$(echo "$line" | sed -n 's/^\[\([a-zA-Z0-9]\+\)\]$/\1/p') 52 | elif echo "$line" | grep -qE '^brightness=([0-9]+)$'; then 53 | brightness=$(echo "$line" | sed -n 's/^brightness=\([0-9]\+\)$/\1/p') 54 | elif echo "$line" | grep -qE '^color=0x([0-9A-Fa-f]+)$'; then 55 | color=$(echo "$line" | sed -n 's/^color=0x\([0-9A-Fa-f]\+\)$/\1/p') 56 | elif echo "$line" | grep -qE '^duration=([0-9]+)$'; then 57 | duration=$(echo "$line" | sed -n 's/^duration=\([0-9]\+\)$/\1/p') 58 | elif echo "$line" | grep -qE '^effect=([0-9]+)$'; then 59 | effect=$(echo "$line" | sed -n 's/^effect=\([0-9]\+\)$/\1/p') 60 | apply_led_settings $led $brightness $color $duration $effect 61 | elif echo "$line" | grep -qE '^should_enable_low_battery_indication='; then 62 | echo "[$SCRIPT_NAME]: Ignoring should_enable_low_battery_indication setting ..." | tee -a "$LOG_FILE" 63 | fi 64 | done < "$SETTINGS_FILE" 65 | 66 | # Disable write permissions on LED files 67 | chmod a-w $BASE_LED_PATH/* | tee -a "$LOG_FILE" 68 | 69 | echo "[$SCRIPT_NAME]: Finished writing LED information to configuration files ..." | tee -a "$LOG_FILE" 70 | echo "[$SCRIPT_NAME]: LED settings daemon stopped ..." | tee -a "$LOG_FILE" 71 | echo "[$SCRIPT_NAME]: exiting ..." | tee -a "$LOG_FILE" -------------------------------------------------------------------------------- /workspace/src/led_controller.c: -------------------------------------------------------------------------------- 1 | #include "led_controller.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | /* Handle program inputs */ 15 | bool verbose_logging_enabled = argc > 1 && strcmp(argv[1], "-v") == 0; 16 | 17 | /* Core SDL components that every application (I write) requires. */ 18 | CoreSDLComponents core_components; 19 | 20 | /* SDL components that this application (I write) requires beyond the core components. */ 21 | AdditionalSDLComponents components; 22 | 23 | /* User state object, contains all relevant user selection information. */ 24 | AppState app_state; 25 | 26 | /* The collection of user interface related objects. */ 27 | SelectableMenuItems config_page_ui; 28 | 29 | /* The collection of user interface related objects. */ 30 | SelectableMenuItems menu_page_ui; 31 | 32 | Sprite brick_sprite; 33 | /* The last user input converted from SDL_Event. */ 34 | InputType user_input; 35 | 36 | /* SDL event object we read on loop. */ 37 | SDL_Event event; 38 | 39 | /* Initialize onscreen logging message */ 40 | 41 | /* Initialize Primary SDL components */ 42 | core_components.window_width = WINDOW_WIDTH; 43 | core_components.window_height = WINDOW_HEIGHT; 44 | 45 | /* Initialize auxilliary data structures */ 46 | initialize_app_state(&app_state); 47 | update_leds(&app_state); 48 | if (initialize_sdl_core(&core_components, WINDOW_TITLE) != 0 || 49 | initialize_additional_sdl_components(&core_components, &components) != 0) 50 | { 51 | SDL_Log("Failed to initialize SDL, exiting...\n"); 52 | return 1; 53 | } 54 | initialize_config_page_ui(&config_page_ui, &core_components, &components); 55 | initialize_menu_ui(&menu_page_ui, &core_components, &components, &app_state); 56 | 57 | update_config_page_ui_text(&config_page_ui, &core_components, &components, &app_state); 58 | update_menu_ui_text(&menu_page_ui, &core_components, &components, &app_state); 59 | 60 | /* Initialize sprites */ 61 | initialize_brick_sprite(&brick_sprite, core_components.renderer); 62 | /* Render the background */ 63 | SDL_RenderCopy(core_components.renderer, components.backgroundTexture, NULL, NULL); 64 | while (!app_state.should_quit) 65 | { 66 | /* Handle events */ 67 | while (SDL_PollEvent(&event) != 0) 68 | { 69 | /* Ignore unsupported input events */ 70 | if (!is_supported_input_event(event.type)) 71 | { 72 | continue; 73 | } 74 | 75 | if (event.type == SDL_QUIT) 76 | { 77 | app_state.should_quit = true; 78 | break; 79 | } 80 | /* Take and process user input. */ 81 | user_input = sdl_event_to_input_type(&event, false); 82 | 83 | handle_event_updates(&app_state, &core_components, &components, &config_page_ui, &menu_page_ui, user_input, event); 84 | } 85 | 86 | /* Call the render_frame function */ 87 | render_frame(&app_state, &core_components, &components, &brick_sprite, &config_page_ui, &menu_page_ui, verbose_logging_enabled); 88 | } 89 | 90 | save_settings(&app_state); 91 | update_leds(&app_state); 92 | if (app_state.should_install_daemon) 93 | { 94 | install_daemon(); 95 | } 96 | return teardown(&core_components, &components, &config_page_ui, &menu_page_ui, &brick_sprite, &app_state); 97 | } 98 | 99 | void handle_user_input(InputType user_input, AppState *app_state, SDL_GameController *controller) 100 | { 101 | switch (app_state->current_page) 102 | { 103 | case CONFIG_PAGE: 104 | { 105 | switch (user_input) 106 | { 107 | case POWER: 108 | /* power button safely exits app */ 109 | app_state->should_quit = true; 110 | break; 111 | case START: 112 | case SELECT: 113 | app_state->current_page = MENU_PAGE; 114 | break; 115 | break; 116 | case A: 117 | handle_change_setting(app_state, 0); 118 | app_state->should_update_leds = true; 119 | break; 120 | case B: 121 | switch (app_state->current_page) 122 | { 123 | case MENU_PAGE: 124 | /* close menu */ 125 | app_state->current_page = CONFIG_PAGE; 126 | break; 127 | case CONFIG_PAGE: 128 | /* exit app */ 129 | app_state->should_quit = true; 130 | break; 131 | } 132 | break; 133 | case DPAD_UP: 134 | /* Switch between settings */ 135 | app_state->selected_setting = (app_state->selected_setting - 1 + LED_SETTINGS_COUNT) % LED_SETTINGS_COUNT; 136 | break; 137 | case DPAD_DOWN: 138 | /* Switch between settings */ 139 | app_state->selected_setting = (app_state->selected_setting + 1 + LED_SETTINGS_COUNT) % LED_SETTINGS_COUNT; 140 | break; 141 | case DPAD_RIGHT: 142 | handle_change_setting(app_state, 1); 143 | app_state->should_update_leds = true; 144 | break; 145 | case DPAD_LEFT: 146 | handle_change_setting(app_state, -1); 147 | app_state->should_update_leds = true; 148 | break; 149 | case L1: 150 | case L2: 151 | case L3: 152 | app_state->selected_led = (app_state->selected_led - 1 + LED_COUNT) % LED_COUNT; 153 | break; 154 | case R1: 155 | case R2: 156 | case R3: 157 | app_state->selected_led = (app_state->selected_led + 1) % LED_COUNT; 158 | break; 159 | default: 160 | break; 161 | } 162 | } 163 | break; 164 | case MENU_PAGE: 165 | { 166 | switch (user_input) 167 | { 168 | case START: 169 | case SELECT: 170 | case B: 171 | app_state->current_page = CONFIG_PAGE; 172 | break; 173 | case A: 174 | handle_menu_select(app_state, app_state->selected_menu_option); 175 | break; 176 | case DPAD_UP: 177 | /* Switch between settings */ 178 | app_state->selected_menu_option = (app_state->selected_menu_option - 1 + MENU_OPTION_COUNT) % MENU_OPTION_COUNT; 179 | break; 180 | case DPAD_DOWN: 181 | /* Switch between settings */ 182 | app_state->selected_menu_option = (app_state->selected_menu_option + 1 + MENU_OPTION_COUNT) % MENU_OPTION_COUNT; 183 | break; 184 | default: 185 | break; 186 | } 187 | } 188 | break; 189 | } 190 | } 191 | void handle_change_setting(AppState *app_state, int change) 192 | { 193 | LedSettings *selected_led_settings = &app_state->led_settings[app_state->selected_led]; 194 | switch (app_state->selected_setting) 195 | { 196 | case SELECTED_LED: 197 | { 198 | app_state->selected_led = (app_state->selected_led - change + LED_COUNT) % LED_COUNT; 199 | break; 200 | } 201 | case BRIGHTNESS: 202 | { 203 | /* Bounds checking because looping around from max brightness to 0 brightness feels bad. */ 204 | selected_led_settings->brightness = clamp(selected_led_settings->brightness + (change * BRIGHTNESS_INCREMENT), 0, MAX_BRIGHTNESS); 205 | break; 206 | } 207 | case EFFECT: 208 | selected_led_settings->effect = clamp(selected_led_settings->effect + change, 0, ANIMATION_EFFECT_COUNT - 1); 209 | break; 210 | case DURATION: 211 | selected_led_settings->duration = clamp(selected_led_settings->duration + DURATION_INCREMENT * change, 0, MAX_DURATION); 212 | break; 213 | case COLOR: 214 | { 215 | if (app_state->are_extended_colors_enabled) 216 | { 217 | selected_led_settings->color = next_color(selected_led_settings->color, change); 218 | } 219 | else 220 | { 221 | /* Find the current color index in colors */ 222 | int current_color_index = -1; 223 | for (int i = 0; i < num_color; i++) 224 | { 225 | if (colors[i] == selected_led_settings->color) 226 | { 227 | current_color_index = i; 228 | break; 229 | } 230 | } 231 | 232 | /* If the current color is not found, default to the first color */ 233 | if (current_color_index == -1) 234 | { 235 | current_color_index = 0; 236 | } 237 | 238 | /* Update the color index based on the change */ 239 | current_color_index = (current_color_index + change + num_color) % num_color; 240 | 241 | /* Set the new color */ 242 | selected_led_settings->color = colors[current_color_index]; 243 | } 244 | } 245 | break; 246 | case MATCH_SETTINGS: 247 | /* Change was from the user selecting the A button*/ 248 | if (change == 0) 249 | { 250 | color_match_leds(app_state); 251 | } 252 | break; 253 | default: 254 | break; 255 | } 256 | } 257 | 258 | void handle_menu_select(AppState *app_state, MenuOption selected_menu_option) 259 | { 260 | switch (selected_menu_option) 261 | { 262 | case ENABLE_ALL: 263 | turn_on_all_leds(app_state); 264 | break; 265 | case DISABLE_ALL: 266 | turn_off_all_leds(app_state); 267 | break; 268 | case TOGGLE_EXTENDED_COLORS: 269 | app_state->are_extended_colors_enabled = !app_state->are_extended_colors_enabled; 270 | break; 271 | case TOGGLE_LOW_BATTERY_INDICATION: 272 | app_state->should_enable_low_battery_indication = !app_state->should_enable_low_battery_indication; 273 | break; 274 | case UNINSTALL: 275 | uninstall_daemon(); 276 | app_state->should_install_daemon = false; 277 | app_state->should_quit = true; 278 | break; 279 | case QUIT: 280 | app_state->should_quit = true; 281 | break; 282 | default: 283 | break; 284 | } 285 | } 286 | 287 | void handle_event_updates(AppState *app_state, CoreSDLComponents *core_components, AdditionalSDLComponents *components, 288 | SelectableMenuItems *config_page_ui, SelectableMenuItems *menu_page_ui, 289 | InputType user_input, SDL_Event event) 290 | { 291 | handle_user_input(user_input, app_state, core_components->controller); 292 | 293 | if (is_supported_input_event(event.type)) 294 | { 295 | if (app_state->should_update_leds) 296 | { 297 | update_leds(app_state); 298 | } 299 | if (app_state->should_save_settings) 300 | { 301 | save_settings(app_state); 302 | } 303 | update_config_page_ui_text(config_page_ui, core_components, components, app_state); 304 | update_menu_ui_text(menu_page_ui, core_components, components, app_state); 305 | } 306 | } 307 | int initialize_additional_sdl_components(CoreSDLComponents *core_components, AdditionalSDLComponents *components) 308 | { 309 | /* Initialize SDL_image */ 310 | if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) 311 | { 312 | SDL_Log("SDL_image could not initialize! IMG_Error: %s\n", IMG_GetError()); 313 | SDL_DestroyRenderer(core_components->renderer); 314 | SDL_DestroyWindow(core_components->window); 315 | SDL_Quit(); 316 | return 1; 317 | } 318 | 319 | /* Initialize SDL_ttf */ 320 | if (TTF_Init() == -1) 321 | { 322 | SDL_Log("SDL_ttf could not initialize! TTF_Error: %s\n", TTF_GetError()); 323 | return 1; 324 | } 325 | 326 | /* Create a texture to render from the background image. */ 327 | components->backgroundTexture = create_sdl_texture_from_image(core_components->renderer, BACKGROUND_IMAGE_PATH); 328 | 329 | components->menuTexture = create_sdl_texture_from_image(core_components->renderer, MENU_IMAGE_PATH); 330 | 331 | if (!components->backgroundTexture || !components->menuTexture) 332 | { 333 | 334 | return 1; 335 | } 336 | 337 | /* Load font */ 338 | components->font = TTF_OpenFont("assets/retro_gaming.ttf", 32); /* Specify your font path */ 339 | if (!components->font) 340 | { 341 | SDL_Log("Failed to load font! TTF_Error: %s\n", TTF_GetError()); 342 | return 1; 343 | } 344 | 345 | return 0; 346 | } 347 | 348 | int initialize_app_state(AppState *app_state) 349 | { 350 | /* Begin with first LED selected. */ 351 | app_state->selected_led = LED_FRONT; 352 | app_state->selected_setting = BRIGHTNESS; 353 | app_state->should_save_settings = false; 354 | app_state->should_quit = false; 355 | app_state->should_install_daemon = true; 356 | app_state->are_extended_colors_enabled = false; 357 | app_state->should_enable_low_battery_indication = true; 358 | app_state->current_page = CONFIG_PAGE; 359 | app_state->selected_menu_option = ENABLE_ALL; 360 | 361 | /* Load LED settings from file. */ 362 | if (read_settings(app_state) != 0) 363 | { 364 | SDL_Log("Failed to read settings"); 365 | return 1; 366 | } 367 | return 0; 368 | } 369 | 370 | void initialize_config_page_ui(SelectableMenuItems *menu_items, CoreSDLComponents *core_components, AdditionalSDLComponents *components) 371 | { 372 | menu_items->text_color = (SDL_Color){255, 239, 186, 255}; 373 | menu_items->text_shadow_color = (SDL_Color){0, 0, 0, 128}; 374 | menu_items->text_highlight_color = (SDL_Color){255, 173, 99, 255}; 375 | menu_items->item_count = LED_SETTINGS_COUNT; 376 | menu_items->string_length = STRING_LENGTH; 377 | menu_items->menu_text = malloc(menu_items->item_count * sizeof(char *)); 378 | menu_items->menu_text_textures = malloc(menu_items->item_count * sizeof(SDL_Texture *)); 379 | 380 | for (int setting_index = 0; setting_index < menu_items->item_count; setting_index++) 381 | { 382 | menu_items->menu_text[setting_index] = malloc(menu_items->string_length * sizeof(char)); 383 | /* preload default strings */ 384 | snprintf(menu_items->menu_text[setting_index], menu_items->string_length, "%s", led_setting_option_to_string(setting_index)); 385 | 386 | /* create text textures from strings */ 387 | menu_items->menu_text_textures[setting_index] = create_text_texture(core_components->renderer, components->font, &menu_items->text_color, &menu_items->text_shadow_color, menu_items->menu_text[setting_index]); 388 | } 389 | } 390 | 391 | void initialize_menu_ui(SelectableMenuItems *menu_items, CoreSDLComponents *core_components, AdditionalSDLComponents *components, AppState *app_state) 392 | { 393 | menu_items->text_color = (SDL_Color){255, 239, 186, 255}; 394 | menu_items->text_shadow_color = (SDL_Color){0, 0, 0, 128}; 395 | menu_items->text_highlight_color = (SDL_Color){255, 173, 99, 255}; 396 | menu_items->item_count = MENU_OPTION_COUNT; 397 | menu_items->string_length = STRING_LENGTH; 398 | menu_items->menu_text = malloc(menu_items->item_count * sizeof(char *)); 399 | menu_items->menu_text_textures = malloc(menu_items->item_count * sizeof(SDL_Texture *)); 400 | 401 | for (int setting_index = 0; setting_index < menu_items->item_count; setting_index++) 402 | { 403 | menu_items->menu_text[setting_index] = malloc(menu_items->string_length * sizeof(char)); 404 | /* preload default strings */ 405 | snprintf(menu_items->menu_text[setting_index], menu_items->string_length, "%s", menu_option_to_string(setting_index, app_state)); 406 | 407 | /* create text textures from strings */ 408 | menu_items->menu_text_textures[setting_index] = create_text_texture(core_components->renderer, components->font, &menu_items->text_color, &menu_items->text_shadow_color, menu_items->menu_text[setting_index]); 409 | } 410 | } 411 | 412 | void free_menu_items(SelectableMenuItems *menu_items) 413 | { 414 | if (!menu_items) 415 | return; 416 | if (menu_items->menu_text_textures) 417 | { 418 | for (int item_index = 0; item_index < menu_items->item_count; item_index++) 419 | { 420 | if (menu_items->menu_text_textures[item_index]) 421 | { 422 | SDL_DestroyTexture(menu_items->menu_text_textures[item_index]); 423 | } 424 | } 425 | free(menu_items->menu_text_textures); 426 | } 427 | if (menu_items->menu_text) 428 | { 429 | for (int item_index = 0; item_index < menu_items->item_count; item_index++) 430 | { 431 | if (menu_items->menu_text[item_index]) 432 | { 433 | free(menu_items->menu_text[item_index]); 434 | } 435 | } 436 | free(menu_items->menu_text); 437 | } 438 | } 439 | 440 | void update_config_page_ui_text(SelectableMenuItems *menu_items, const CoreSDLComponents *core_components, const AdditionalSDLComponents *components, const AppState *app_state) 441 | { 442 | LedSettingOption selected_setting = app_state->selected_setting; 443 | for (LedSettingOption setting_index = 0; setting_index < LED_SETTINGS_COUNT; setting_index++) 444 | { 445 | switch (setting_index) 446 | { 447 | case SELECTED_LED: 448 | snprintf(menu_items->menu_text[setting_index], menu_items->string_length, "%sLED: %s%s", 449 | selected_setting == SELECTED_LED ? MENU_CARRET_LEFT : "", 450 | led_to_string(app_state->selected_led), 451 | selected_setting == SELECTED_LED ? MENU_CARRET_RIGHT : ""); 452 | break; 453 | case BRIGHTNESS: 454 | /* Write menu text to string */ 455 | snprintf(menu_items->menu_text[setting_index], 456 | menu_items->string_length, "%sBrightness: %d%s", selected_setting == BRIGHTNESS ? MENU_CARRET_LEFT : "", 457 | app_state->led_settings[app_state->selected_led].brightness / BRIGHTNESS_INCREMENT, 458 | selected_setting == BRIGHTNESS ? MENU_CARRET_RIGHT : ""); 459 | break; 460 | case EFFECT: 461 | snprintf(menu_items->menu_text[setting_index], 462 | menu_items->string_length, 463 | "%sEffect: %s%s", 464 | selected_setting == EFFECT ? MENU_CARRET_LEFT : "", 465 | animation_effect_to_string(app_state->led_settings[app_state->selected_led].effect), 466 | selected_setting == EFFECT ? MENU_CARRET_RIGHT : ""); 467 | break; 468 | case COLOR: 469 | snprintf(menu_items->menu_text[setting_index], 470 | menu_items->string_length, "%sColor: %s%s", 471 | selected_setting == COLOR ? MENU_CARRET_LEFT : "", 472 | color_to_string(app_state->led_settings[app_state->selected_led].color), 473 | selected_setting == COLOR ? MENU_CARRET_RIGHT : ""); 474 | break; 475 | case DURATION: 476 | snprintf(menu_items->menu_text[setting_index], 477 | menu_items->string_length, 478 | "%sDuration: %dms%s", 479 | selected_setting == DURATION ? MENU_CARRET_LEFT : "", 480 | app_state->led_settings[app_state->selected_led].duration, 481 | selected_setting == DURATION ? MENU_CARRET_RIGHT : ""); 482 | break; 483 | case MATCH_SETTINGS: 484 | snprintf(menu_items->menu_text[setting_index], 485 | menu_items->string_length, 486 | "Sync LED colors"); 487 | break; 488 | } 489 | 490 | /* Create texture from string */ 491 | menu_items->menu_text_textures[setting_index] = create_text_texture(core_components->renderer, components->font, selected_setting == setting_index ? &menu_items->text_highlight_color : &menu_items->text_color, &menu_items->text_shadow_color, menu_items->menu_text[setting_index]); 492 | } 493 | } 494 | 495 | void update_menu_ui_text(SelectableMenuItems *menu_items, const CoreSDLComponents *core_components, const AdditionalSDLComponents *components, const AppState *app_state) 496 | { 497 | MenuOption selected_menu_option = app_state->selected_menu_option; 498 | 499 | for (int menu_index = 0; menu_index < menu_items->item_count; menu_index++) 500 | { 501 | snprintf(menu_items->menu_text[menu_index], menu_items->string_length, "%s%s", selected_menu_option == menu_index ? ">>> " : "", 502 | menu_option_to_string(menu_index, app_state)); 503 | 504 | menu_items->menu_text_textures[menu_index] = create_text_texture(core_components->renderer, components->font, selected_menu_option == menu_index ? &menu_items->text_highlight_color : &menu_items->text_color, &menu_items->text_shadow_color, menu_items->menu_text[menu_index]); 505 | } 506 | } 507 | int read_settings(AppState *app_state) 508 | { 509 | FILE *file = fopen(SETTINGS_FILE, "r"); 510 | if (!file) 511 | { 512 | perror("fopen"); 513 | SDL_Log("Failed to open %s for reading", SETTINGS_FILE); 514 | return 1; 515 | } 516 | 517 | char line[STRING_LENGTH]; 518 | int led_index = -1; 519 | SDL_Log("Reading settings from %s ...", SETTINGS_FILE); 520 | 521 | while (fgets(line, sizeof(line), file)) 522 | { 523 | char led_name[STRING_LENGTH]; 524 | if (sscanf(line, "[%[^]]]", led_name) == 1) 525 | { 526 | if (strcmp(led_name, "global") == 0) 527 | { 528 | led_index = -2; // Special index for global settings 529 | } 530 | else 531 | { 532 | led_index = internal_led_name_to_led(led_name); 533 | } 534 | continue; 535 | } 536 | 537 | if (led_index == -2) 538 | { 539 | // Handle global settings 540 | int temp_value; 541 | if (sscanf(line, "should_enable_low_battery_indication=%d", &temp_value) == 1) 542 | { 543 | app_state->should_enable_low_battery_indication = (temp_value != 0) ? true : false; 544 | } 545 | } 546 | else if (led_index >= 0 && led_index < LED_COUNT) 547 | { 548 | sscanf(line, "brightness=%d", &app_state->led_settings[led_index].brightness); 549 | sscanf(line, "color=%x", &app_state->led_settings[led_index].color); 550 | sscanf(line, "duration=%d", &app_state->led_settings[led_index].duration); 551 | sscanf(line, "effect=%d", (int *)&app_state->led_settings[led_index].effect); 552 | 553 | /* Bounds checking */ 554 | app_state->led_settings[led_index].brightness = 555 | clamp(app_state->led_settings[led_index].brightness, 0, MAX_BRIGHTNESS); 556 | app_state->led_settings[led_index].color = 557 | clamp(app_state->led_settings[led_index].color, 0, 0xFFFFFF); 558 | app_state->led_settings[led_index].duration = 559 | clamp(app_state->led_settings[led_index].duration, 0, MAX_DURATION); 560 | app_state->led_settings[led_index].effect = 561 | clamp(app_state->led_settings[led_index].effect, 0, ANIMATION_EFFECT_COUNT - 1); 562 | } 563 | } 564 | SDL_Log("Settings read successfully"); 565 | 566 | fclose(file); 567 | return 0; 568 | } 569 | 570 | int save_settings(AppState *app_state) 571 | { 572 | app_state->should_save_settings = false; 573 | FILE *file = fopen(SETTINGS_FILE, "w"); 574 | if (!file) 575 | { 576 | perror("fopen"); 577 | SDL_Log("Failed to open %s for writing", SETTINGS_FILE); 578 | return 1; 579 | } 580 | 581 | SDL_Log("Saving settings to %s ...", SETTINGS_FILE); 582 | 583 | // Save global settings first 584 | fprintf(file, "[global]\n"); 585 | fprintf(file, "should_enable_low_battery_indication=%d\n\n", app_state->should_enable_low_battery_indication); 586 | 587 | // Save LED-specific settings 588 | for (int led_index = 0; led_index < LED_COUNT; led_index++) 589 | { 590 | fprintf(file, "[%s]\n", led_internal_name(led_index)); 591 | fprintf(file, "brightness=%d\n", app_state->led_settings[led_index].brightness); 592 | fprintf(file, "color=0x%06X\n", app_state->led_settings[led_index].color); 593 | fprintf(file, "duration=%d\n", app_state->led_settings[led_index].duration); 594 | fprintf(file, "effect=%d\n\n", app_state->led_settings[led_index].effect); 595 | } 596 | SDL_Log("Settings saved successfully"); 597 | 598 | fclose(file); 599 | return 0; 600 | } 601 | 602 | int initialize_brick_sprite(Sprite *brick_sprite, SDL_Renderer *renderer) 603 | { 604 | /* Load the brick sprite image to a texture */ 605 | 606 | brick_sprite->sprite_texture = create_sdl_texture_from_image(renderer, BRICK_SPRITE_SHEET_PATH); 607 | if (!brick_sprite->sprite_texture) 608 | { 609 | SDL_Log("Failed to load brick sprite texture from %s", BRICK_SPRITE_SHEET_PATH); 610 | return 1; 611 | } 612 | 613 | /* We have one animation for each LED. */ 614 | brick_sprite->animations = (AnimationInfo *)malloc(sizeof(AnimationInfo) * LED_COUNT); 615 | brick_sprite->animation_count = LED_COUNT; 616 | brick_sprite->sprite_width = BRICK_SPRITE_WIDTH; 617 | brick_sprite->sprite_height = BRICK_SPRITE_HEIGHT; 618 | brick_sprite->current_animation_index = 0; 619 | 620 | /* Define what frames to display for each LED option. */ 621 | static int front_frames[] = {0, 1, 2, 3, 2, 1, 0}; 622 | static int top_frames[] = {4, 5, 6, 8, 6, 5, 4}; 623 | static int back_frames[] = {4, 5, 6, 7, 6, 5, 4}; 624 | static int frame_durations[] = {150, 150, 150, 500, 150, 150, 150}; 625 | 626 | brick_sprite->animations[LED_FRONT] = (AnimationInfo){front_frames, frame_durations, 7, 0, 0}; 627 | brick_sprite->animations[LED_TOP] = (AnimationInfo){top_frames, frame_durations, 7, 0, 0}; 628 | brick_sprite->animations[LED_BACK] = (AnimationInfo){back_frames, frame_durations, 7, 0, 0}; 629 | return 0; 630 | } 631 | 632 | void render_frame(AppState *app_state, CoreSDLComponents *core_components, AdditionalSDLComponents *components, Sprite *brick_sprite, SelectableMenuItems *config_page_ui, SelectableMenuItems *menu_page_ui, bool verbose_logging_enabled) 633 | { 634 | /* Clear screen */ 635 | SDL_RenderClear(core_components->renderer); 636 | 637 | /* Render background texture */ 638 | SDL_RenderCopy(core_components->renderer, components->backgroundTexture, NULL, NULL); 639 | 640 | /* Render the user-selected color in front of a black square */ 641 | render_colored_square(app_state, core_components); 642 | 643 | char log_message[STRING_LENGTH]; 644 | snprintf(log_message, sizeof(log_message), "Selected LED: %d\n", app_state->selected_led); 645 | debug_log(log_message, verbose_logging_enabled); 646 | 647 | /* Render brick sprite */ 648 | brick_sprite->current_animation_index = app_state->selected_led; 649 | update_sprite_render(core_components->renderer, brick_sprite, 10, 99); 650 | 651 | /* Render the interactable user interface. */ 652 | render_menu_items(core_components->renderer, config_page_ui, 550, 150); 653 | 654 | if (app_state->current_page == MENU_PAGE) 655 | { /* Render menu last on stack if it needs to show*/ 656 | SDL_RenderCopy(core_components->renderer, components->menuTexture, NULL, NULL); 657 | render_menu_items(core_components->renderer, menu_page_ui, 200, 210); 658 | } 659 | /* Main render call to update screen */ 660 | SDL_RenderPresent(core_components->renderer); 661 | 662 | /* Delay to control frame rate Approximately 60 frames per second */ 663 | SDL_Delay(16); 664 | } 665 | 666 | void render_colored_square(AppState *app_state, CoreSDLComponents *core_components) 667 | { 668 | /* Adjusted to add 4 pixels on top */ 669 | SDL_Rect black_rect = {20, 96, BRICK_SPRITE_WIDTH, BRICK_SPRITE_HEIGHT + 4}; 670 | /* Black color */ 671 | SDL_SetRenderDrawColor(core_components->renderer, 0, 0, 0, 255); 672 | SDL_RenderFillRect(core_components->renderer, &black_rect); 673 | /* Adjusted to match the new black_rect */ 674 | 675 | SDL_Rect color_rect = {25, 101, BRICK_SPRITE_WIDTH - 10, BRICK_SPRITE_HEIGHT - 6}; 676 | int32_t color = app_state->led_settings[app_state->selected_led].color; 677 | SDL_SetRenderDrawColor(core_components->renderer, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, 255); 678 | SDL_RenderFillRect(core_components->renderer, &color_rect); 679 | } 680 | 681 | void render_text_texture(SDL_Renderer *renderer, SDL_Texture *texture, int x, int y) 682 | { 683 | int text_width, text_height; 684 | SDL_QueryTexture(texture, NULL, NULL, &text_width, &text_height); 685 | SDL_Rect dstrect = {x, y, text_width, text_height}; 686 | SDL_RenderCopy(renderer, texture, NULL, &dstrect); 687 | } 688 | 689 | void render_menu_items(SDL_Renderer *renderer, SelectableMenuItems *menu_items, int start_x, int start_y) 690 | { 691 | /* x and y UI element spacing. */ 692 | const int x_offset = 20; 693 | const int y_offset = 60; 694 | for (int item_index = 0; item_index < menu_items->item_count; item_index++) 695 | { 696 | render_text_texture(renderer, menu_items->menu_text_textures[item_index], start_x + x_offset, start_y + y_offset * item_index); 697 | } 698 | } 699 | 700 | SDL_Surface *load_image(const char *image_name) 701 | { 702 | char image_path[STRING_LENGTH]; 703 | snprintf(image_path, sizeof(image_path), "%s/%s", IMAGE_DIR, image_name); 704 | SDL_Surface *surface = IMG_Load(image_path); 705 | if (!surface) 706 | { 707 | SDL_Log("Unable to load image %s ! IMG_Error: %s\n", image_name, IMG_GetError()); 708 | } 709 | return surface; 710 | } 711 | 712 | void write_max_scale_data(FILE *file, const AppState *app_state, const Led led, char *filepath) 713 | { 714 | if (led == LED_TOP) 715 | { 716 | snprintf(filepath, STRING_LENGTH, "%s/max_scale", SYS_FILE_PATH); 717 | } 718 | else 719 | { 720 | snprintf(filepath, STRING_LENGTH, "%s/max_scale_%s", SYS_FILE_PATH, led_internal_name(led)); 721 | } 722 | 723 | file = fopen(filepath, "w"); 724 | if (file != NULL) 725 | { 726 | fprintf(file, "%d\n", app_state->led_settings[led].brightness); 727 | fclose(file); 728 | } 729 | else 730 | { 731 | SDL_Log("Failed to open file: %s", filepath); 732 | } 733 | } 734 | 735 | void write_effect_data(FILE *file, const AppState *app_state, const Led led, char *filepath) 736 | { 737 | const char *led_suffix[1] = {led_internal_name(led)}; 738 | const char **suffix_array = (led == LED_FRONT) ? front_led_suffix : led_suffix; 739 | 740 | for (int suffix_index = 0; suffix_index < (led == LED_FRONT ? 2 : 1); suffix_index++) 741 | { 742 | snprintf(filepath, STRING_LENGTH, "%s/effect_%s", SYS_FILE_PATH, suffix_array[suffix_index]); 743 | file = fopen(filepath, "w"); 744 | if (file != NULL) 745 | { 746 | fprintf(file, "%d\n", app_state->led_settings[led].effect); 747 | fclose(file); 748 | } 749 | else 750 | { 751 | SDL_Log("Failed to open file: %s", filepath); 752 | } 753 | } 754 | } 755 | 756 | void write_effect_duration_data(FILE *file, const AppState *app_state, const Led led, char *filepath) 757 | { 758 | const char *led_suffix[1] = {led_internal_name(led)}; 759 | const char **suffix_array = (led == LED_FRONT) ? front_led_suffix : led_suffix; 760 | 761 | for (int suffix_index = 0; suffix_index < (led == LED_FRONT ? 2 : 1); suffix_index++) 762 | { 763 | snprintf(filepath, STRING_LENGTH, "%s/effect_duration_%s", SYS_FILE_PATH, suffix_array[suffix_index]); 764 | file = fopen(filepath, "w"); 765 | if (file != NULL) 766 | { 767 | fprintf(file, "%d\n", app_state->led_settings[led].duration); 768 | fclose(file); 769 | } 770 | else 771 | { 772 | SDL_Log("Failed to open file: %s", filepath); 773 | } 774 | } 775 | } 776 | 777 | void write_color_data(FILE *file, const AppState *app_state, const Led led, char *filepath) 778 | { 779 | const char *led_suffix[1] = {led_internal_name(led)}; 780 | const char **suffix_array = (led == LED_FRONT) ? front_led_suffix : led_suffix; 781 | 782 | for (int suffix_index = 0; suffix_index < (led == LED_FRONT ? 2 : 1); suffix_index++) 783 | { 784 | snprintf(filepath, STRING_LENGTH, "%s/effect_rgb_hex_%s", SYS_FILE_PATH, suffix_array[suffix_index]); 785 | file = fopen(filepath, "w"); 786 | if (file != NULL) 787 | { 788 | 789 | fprintf(file, "%06X\n", app_state->led_settings[led].color); 790 | 791 | fclose(file); 792 | } 793 | else 794 | { 795 | SDL_Log("Failed to open file: %s", filepath); 796 | } 797 | } 798 | } 799 | 800 | void update_leds(AppState *app_state) 801 | { 802 | char filepath[STRING_LENGTH]; 803 | FILE *file = NULL; 804 | 805 | app_state->should_update_leds = false; 806 | for (Led led = 0; led < LED_COUNT; led++) 807 | { 808 | write_max_scale_data(file, app_state, led, filepath); 809 | write_color_data(file, app_state, led, filepath); 810 | write_effect_duration_data(file, app_state, led, filepath); 811 | write_effect_data(file, app_state, led, filepath); 812 | } 813 | } 814 | 815 | void install_daemon() 816 | { 817 | system("sh scripts/install.sh"); 818 | } 819 | 820 | void uninstall_daemon() 821 | { 822 | system("sh scripts/uninstall.sh"); 823 | } 824 | 825 | void color_match_leds(AppState *app_state) 826 | { 827 | for (Led led = 0; led < LED_COUNT; led++) 828 | { 829 | app_state->led_settings[led].color = app_state->led_settings[app_state->selected_led].color; 830 | } 831 | } 832 | 833 | void turn_off_all_leds(AppState *app_state) 834 | { 835 | for (Led led = 0; led < LED_COUNT; led++) 836 | { 837 | app_state->led_settings[led].brightness = 0; 838 | app_state->led_settings[led].effect = DISABLE; 839 | } 840 | update_leds(app_state); 841 | system("sh scripts/turn_off_all_leds.sh"); 842 | } 843 | 844 | void turn_on_all_leds(AppState *app_state) 845 | { 846 | for (Led led = 0; led < LED_COUNT; led++) 847 | { 848 | app_state->led_settings[led].brightness = MAX_BRIGHTNESS; 849 | app_state->led_settings[led].effect = STATIC; 850 | } 851 | update_leds(app_state); 852 | system("sh scripts/turn_on_all_leds.sh"); 853 | } 854 | 855 | int teardown(CoreSDLComponents *core_components, AdditionalSDLComponents *components, 856 | SelectableMenuItems *config_menu_items, SelectableMenuItems *main_menu_items, Sprite *brick_sprite, AppState *app_state) 857 | { 858 | free_menu_items(config_menu_items); 859 | free_menu_items(main_menu_items); 860 | free_sprite(brick_sprite); 861 | free_sdl_core(core_components); 862 | SDL_DestroyTexture(components->backgroundTexture); 863 | SDL_DestroyTexture(components->menuTexture); 864 | TTF_CloseFont(components->font); 865 | IMG_Quit(); 866 | TTF_Quit(); 867 | SDL_Quit(); 868 | return app_state->should_install_daemon ? 0 : 66; 869 | } 870 | -------------------------------------------------------------------------------- /workspace/src/led_controller_common.c: -------------------------------------------------------------------------------- 1 | #include "led_controller_common.h" 2 | 3 | const char *animation_effect_to_string(AnimationEffect effect) 4 | { 5 | switch (effect) 6 | { 7 | case DISABLE: 8 | return "Disabled"; 9 | case LINEAR: 10 | return "Linear"; 11 | case BREATH: 12 | return "Breath"; 13 | case SNIFF: 14 | return "Sniff"; 15 | case STATIC: 16 | return "Static"; 17 | case BLINK1: 18 | return "Blink 1"; 19 | case BLINK2: 20 | return "Blink 2"; 21 | case BLINK3: 22 | return "Blink 3"; 23 | default: 24 | return "UNKNOWN"; 25 | } 26 | } 27 | 28 | const char *color_to_string(uint32_t color) 29 | { 30 | static char hex_color[STRING_LENGTH]; 31 | switch (color) 32 | { 33 | case 0xFF0000: 34 | return "Red"; 35 | case 0xFF8080: 36 | return "Light Red"; 37 | case 0x800000: 38 | return "Maroon"; 39 | case 0xFF0080: 40 | return "Hot Pink"; 41 | case 0xFF8000: 42 | return "Orange"; 43 | case 0x00FF00: 44 | return "Green"; 45 | case 0x00FF80: 46 | return "Lime"; 47 | case 0xFFFF00: 48 | return "Yellow"; 49 | case 0x808000: 50 | return "Olive"; 51 | case 0x0000FF: 52 | return "Blue"; 53 | case 0x0080FF: 54 | return "Light Blue"; 55 | case 0x000080: 56 | return "Navy"; 57 | case 0x00FFFF: 58 | return "Cyan"; 59 | case 0x008080: 60 | return "Teal"; 61 | case 0xFF00FF: 62 | return "Magenta"; 63 | case 0xFF80C0: 64 | return "Pink"; 65 | case 0xFFFFFF: 66 | return "White"; 67 | default: 68 | snprintf(hex_color, sizeof(hex_color), "0x%06X", color); 69 | return hex_color; 70 | } 71 | } 72 | 73 | uint32_t next_color(uint32_t color, int sign) 74 | { 75 | int16_t r = (color >> 16) & 0xFF; 76 | int16_t g = (color >> 8) & 0xFF; 77 | int16_t b = color & 0xFF; 78 | 79 | // Red -> Yellow -> Green -> Cyan -> Blue -> Magenta -> Red 80 | if (r == 255 && g < 255 && b == 0) 81 | { // Red to Yellow 82 | g = (g + sign * COLOR_CYLCE_INCREMENT); 83 | if (g > 255) 84 | g = 255; 85 | if (g < 0) 86 | g = 0; 87 | } 88 | else if (r > 0 && g == 255 && b == 0) 89 | { // Yellow to Green 90 | r = (r - sign * COLOR_CYLCE_INCREMENT); 91 | if (r > 255) 92 | r = 255; 93 | if (r < 0) 94 | r = 0; 95 | } 96 | else if (g == 255 && r == 0 && b < 255) 97 | { // Green to Cyan 98 | b = (b + sign * COLOR_CYLCE_INCREMENT); 99 | if (b > 255) 100 | b = 255; 101 | if (b < 0) 102 | b = 0; 103 | } 104 | else if (g > 0 && b == 255 && r == 0) 105 | { // Cyan to Blue 106 | g = (g - sign * COLOR_CYLCE_INCREMENT); 107 | if (g > 255) 108 | g = 255; 109 | if (g < 0) 110 | g = 0; 111 | } 112 | else if (b == 255 && g == 0 && r < 255) 113 | { // Blue to Magenta 114 | r = (r + sign * COLOR_CYLCE_INCREMENT); 115 | if (r > 255) 116 | r = 255; 117 | if (r < 0) 118 | r = 0; 119 | } 120 | else if (r == 255 && g == 0 && b > 0) 121 | { // Magenta to Red 122 | b = (b - sign * COLOR_CYLCE_INCREMENT); 123 | if (b > 255) 124 | b = 255; 125 | if (b < 0) 126 | b = 0; 127 | } 128 | else 129 | { // Initialize to red if no valid state 130 | r = 255; 131 | g = 0; 132 | b = 0; 133 | } 134 | return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; 135 | } 136 | 137 | const char *led_setting_option_to_string(LedSettingOption setting) 138 | { 139 | switch (setting) 140 | { 141 | case SELECTED_LED: 142 | return "LED: "; 143 | break; 144 | case BRIGHTNESS: 145 | return "Brightness: "; 146 | case EFFECT: 147 | return "Effect: "; 148 | case COLOR: 149 | return "Color "; 150 | case DURATION: 151 | return "Duration: "; 152 | default: 153 | return "UNKNOWN "; 154 | } 155 | } 156 | 157 | const char *menu_option_to_string(MenuOption option, const AppState *app_state) 158 | { 159 | switch (option) 160 | { 161 | case TOGGLE_EXTENDED_COLORS: 162 | if (app_state->are_extended_colors_enabled) 163 | return "Disable extended colors"; 164 | else 165 | return "Enable extended colors"; 166 | case TOGGLE_LOW_BATTERY_INDICATION: 167 | if (app_state->should_enable_low_battery_indication) 168 | return "Disable low battery warning"; 169 | else 170 | return "Enable low battery warning"; 171 | case ENABLE_ALL: 172 | return "Turn ON all LEDs"; 173 | case DISABLE_ALL: 174 | return "Turn OFF all LEDs"; 175 | case UNINSTALL: 176 | return "Uninstall"; 177 | case QUIT: 178 | return "Quit"; 179 | default: 180 | return "UNKNOWN"; 181 | } 182 | } 183 | 184 | const char *led_to_string(Led led) 185 | { 186 | switch (led) 187 | { 188 | case LED_FRONT: 189 | return " Front LED "; 190 | case LED_TOP: 191 | return " Top LED "; 192 | case LED_BACK: 193 | return " Back LED "; 194 | default: 195 | return "UNKNOWN"; 196 | } 197 | } 198 | 199 | const char *led_internal_name(Led led) 200 | { 201 | switch (led) 202 | { 203 | case LED_FRONT: 204 | return "f1f2"; 205 | case LED_TOP: 206 | return "m"; 207 | case LED_BACK: 208 | return "lr"; 209 | default: 210 | return "UNKNOWN"; 211 | } 212 | } 213 | 214 | Led internal_led_name_to_led(const char *led_name) 215 | { 216 | if (strcmp(led_name, "f1f2") == 0) 217 | { 218 | return LED_FRONT; 219 | } 220 | else if (strcmp(led_name, "m") == 0) 221 | { 222 | return LED_TOP; 223 | } 224 | else if (strcmp(led_name, "lr") == 0) 225 | { 226 | return LED_BACK; 227 | } 228 | else 229 | { 230 | return -1; 231 | } 232 | } 233 | 234 | void debug_log(const char *message, bool verbose_logging_enabled) 235 | { 236 | if (verbose_logging_enabled) 237 | { 238 | printf("%s\n", message); 239 | } 240 | } 241 | 242 | int clamp(int value, int min, int max) 243 | { 244 | if (value < min) 245 | { 246 | return min; 247 | } 248 | else if (value > max) 249 | { 250 | return max; 251 | } 252 | else 253 | { 254 | return value; 255 | } 256 | } -------------------------------------------------------------------------------- /workspace/src/sdl_base.c: -------------------------------------------------------------------------------- 1 | #include "sdl_base.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | int initialize_sdl_core(CoreSDLComponents *core_components, char *window_title) 8 | { 9 | if (window_title == NULL) 10 | { 11 | window_title = ""; 12 | } 13 | /* Initialize SDL */ 14 | if (SDL_Init(SDL_INIT_VIDEO) < 0) 15 | { 16 | SDL_Log("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); 17 | return 1; 18 | } 19 | 20 | /* Initialize window */ 21 | core_components->window = SDL_CreateWindow(window_title, 22 | SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 23 | core_components->window_width, core_components->window_height, 24 | SDL_WINDOW_SHOWN); 25 | if (!core_components->window) 26 | { 27 | SDL_Log("Window could not be created! SDL_Error: %s\n", SDL_GetError()); 28 | SDL_Quit(); 29 | return 1; 30 | } 31 | 32 | /* Initialize renderer */ 33 | core_components->renderer = SDL_CreateRenderer(core_components->window, -1, SDL_RENDERER_ACCELERATED); 34 | if (!core_components->renderer) 35 | { 36 | SDL_Log("Renderer could not be created! SDL_Error: %s\n", SDL_GetError()); 37 | SDL_DestroyWindow(core_components->window); 38 | SDL_Quit(); 39 | return 1; 40 | } 41 | 42 | /* Initialize controller */ 43 | if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) < 0) 44 | { 45 | SDL_Log("Unable to initialize SDL: %s", SDL_GetError()); 46 | return 1; 47 | } 48 | 49 | core_components->controller = NULL; 50 | for (int i = 0; i < SDL_NumJoysticks(); ++i) 51 | { 52 | if (SDL_IsGameController(i)) 53 | { 54 | core_components->controller = SDL_GameControllerOpen(i); 55 | if (core_components->controller) 56 | { 57 | SDL_Log("Game Controller %s connected", SDL_GameControllerName(core_components->controller)); 58 | break; 59 | } 60 | } 61 | } 62 | 63 | if (!core_components->controller) 64 | { 65 | SDL_Log("No game controller available"); 66 | } 67 | 68 | /* Clear screen */ 69 | SDL_RenderClear(core_components->renderer); 70 | 71 | return 0; 72 | } 73 | 74 | void free_sdl_core(CoreSDLComponents *core_components) 75 | { 76 | if (core_components->controller != NULL) 77 | { 78 | SDL_GameControllerClose(core_components->controller); 79 | } 80 | if (core_components->renderer != NULL) 81 | { 82 | SDL_DestroyRenderer(core_components->renderer); 83 | } 84 | if (core_components->window != NULL) 85 | { 86 | SDL_DestroyWindow(core_components->window); 87 | } 88 | } 89 | 90 | void free_sprite(Sprite *sprite) 91 | { 92 | if (sprite == NULL) 93 | { /* Nothing to clean up if sprite is NULL */ 94 | return; 95 | } 96 | 97 | if (sprite->sprite_texture != NULL) 98 | { 99 | SDL_DestroyTexture(sprite->sprite_texture); 100 | sprite->sprite_texture = NULL; 101 | } 102 | 103 | if (sprite->animations != NULL) 104 | { 105 | free(sprite->animations); 106 | sprite->animations = NULL; 107 | } 108 | } 109 | 110 | void update_sprite_render(SDL_Renderer *renderer, Sprite *sprite, int position_x, int position_y) 111 | { 112 | /* Get the current animation and frame to render */ 113 | AnimationInfo *current_animation = &sprite->animations[sprite->current_animation_index]; 114 | int sprite_sheet_offset = current_animation->frame_indicies[current_animation->current_frame_index]; 115 | int frame_duration = current_animation->frame_duration_millis[current_animation->current_frame_index]; 116 | 117 | /* Offset the frame index by the width of the sprite to display the next frame */ 118 | SDL_Rect src_rect = {sprite_sheet_offset * sprite->sprite_width, 0, sprite->sprite_width, sprite->sprite_height}; 119 | 120 | /* Position the sprite at x, y with the width and height of the sprite and copy to the renderer. */ 121 | SDL_Rect dst_rect = {position_x, position_y, sprite->sprite_width, sprite->sprite_height}; 122 | SDL_RenderCopy(renderer, sprite->sprite_texture, &src_rect, &dst_rect); 123 | 124 | /* Update the frame index for the next sprite */ 125 | Uint32 current_time_millis = SDL_GetTicks(); 126 | if (current_time_millis - current_animation->last_frame_time_millis >= frame_duration) 127 | { 128 | current_animation->current_frame_index = (current_animation->current_frame_index + 1) % current_animation->frame_count; 129 | current_animation->last_frame_time_millis = current_time_millis; 130 | } 131 | } 132 | 133 | SDL_Texture *create_sdl_texture_from_image(SDL_Renderer *renderer, const char *full_image_path) 134 | { 135 | SDL_Surface *surface = IMG_Load(full_image_path); 136 | if (!surface) 137 | { 138 | SDL_Log("Unable to load image %s ! IMG_Error: %s\n", full_image_path, IMG_GetError()); 139 | return NULL; 140 | } 141 | 142 | SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); 143 | SDL_FreeSurface(surface); 144 | if (!texture) 145 | { 146 | SDL_Log("Unable to create image texture! SDL_Error: %s\n", SDL_GetError()); 147 | } 148 | return texture; 149 | } 150 | 151 | SDL_Texture *create_text_texture(SDL_Renderer *renderer, TTF_Font *font, const SDL_Color *text_color, const SDL_Color *shadow_color, const char *text) 152 | { 153 | /* Define colors for shadow and main text */ 154 | if (shadow_color == NULL) 155 | { 156 | SDL_Color default_shadow_color = {0, 0, 0, 1}; 157 | shadow_color = &default_shadow_color; 158 | } 159 | SDL_Surface *shadow_surface = TTF_RenderText_Solid(font, text, *shadow_color); 160 | if (!shadow_surface) 161 | { 162 | return NULL; 163 | } 164 | 165 | /* Render main text surface */ 166 | SDL_Surface *text_surface = TTF_RenderText_Solid(font, text, *text_color); 167 | if (!text_surface) 168 | { 169 | SDL_FreeSurface(text_surface); 170 | return NULL; 171 | } 172 | 173 | /* Create a larger surface to combine shadow and main text */ 174 | int shadow_offset = 4; 175 | int width = text_surface->w + shadow_offset; 176 | int height = text_surface->h + shadow_offset; 177 | SDL_Surface *combined_surface = SDL_CreateRGBSurface(0, width, height, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); 178 | 179 | if (!combined_surface) 180 | { 181 | SDL_FreeSurface(shadow_surface); 182 | SDL_FreeSurface(text_surface); 183 | return NULL; 184 | } 185 | 186 | /* Blit the shadow first (offset by shadow_offset) */ 187 | SDL_Rect shadow_rect = {shadow_offset, shadow_offset, shadow_surface->w, shadow_surface->h}; 188 | SDL_BlitSurface(shadow_surface, NULL, combined_surface, &shadow_rect); 189 | 190 | /* Blit the main text on top (without offset) */ 191 | SDL_Rect text_rect = {0, 0, text_surface->w, text_surface->h}; 192 | SDL_BlitSurface(text_surface, NULL, combined_surface, &text_rect); 193 | 194 | /* Create texture from combined surface */ 195 | SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, combined_surface); 196 | 197 | /* Free the surfaces */ 198 | SDL_FreeSurface(shadow_surface); 199 | SDL_FreeSurface(text_surface); 200 | SDL_FreeSurface(combined_surface); 201 | 202 | return texture; 203 | } 204 | 205 | InputType sdl_event_to_input_type(SDL_Event *event, bool verbose) 206 | { 207 | if (is_supported_input_event(event->type)) 208 | { 209 | // Handle keyboard events 210 | if (event->type == SDL_KEYDOWN) 211 | { 212 | if (verbose) 213 | { 214 | SDL_Log("Keydown event: %d|0x%x \n", event->key.keysym.sym, event->key.keysym.sym); 215 | } 216 | switch (event->key.keysym.sym) 217 | { 218 | case SDLK_POWER: 219 | return POWER; 220 | case SDLK_ESCAPE: 221 | return START; 222 | case SDLK_UP: 223 | case SDLK_w: 224 | return DPAD_UP; 225 | case SDLK_LEFT: 226 | case SDLK_a: 227 | return DPAD_LEFT; 228 | case SDLK_DOWN: 229 | case SDLK_s: 230 | return DPAD_DOWN; 231 | case SDLK_RIGHT: 232 | case SDLK_d: 233 | return DPAD_RIGHT; 234 | case SDLK_RETURN: 235 | case SDLK_KP_ENTER: 236 | case SDLK_SPACE: 237 | case SDLK_x: 238 | return A; 239 | case SDLK_BACKSPACE: 240 | case SDLK_DELETE: 241 | case SDLK_z: 242 | return B; 243 | case SDLK_c: 244 | return X; 245 | case SDLK_v: 246 | return Y; 247 | case SDLK_TAB: 248 | return SELECT; 249 | case SDLK_PERIOD: 250 | return R1; 251 | case SDLK_COMMA: 252 | return L1; 253 | case SDLK_k: 254 | return L2; 255 | case SDLK_l: 256 | return R2; 257 | default: 258 | return UNKNOWN; 259 | } 260 | } 261 | else if (event->type == SDL_MOUSEBUTTONDOWN) 262 | { 263 | if (verbose) 264 | { 265 | SDL_Log("Mouse Button down event: %d|0x%x \n", event->button.button, event->button.button); 266 | } 267 | 268 | switch (event->button.button) 269 | { 270 | case SDL_BUTTON_LEFT: 271 | return A; 272 | case SDL_BUTTON_RIGHT: 273 | return B; 274 | default: 275 | return UNKNOWN; 276 | } 277 | } 278 | else if (event->type == SDL_CONTROLLERBUTTONDOWN) 279 | { 280 | if (verbose) 281 | { 282 | SDL_Log("Controller button down event: %d|0x%x \n", event->cbutton.button, event->cbutton.button); 283 | } 284 | switch (event->cbutton.button) 285 | { 286 | case SDL_CONTROLLER_BUTTON_A: // Trimui Brick buttons are backwards :( 287 | return B; 288 | case SDL_CONTROLLER_BUTTON_B: // Trimui Brick buttons are backwards :( 289 | return A; 290 | case SDL_CONTROLLER_BUTTON_X: 291 | return Y; 292 | case SDL_CONTROLLER_BUTTON_Y: // Trimui Brick buttons are backwards :( 293 | return X; 294 | case SDL_CONTROLLER_BUTTON_DPAD_UP: 295 | return DPAD_UP; 296 | case SDL_CONTROLLER_BUTTON_DPAD_DOWN: 297 | return DPAD_DOWN; 298 | case SDL_CONTROLLER_BUTTON_DPAD_LEFT: 299 | return DPAD_LEFT; 300 | case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: 301 | return DPAD_RIGHT; 302 | case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: 303 | return L1; 304 | case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: 305 | return R1; 306 | case SDL_CONTROLLER_BUTTON_LEFTSTICK: 307 | return L3; 308 | case SDL_CONTROLLER_BUTTON_RIGHTSTICK: 309 | return R3; 310 | case SDL_CONTROLLER_BUTTON_START: 311 | return START; 312 | case SDL_CONTROLLER_BUTTON_BACK: 313 | return SELECT; 314 | case SDL_CONTROLLER_BUTTON_GUIDE: 315 | return MENU; 316 | default: 317 | return UNKNOWN; 318 | } 319 | } 320 | } 321 | return UNKNOWN; 322 | }; 323 | 324 | bool is_supported_input_event(Uint32 event_type) 325 | { 326 | const Uint32 supported_events[] = { 327 | SDL_KEYDOWN, 328 | SDL_MOUSEBUTTONDOWN, 329 | SDL_CONTROLLERBUTTONDOWN, 330 | // TODO: Add control stick support 331 | // SDL_JOYAXISMOTION, 332 | // SDL_JOYBALLMOTION, 333 | // SDL_JOYHATMOTION, 334 | // SDL_JOYBUTTONDOWN, 335 | // SDL_JOYBUTTONUP, 336 | SDL_QUIT}; 337 | const int num_supported_events = sizeof(supported_events) / sizeof(supported_events[0]); 338 | 339 | for (int i = 0; i < num_supported_events; ++i) 340 | { 341 | if (event_type == supported_events[i]) 342 | { 343 | return true; 344 | } 345 | } 346 | return false; 347 | } 348 | 349 | const char *get_input_type_name(Uint32 inputType) 350 | { 351 | switch (inputType) 352 | { 353 | case START: 354 | return "START"; 355 | case SELECT: 356 | return "SELECT"; 357 | case MENU: 358 | return "MENU"; 359 | case POWER: 360 | return "POWER"; 361 | case DPAD_LEFT: 362 | return "DPAD_LEFT"; 363 | case DPAD_RIGHT: 364 | return "DPAD_RIGHT"; 365 | case DPAD_UP: 366 | return "DPAD_UP"; 367 | case DPAD_DOWN: 368 | return "DPAD_DOWN"; 369 | case A: 370 | return "A"; 371 | case B: 372 | return "B"; 373 | case X: 374 | return "X"; 375 | case Y: 376 | return "Y"; 377 | case L1: 378 | return "L1"; 379 | case R1: 380 | return "R1"; 381 | case L2: 382 | return "L2"; 383 | case R2: 384 | return "R2"; 385 | case L3: 386 | return "L3"; 387 | case R3: 388 | return "R3"; 389 | default: 390 | return "Unknown input type"; 391 | } 392 | } 393 | 394 | const char *get_event_name(Uint32 eventType) 395 | { 396 | switch (eventType) 397 | { 398 | case SDL_QUIT: 399 | return "SDL_QUIT"; 400 | case SDL_APP_TERMINATING: 401 | return "SDL_APP_TERMINATING"; 402 | case SDL_APP_LOWMEMORY: 403 | return "SDL_APP_LOWMEMORY"; 404 | case SDL_APP_WILLENTERBACKGROUND: 405 | return "SDL_APP_WILLENTERBACKGROUND"; 406 | case SDL_APP_DIDENTERBACKGROUND: 407 | return "SDL_APP_DIDENTERBACKGROUND"; 408 | case SDL_APP_WILLENTERFOREGROUND: 409 | return "SDL_APP_WILLENTERFOREGROUND"; 410 | case SDL_APP_DIDENTERFOREGROUND: 411 | return "SDL_APP_DIDENTERFOREGROUND"; 412 | case SDL_DISPLAYEVENT: 413 | return "SDL_DISPLAYEVENT"; 414 | case SDL_WINDOWEVENT: 415 | return "SDL_WINDOWEVENT"; 416 | case SDL_SYSWMEVENT: 417 | return "SDL_SYSWMEVENT"; 418 | case SDL_KEYDOWN: 419 | return "SDL_KEYDOWN"; 420 | case SDL_KEYUP: 421 | return "SDL_KEYUP"; 422 | case SDL_TEXTEDITING: 423 | return "SDL_TEXTEDITING"; 424 | case SDL_TEXTINPUT: 425 | return "SDL_TEXTINPUT"; 426 | case SDL_KEYMAPCHANGED: 427 | return "SDL_KEYMAPCHANGED"; 428 | case SDL_MOUSEMOTION: 429 | return "SDL_MOUSEMOTION"; 430 | case SDL_MOUSEBUTTONDOWN: 431 | return "SDL_MOUSEBUTTONDOWN"; 432 | case SDL_MOUSEBUTTONUP: 433 | return "SDL_MOUSEBUTTONUP"; 434 | case SDL_MOUSEWHEEL: 435 | return "SDL_MOUSEWHEEL"; 436 | case SDL_JOYAXISMOTION: 437 | return "SDL_JOYAXISMOTION"; 438 | case SDL_JOYBALLMOTION: 439 | return "SDL_JOYBALLMOTION"; 440 | case SDL_JOYHATMOTION: 441 | return "SDL_JOYHATMOTION"; 442 | case SDL_JOYBUTTONDOWN: 443 | return "SDL_JOYBUTTONDOWN"; 444 | case SDL_JOYBUTTONUP: 445 | return "SDL_JOYBUTTONUP"; 446 | case SDL_JOYDEVICEADDED: 447 | return "SDL_JOYDEVICEADDED"; 448 | case SDL_JOYDEVICEREMOVED: 449 | return "SDL_JOYDEVICEREMOVED"; 450 | case SDL_CONTROLLERAXISMOTION: 451 | return "SDL_CONTROLLERAXISMOTION"; 452 | case SDL_CONTROLLERBUTTONDOWN: 453 | return "SDL_CONTROLLERBUTTONDOWN"; 454 | case SDL_CONTROLLERBUTTONUP: 455 | return "SDL_CONTROLLERBUTTONUP"; 456 | case SDL_CONTROLLERDEVICEADDED: 457 | return "SDL_CONTROLLERDEVICEADDED"; 458 | case SDL_CONTROLLERDEVICEREMOVED: 459 | return "SDL_CONTROLLERDEVICEREMOVED"; 460 | case SDL_CONTROLLERDEVICEREMAPPED: 461 | return "SDL_CONTROLLERDEVICEREMAPPED"; 462 | case SDL_FINGERDOWN: 463 | return "SDL_FINGERDOWN"; 464 | case SDL_FINGERUP: 465 | return "SDL_FINGERUP"; 466 | case SDL_FINGERMOTION: 467 | return "SDL_FINGERMOTION"; 468 | case SDL_DOLLARGESTURE: 469 | return "SDL_DOLLARGESTURE"; 470 | case SDL_DOLLARRECORD: 471 | return "SDL_DOLLARRECORD"; 472 | case SDL_MULTIGESTURE: 473 | return "SDL_MULTIGESTURE"; 474 | case SDL_CLIPBOARDUPDATE: 475 | return "SDL_CLIPBOARDUPDATE"; 476 | case SDL_DROPFILE: 477 | return "SDL_DROPFILE"; 478 | case SDL_DROPTEXT: 479 | return "SDL_DROPTEXT"; 480 | case SDL_DROPBEGIN: 481 | return "SDL_DROPBEGIN"; 482 | case SDL_DROPCOMPLETE: 483 | return "SDL_DROPCOMPLETE"; 484 | case SDL_AUDIODEVICEADDED: 485 | return "SDL_AUDIODEVICEADDED"; 486 | case SDL_AUDIODEVICEREMOVED: 487 | return "SDL_AUDIODEVICEREMOVED"; 488 | case SDL_SENSORUPDATE: 489 | return "SDL_SENSORUPDATE"; 490 | case SDL_RENDER_TARGETS_RESET: 491 | return "SDL_RENDER_TARGETS_RESET"; 492 | case SDL_RENDER_DEVICE_RESET: 493 | return "SDL_RENDER_DEVICE_RESET"; 494 | case SDL_USEREVENT: 495 | return "SDL_USEREVENT"; 496 | case SDL_LASTEVENT: 497 | return "SDL_LASTEVENT"; 498 | default: 499 | return "Unknown event type"; 500 | } 501 | } 502 | 503 | const char *get_button_name(SDL_GameControllerButton button) 504 | { 505 | switch (button) 506 | { 507 | case SDL_CONTROLLER_BUTTON_A: 508 | return "SDL_CONTROLLER_BUTTON_A"; 509 | case SDL_CONTROLLER_BUTTON_B: 510 | return "SDL_CONTROLLER_BUTTON_B"; 511 | case SDL_CONTROLLER_BUTTON_X: 512 | return "SDL_CONTROLLER_BUTTON_X"; 513 | case SDL_CONTROLLER_BUTTON_Y: 514 | return "SDL_CONTROLLER_BUTTON_Y"; 515 | case SDL_CONTROLLER_BUTTON_BACK: 516 | return "SDL_CONTROLLER_BUTTON_BACK"; 517 | case SDL_CONTROLLER_BUTTON_GUIDE: 518 | return "SDL_CONTROLLER_BUTTON_GUIDE"; 519 | case SDL_CONTROLLER_BUTTON_START: 520 | return "SDL_CONTROLLER_BUTTON_START"; 521 | case SDL_CONTROLLER_BUTTON_LEFTSTICK: 522 | return "SDL_CONTROLLER_BUTTON_LEFTSTICK"; 523 | case SDL_CONTROLLER_BUTTON_RIGHTSTICK: 524 | return "SDL_CONTROLLER_BUTTON_RIGHTSTICK"; 525 | case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: 526 | return "SDL_CONTROLLER_BUTTON_LEFTSHOULDER"; 527 | case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: 528 | return "SDL_CONTROLLER_BUTTON_RIGHTSHOULDER"; 529 | case SDL_CONTROLLER_BUTTON_DPAD_UP: 530 | return "SDL_CONTROLLER_BUTTON_DPAD_UP"; 531 | case SDL_CONTROLLER_BUTTON_DPAD_DOWN: 532 | return "SDL_CONTROLLER_BUTTON_DPAD_DOWN"; 533 | case SDL_CONTROLLER_BUTTON_DPAD_LEFT: 534 | return "SDL_CONTROLLER_BUTTON_DPAD_LEFT"; 535 | case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: 536 | return "SDL_CONTROLLER_BUTTON_DPAD_RIGHT"; 537 | case SDL_CONTROLLER_BUTTON_INVALID: 538 | return "SDL_CONTROLLER_BUTTON_INVALID"; 539 | case SDL_CONTROLLER_BUTTON_MAX: 540 | return "SDL_CONTROLLER_BUTTON_MAX"; 541 | default: 542 | return "Unknown button"; 543 | } 544 | } 545 | 546 | const char *get_key_name(SDL_Keycode key) 547 | { 548 | switch (key) 549 | { 550 | case SDLK_RETURN: 551 | return "Return"; 552 | case SDLK_ESCAPE: 553 | return "Escape"; 554 | case SDLK_BACKSPACE: 555 | return "Backspace"; 556 | case SDLK_TAB: 557 | return "Tab"; 558 | case SDLK_SPACE: 559 | return "Space"; 560 | case SDLK_EXCLAIM: 561 | return "!"; 562 | case SDLK_QUOTEDBL: 563 | return "\""; 564 | case SDLK_HASH: 565 | return "#"; 566 | case SDLK_PERCENT: 567 | return "%"; 568 | case SDLK_DOLLAR: 569 | return "$"; 570 | case SDLK_AMPERSAND: 571 | return "&"; 572 | case SDLK_QUOTE: 573 | return "'"; 574 | case SDLK_LEFTPAREN: 575 | return "("; 576 | case SDLK_RIGHTPAREN: 577 | return ")"; 578 | case SDLK_ASTERISK: 579 | return "*"; 580 | case SDLK_PLUS: 581 | return "+"; 582 | case SDLK_COMMA: 583 | return ","; 584 | case SDLK_MINUS: 585 | return "-"; 586 | case SDLK_PERIOD: 587 | return "."; 588 | case SDLK_SLASH: 589 | return "/"; 590 | case SDLK_0: 591 | return "0"; 592 | case SDLK_1: 593 | return "1"; 594 | case SDLK_2: 595 | return "2"; 596 | case SDLK_3: 597 | return "3"; 598 | case SDLK_4: 599 | return "4"; 600 | case SDLK_5: 601 | return "5"; 602 | case SDLK_6: 603 | return "6"; 604 | case SDLK_7: 605 | return "7"; 606 | case SDLK_8: 607 | return "8"; 608 | case SDLK_9: 609 | return "9"; 610 | case SDLK_a: 611 | return "a"; 612 | case SDLK_b: 613 | return "b"; 614 | case SDLK_c: 615 | return "c"; 616 | case SDLK_d: 617 | return "d"; 618 | case SDLK_e: 619 | return "e"; 620 | case SDLK_f: 621 | return "f"; 622 | case SDLK_g: 623 | return "g"; 624 | case SDLK_h: 625 | return "h"; 626 | case SDLK_i: 627 | return "i"; 628 | case SDLK_j: 629 | return "j"; 630 | case SDLK_k: 631 | return "k"; 632 | case SDLK_l: 633 | return "l"; 634 | case SDLK_m: 635 | return "m"; 636 | case SDLK_n: 637 | return "n"; 638 | case SDLK_o: 639 | return "o"; 640 | case SDLK_p: 641 | return "p"; 642 | case SDLK_q: 643 | return "q"; 644 | case SDLK_r: 645 | return "r"; 646 | case SDLK_s: 647 | return "s"; 648 | case SDLK_t: 649 | return "t"; 650 | case SDLK_u: 651 | return "u"; 652 | case SDLK_v: 653 | return "v"; 654 | case SDLK_w: 655 | return "w"; 656 | case SDLK_x: 657 | return "x"; 658 | case SDLK_y: 659 | return "y"; 660 | case SDLK_z: 661 | return "z"; 662 | case SDLK_F1: 663 | return "F1"; 664 | case SDLK_F2: 665 | return "F2"; 666 | case SDLK_F3: 667 | return "F3"; 668 | case SDLK_F4: 669 | return "F4"; 670 | case SDLK_F5: 671 | return "F5"; 672 | case SDLK_F6: 673 | return "F6"; 674 | case SDLK_F7: 675 | return "F7"; 676 | case SDLK_F8: 677 | return "F8"; 678 | case SDLK_F9: 679 | return "F9"; 680 | case SDLK_F10: 681 | return "F10"; 682 | case SDLK_F11: 683 | return "F11"; 684 | case SDLK_F12: 685 | return "F12"; 686 | case SDLK_UP: 687 | return "Up Arrow"; 688 | case SDLK_DOWN: 689 | return "Down Arrow"; 690 | case SDLK_LEFT: 691 | return "Left Arrow"; 692 | case SDLK_RIGHT: 693 | return "Right Arrow"; 694 | case SDLK_INSERT: 695 | return "Insert"; 696 | case SDLK_DELETE: 697 | return "Delete"; 698 | case SDLK_HOME: 699 | return "Home"; 700 | case SDLK_END: 701 | return "End"; 702 | case SDLK_PAGEUP: 703 | return "Page Up"; 704 | case SDLK_PAGEDOWN: 705 | return "Page Down"; 706 | case SDLK_CAPSLOCK: 707 | return "Caps Lock"; 708 | case SDLK_LCTRL: 709 | return "Left Ctrl"; 710 | case SDLK_RCTRL: 711 | return "Right Ctrl"; 712 | case SDLK_LSHIFT: 713 | return "Left Shift"; 714 | case SDLK_RSHIFT: 715 | return "Right Shift"; 716 | case SDLK_LALT: 717 | return "Left Alt"; 718 | case SDLK_RALT: 719 | return "Right Alt"; 720 | case SDLK_LGUI: 721 | return "Left GUI (Windows/Command)"; 722 | case SDLK_RGUI: 723 | return "Right GUI (Windows/Command)"; 724 | case SDLK_PRINTSCREEN: 725 | return "Print Screen"; 726 | case SDLK_SCROLLLOCK: 727 | return "Scroll Lock"; 728 | case SDLK_PAUSE: 729 | return "Pause"; 730 | case SDLK_NUMLOCKCLEAR: 731 | return "Num Lock"; 732 | default: 733 | return "Unknown Key"; 734 | } 735 | } 736 | --------------------------------------------------------------------------------