├── .github └── workflows │ └── build-truenas.yml ├── .gitignore ├── LICENSE ├── README.md ├── SPECS ├── Makefile ├── kmod-led-ugreen.spec └── ugreen-led.conf ├── build-scripts ├── debian │ ├── Dockerfile │ ├── README.md │ ├── build-dkms-deb.sh │ ├── build-utils-deb.sh │ ├── build.sh │ └── docker-run.sh └── truenas │ ├── Dockerfile │ ├── README.md │ ├── build-all.sh │ ├── build-truenas-kmod.sh │ └── build.sh ├── cli ├── .clangd ├── Makefile ├── i2c.cpp ├── i2c.h ├── ugreen_leds.cpp ├── ugreen_leds.h └── ugreen_leds_cli.cpp ├── kmod ├── Makefile ├── dkms.conf ├── led-ugreen.c └── led-ugreen.h └── scripts ├── blink-disk.cpp ├── check-standby.cpp ├── systemd ├── ugreen-diskiomon.service ├── ugreen-netdevmon@.service └── ugreen-probe-leds.service ├── ugreen-diskiomon ├── ugreen-leds.conf ├── ugreen-netdevmon └── ugreen-probe-leds /.github/workflows/build-truenas.yml: -------------------------------------------------------------------------------- 1 | name: Build kernel module for TrueNAS 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | build-and-run: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | # Give the default GITHUB_TOKEN write permission to commit and push the changed files back to the repository. 17 | contents: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | ref: gh-actions 24 | 25 | - name: Build the kernel modules 26 | working-directory: build-scripts/truenas 27 | run: bash build-all.sh 28 | 29 | - name: Push results to GitHub 30 | uses: stefanzweifel/git-auto-commit-action@v5 31 | with: 32 | commit_message: "Add compiled modules" 33 | branch: gh-actions 34 | file_pattern: 'build-scripts/truenas/build/*/*/led-ugreen.ko' 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | dx4600_leds_cli 3 | ugreen_leds_cli 4 | run.sh 5 | zpool_leds.sh 6 | 7 | *.mod.c 8 | *.o 9 | *.cmd 10 | *.mod 11 | *.d 12 | modules.order 13 | compile_commands.json 14 | 15 | .cache 16 | Module.symvers 17 | release 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 miskcoo@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LED Controller for UGREEN's DX/DXP NAS Series 2 | == 3 | 4 | UGREEN's DX/DXP NAS Series covers 2 to 8 bay NAS devices with a built-in system based on OpenWRT called `UGOS` or Debian called `UGOS-Pro`. 5 | Debian Linux or dedicated NAS operating systems and appliances are compatible with the hardware, but do not have drivers for the LED lights on the front panel to indicate power, network and hard drive activity. 6 | Instead, when using a 3rd party OS with e.g. DX 4600 Pro, only the power indicator light blinks, and the other LEDs are off by default. 7 | For the DXP series, all LEDs blink in rolling sequence when non-UGOS systems are running. 8 | 9 | This repository 10 | - Describes the control logic of UGOS for the LED lights on the device front 11 | - Provides a command-line tool and a kernel module to control them 12 | 13 | For the process of understanding this control logic, please refer to [my blog (in Chinese)](https://blog.miskcoo.com/2024/05/ugreen-dx4600-pro-led-controller). 14 | 15 | > [!NOTE] 16 | > Only tested on the following devices: 17 | > - [x] UGREEN DX4600 Pro 18 | > - [x] UGREEN DX4700+ 19 | > - [x] UGREEN DXP2800 (reported in [#19](https://github.com/miskcoo/ugreen_leds_controller/issues/19)) 20 | > - [x] UGREEN DXP4800 (confirmed in [#41](https://github.com/miskcoo/ugreen_leds_controller/issues/41)) 21 | > - [x] UGREEN DXP4800 Plus (reported [here](https://gist.github.com/Kerryliu/c380bb6b3b69be5671105fc23e19b7e8)) 22 | > - [x] UGREEN DXP6800 Pro (reported in [#7](https://github.com/miskcoo/ugreen_leds_controller/issues/7)) 23 | > - [x] UGREEN DXP8800 Plus (see [this repo](https://github.com/meyergru/ugreen_dxp8800_leds_controller) and [#1](https://github.com/miskcoo/ugreen_leds_controller/issues/1)) 24 | > - [ ] UGREEN DXP480T Plus (**Not yet**, but the protocol has been understood, see [#6](https://github.com/miskcoo/ugreen_leds_controller/issues/6#issuecomment-2156807225)) 25 | > 26 | >**I am not sure whether this is compatible with other devices. 27 | >If you have tested it with different devices, please feel free to update the list above!** 28 | > 29 | > Please follow the [Preparation](#Preparation) section to check if the protocol is compatible, and run `./ugreen_leds_cli all` to see which LEDs are supported by this tool. 30 | 31 | For third-party systems, I am using Debian 12 "Bookworm", but you can find some manuals for other systems: 32 | - **DSM**: see [#8](https://github.com/miskcoo/ugreen_leds_controller/issues/8) 33 | - **TrueNAS**: see [#13](https://github.com/miskcoo/ugreen_leds_controller/issues/13) and [this repo](https://github.com/0x556c79/install-ugreen-leds-controller) (and maybe [here](https://github.com/miskcoo/ugreen_leds_controller/tree/truenas-build/build-scripts/truenas)) for how to build the module, and [here](https://gist.github.com/Kerryliu/c380bb6b3b69be5671105fc23e19b7e8) for a script using the cli tool; [here](https://github.com/miskcoo/ugreen_leds_controller/tree/gh-actions/build-scripts/truenas/build) for pre-build drivers 34 | - **unRAID**: there is a [plugin](https://forums.unraid.net/topic/168423-ugreen-nas-led-control/); see also [this repo](https://github.com/ich777/unraid-ugreenleds-driver/tree/master/source/usr/bin) 35 | - **Proxmox**: you need to use the cli tool in Proxmox, not in a VM 36 | - **Debian**: see [the section below](#start-at-boot-for-debian-12) 37 | 38 | Below is an example: 39 | ![](https://blog.miskcoo.com/assets/images/dx4600-pro-leds.gif) 40 | 41 | It can be achieved by the following commands: 42 | ```bash 43 | ugreen_leds_cli all -off -status 44 | ugreen_leds_cli power -color 255 0 255 -blink 400 600 45 | sleep 0.1 46 | ugreen_leds_cli netdev -color 255 0 0 -blink 400 600 47 | sleep 0.1 48 | ugreen_leds_cli disk1 -color 255 255 0 -blink 400 600 49 | sleep 0.1 50 | ugreen_leds_cli disk2 -color 0 255 0 -blink 400 600 51 | sleep 0.1 52 | ugreen_leds_cli disk3 -color 0 255 255 -blink 400 600 53 | sleep 0.1 54 | ugreen_leds_cli disk4 -color 0 0 255 -blink 400 600 55 | ``` 56 | 57 | ## Preparation 58 | 59 | We communicate with the control chip of the LED via I2C, corresponding to the device with address `0x3a` on *SMBus I801 adapter*. 60 | Before proceeding, we need to load the `i2c-dev` module and install the `i2c-tools` tool. 61 | 62 | ``` 63 | $ apt install -y i2c-tools 64 | $ modprobe -v i2c-dev 65 | ``` 66 | 67 | Now, we can check if the device located at address `0x3a` of *SMBus I801 adapter* is visible. 68 | 69 | ``` 70 | $ i2cdetect -l 71 | i2c-0 i2c Synopsys DesignWare I2C adapter I2C adapter 72 | i2c-1 smbus SMBus I801 adapter at efa0 SMBus adapter 73 | i2c-2 i2c Synopsys DesignWare I2C adapter I2C adapter 74 | 75 | $ i2cdetect -y 1 76 | 0 1 2 3 4 5 6 7 8 9 a b c d e f 77 | 00: 08 -- -- -- -- -- -- -- 78 | 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 79 | 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 80 | 30: 30 -- -- -- -- 35 UU UU -- -- 3a -- -- -- -- -- 81 | 40: -- -- -- -- 44 -- -- -- -- -- -- -- -- -- -- -- 82 | 50: UU -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 83 | 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 84 | 70: -- -- -- -- -- -- -- -- 85 | ``` 86 | 87 | ## Build & Usage 88 | 89 | > [!IMPORTANT] 90 | > The command-line tool and the kernel module do conflict. 91 | > To use the command-line tool, you must unload the `led_ugreen` module. 92 | 93 | ### The Command-line Tool 94 | 95 | Use `cd cli && make` to build the command-line tool, and `ugreen_leds_cli` to modify the LED states (requires root permissions). 96 | 97 | ``` 98 | Usage: ugreen_leds_cli [LED-NAME...] [-on] [-off] [-(blink|breath) T_ON T_OFF] 99 | [-color R G B] [-brightness BRIGHTNESS] [-status] 100 | 101 | LED_NAME: separated by white space, possible values are 102 | { power, netdev, disk[1-8], all }. 103 | -on / -off: turn on / off corresponding LEDs. 104 | -blink / -breath: set LED to the blink / breath mode. This 105 | mode keeps the LED on for T_ON millseconds and then 106 | keeps it off for T_OFF millseconds. 107 | T_ON and T_OFF should belong to [0, 65535]. 108 | -color: set the color of corresponding LEDs. 109 | R, G and B should belong to [0, 255]. 110 | -brightness: set the brightness of corresponding LEDs. 111 | BRIGHTNESS should belong to [0, 255]. 112 | -status: display the status of corresponding LEDs. 113 | ``` 114 | 115 | Below is an example: 116 | 117 | ```bash 118 | # turn on all LEDs 119 | ugreen_leds_cli all -on 120 | 121 | # query LEDs' status 122 | ugreen_leds_cli all -status 123 | 124 | # turn on the power indicator, 125 | # and then set its color to blue, 126 | # and then set its brightness to 128 / 256, 127 | # and finally display its status 128 | ugreen_leds_cli power -on -color 0 0 255 -brightness 128 -status 129 | ``` 130 | 131 | ### The Kernel Module 132 | 133 | There are three methods to install the module: 134 | 135 | - **A)** Run `cd kmod && make` to build the kernel module, and then load it with `sudo insmod led-ugreen.ko`. 136 | 137 | - **B)** Alternatively, you can install it with dkms: 138 | 139 | ```bash 140 | cp -r kmod /usr/src/led-ugreen-0.1 141 | dkms add -m led-ugreen -v 0.1 142 | dkms build -m led-ugreen -v 0.1 && dkms install -m led-ugreen -v 0.1 143 | ``` 144 | 145 | - **C)** You can also directly install the `.deb` package [here](https://github.com/miskcoo/ugreen_leds_controller/releases). 146 | 147 | After loading the `led-ugreen` module, you need to run `scripts/ugreen-probe-leds`, and you can see LEDs in `/sys/class/leds`. 148 | 149 | Below is an example of setting color, brightness, and blink of the `power` LED: 150 | 151 | ```bash 152 | echo 255 > /sys/class/leds/power/brightness # non-zero brightness turns it on 153 | echo "255 0 0" > /sys/class/leds/power/color # set the color to RGB(255, 0, 0) 154 | echo "blink 100 100" > /sys/class/leds/power/blink_type # blink at 10Hz 155 | ``` 156 | 157 | To blink the `netdev` LED when an NIC is active, you can use the `ledtrig-netdev` module (see `scripts/ugreen-netdevmon`): 158 | 159 | ```bash 160 | led="netdev" 161 | modprobe ledtrig-netdev 162 | echo netdev > /sys/class/leds/$led/trigger 163 | echo enp2s0 > /sys/class/leds/$led/device_name 164 | echo 1 > /sys/class/leds/$led/link 165 | echo 1 > /sys/class/leds/$led/tx 166 | echo 1 > /sys/class/leds/$led/rx 167 | echo 100 > /sys/class/leds/$led/interval 168 | ``` 169 | 170 | To blink the `disk` LED when a block device is active, you can use the `ledtrig-oneshot` module and monitor the changes of`/sys/block/sda/stat` (see `scripts/ugreen-diskiomon` for an example). If you are using zfs, you can combine this script with that provided in [#1](https://github.com/miskcoo/ugreen_leds_controller/issues/1) to change the LED's color when a disk drive failure occurs. 171 | To see how to map the disk LEDs to correct disk slots, please read the [Disk Mapping](#disk-mapping) section. 172 | 173 | #### Start at Boot (for Debian 12) 174 | 175 | The configure file of `ugreen-diskiomon` and `ugreen-netdevmon` is `/etc/ugreen-led.conf`. 176 | Please see `scripts/ugreen-leds.conf` for an example. 177 | 178 | - Add the following lines to `/etc/modules-load.d/ugreen-led.conf` 179 | ``` 180 | cat > /etc/modules-load.d/ugreen-led.conf << EOF 181 | i2c-dev 182 | led-ugreen 183 | ledtrig-oneshot 184 | ledtrig-netdev 185 | EOF 186 | ``` 187 | 188 | - Install the `smartctl` tool: `apt install smartmontools` 189 | 190 | - Install the kernel module by one of the three methods mentioned above. 191 | For example, directly install [the deb package](https://github.com/miskcoo/ugreen_leds_controller/releases). 192 | 193 | - Copy files in the `scripts` directory: 194 | ```bash 195 | # copy the scripts 196 | scripts=(ugreen-diskiomon ugreen-netdevmon ugreen-probe-leds) 197 | for f in ${scripts[@]}; do 198 | chmod +x "scripts/$f" 199 | cp "scripts/$f" /usr/bin 200 | done 201 | 202 | # copy the configuration file, you can change it if needed 203 | cp scripts/ugreen-leds.conf /etc/ugreen-leds.conf 204 | 205 | # copy the systemd services 206 | cp scripts/systemd/*.service /etc/systemd/system/ 207 | 208 | systemctl daemon-reload 209 | 210 | # change enp2s0 to the network device you want to monitor 211 | systemctl start ugreen-netdevmon@enp2s0 212 | systemctl start ugreen-diskiomon 213 | 214 | # if you confirm that everything works well, 215 | # run the command below to make the service start at boot 216 | systemctl enable ugreen-netdevmon@enp2s0 217 | systemctl enable ugreen-diskiomon 218 | ``` 219 | 220 | - (_Optional_) To reduce the CPU usage of blinking LEDs when disks are active, you can enter the `scripts` directory and do the following things: 221 | ```bash 222 | # compile the disk activities monitor 223 | g++ -std=c++17 -O2 blink-disk.cpp -o ugreen-blink-disk 224 | 225 | # copy the binary file (the path can be changed, see BLINK_MON_PATH in ugreen-leds.conf) 226 | cp ugreen-blink-disk /usr/bin 227 | ``` 228 | 229 | - (_Optional_) Similarly, to reduce the latency of the standby check, you can enter the `scripts` directory and do the following things: 230 | ```bash 231 | # compile the disk standby checker 232 | g++ -std=c++17 -O2 check-standby.cpp -o ugreen-check-standby 233 | 234 | # copy the binary file (the path can be changed, see STANDBY_MON_PATH in ugreen-leds.conf) 235 | cp ugreen-check-standby /usr/bin 236 | ``` 237 | 238 | ## Disk Mapping 239 | 240 | To make the disk LEDs useful, we should map the disk LEDs to correct disk slots. First of all, we should highlight that using `/dev/sdX` is never a smart idea, as it may change at every boot. 241 | In the script `ugreen-diskiomon` we provide three mapping methods: **by ATA**, **by HCTL** and **by serial**. 242 | 243 | The best mapping method is using serial numbers, but it needs to record them manually and fill the `DISK_SERIAL` array in `/etc/ugreen-leds.conf`. We use ATA mapping by default, and find that UGOS also uses a similar mapping method (see [#15](https://github.com/miskcoo/ugreen_leds_controller/pull/15)). 244 | See the comments in `scripts/ugreen-leds.conf` for more details. 245 | 246 | The HCTL mapping depends on how the SATA controllers are connected to the PCIe bus and the disk slots. To check the HCTL order, you can run the following command, and check the serial of your disks: 247 | 248 | ```bash 249 | # lsblk -S -x hctl -o name,hctl,serial 250 | NAME HCTL SERIAL 251 | sda 0:0:0:0 XXKEREXX 252 | sdc 1:0:0:0 XXKG2BXX 253 | sdb 2:0:0:0 XXGMU6XX 254 | sdd 3:0:0:0 XXKJEZXX 255 | sde 4:0:0:0 XXKJHBXX 256 | sdf 5:0:0:0 XXGT2ZXX 257 | sdg 6:0:0:0 XXKH3SXX 258 | sdh 7:0:0:0 XXJDB1XX 259 | ``` 260 | > [!NOTE] 261 | > As far as we know, the mapping between HCTL and the disk serial are stable at each boot (see [#4](https://github.com/miskcoo/ugreen_leds_controller/pull/4) and [#9](https://github.com/miskcoo/ugreen_leds_controller/issues/9)). 262 | > However, it has been reported that the exact order is model-dependent (see [#9](https://github.com/miskcoo/ugreen_leds_controller/issues/9)). 263 | > - For DX4600 Pro and DXP8800 Plus, the mapping is `X:0:0:0 -> diskX`. 264 | > - For DXP6800 Pro, `0:0:0:0` and `1:0:0:0` are mapped to `disk5` and `disk6`, and `2:0:0:0` to `6:0:0:0` are mapped to `disk1` to `disk4`. 265 | > 266 | > The script will use `dmidecode` to detect the device model, but I suggest to check the mapping outputed by the script manually. 267 | 268 | ## Communication Protocols 269 | 270 | The IDs for the LED lights on the front panel of the NAS chassis are as follows: 271 | 272 | | ID | LED | 273 | |-------------|--------------------------------| 274 | | `0` | Power indicator | 275 | | `1` | Network device indicator | 276 | | `2`,`3`,... | Hard drive indicator "disk1", "disk2" etc. | 277 | 278 | ### Query Status 279 | 280 | Reading 11 bytes from the address `0x81 + LED_ID` allows us to obtain the current status of the corresponding LED. The meaning of these 11 bytes is as follows: 281 | 282 | | Address | Meaning of Corresponding Data | 283 | |---------|--------------------------------| 284 | | `0x00` | LED status: 0 (off), 1 (on), 2 (blink), 3 (breath) | 285 | | `0x01` | LED brightness | 286 | | `0x02` | LED color (Red component in RGB) | 287 | | `0x03` | LED color (Green component in RGB) | 288 | | `0x04` | LED color (Blue component in RGB) | 289 | | `0x05` | Milliseconds needed to complete one blink/breath cycle (high 8 bits) | 290 | | `0x06` | Milliseconds needed to complete one blink/breath cycle (low 8 bits) | 291 | | `0x07` | Milliseconds the LED is on during one blink/breath cycle (high 8 bits) | 292 | | `0x08` | Milliseconds the LED is on during one blink/breath cycle (low 8 bits) | 293 | | `0x09` | Checksum of data in the range 0x00 - 0x08 (high 8 bits) | 294 | | `0x0a` | Checksum of data in the range 0x00 - 0x08 (low 8 bits) | 295 | 296 | The checksum is a 16-bit value obtained by summing all the data at the corresponding positions as unsigned integers. 297 | 298 | We can directly use `i2cget` to read from the relevant registers. For example, below is the status of the power indicator light (purple, blinking once per second, lit for 40% of the time, with a brightness of 180/256): 299 | 300 | ``` 301 | $ i2cget -y 0x01 0x3a 0x81 i 0x0b 0x02 0xb4 0xff 0x00 0xff 0x03 0xe8 0x01 0x90 0x04 0x30 302 | ``` 303 | 304 | ### Change Status 305 | 306 | By writing 12 bytes to the address `0x00 + LED_ID`, we can modify the current status of the corresponding LED. The meaning of these 12 bytes is as follows: 307 | 308 | | Address | Meaning of Corresponding Data | 309 | |---------|--------------------------------| 310 | | `0x00` | LED ID | 311 | | `0x01` | Constant: 0xa0 | 312 | | `0x02` | Constant: 0x01 | 313 | | `0x03` | Constant: 0x00 | 314 | | `0x04` | Constant: 0x00 | 315 | | `0x05` | If the value is 1, it indicates modifying brightness
If the value is 2, it indicates modifying color
If the value is 3, it indicates setting the on/off state
If the value is 4, it indicates setting the blink state
If the value is 5, it indicates setting the breath state | 316 | | `0x06` | First parameter | 317 | | `0x07` | Second parameter | 318 | | `0x08` | Third parameter | 319 | | `0x09` | Fourth parameter | 320 | | `0x0a` | Checksum of data in the range 0x01 - 0x09 (high 8 bits) | 321 | | `0x0b` | Checksum of data in the range 0x01 - 0x09 (low 8 bits) | 322 | 323 | For the four different modification types at address `0x05`: 324 | - If we need to modify **brightness**, the first parameter contains brightness information. 325 | - If we need to modify **color**, the first three parameters represent RGB information. 326 | - If we need to toggle the **on/off state**, the first parameter is either 0 (off) or 1 (on). 327 | - If we need to set the **blink/breath state**, the first two parameters together form a 16-bit unsigned integer in big-endian order, representing the number of milliseconds needed to complete one blink/breath cycle. The next two parameters, also in big-endian order, represent the time in milliseconds the LED is on during one blink/breath cycle. 328 | 329 | Below is an example for turning off and on the power indicator light using `i2cset`: 330 | 331 | ``` 332 | # turn off power LED 333 | $ i2cset -y 0x01 0x3a 0x00 0x00 0xa0 0x01 0x00 0x00 0x03 0x01 0x00 0x00 0x00 0x00 0xa5 i 334 | 335 | # turn on power LED 336 | $ i2cset -y 0x01 0x3a 0x00 0x00 0xa0 0x01 0x00 0x00 0x03 0x00 0x00 0x00 0x00 0x00 0xa4 i 337 | ``` 338 | 339 | ## Acknowledgement 340 | 341 | ChatGPT, [this V2EX post](https://fast.v2ex.com/t/991429), Ghidra 342 | -------------------------------------------------------------------------------- /SPECS/Makefile: -------------------------------------------------------------------------------- 1 | # If KERNELRELEASE is defined, the make command using this Makefile has 2 | # been invoked by the kernel build system and so can use its language. 3 | # Otherwise, if KERNELRELEASE is null, a make command was issued from 4 | # the command line. So invoke the kernel build system. 5 | 6 | ifeq ($(KERNELRELEASE),) 7 | 8 | # KVERSION should be set in the environment if this 9 | # build is not for the currently running kernel. 10 | KVERSION ?= $(shell uname -r) 11 | 12 | # BUILD_DIR should be set in the environment if a 13 | # subdirectory of /lib/modules/ is not appropriate. 14 | BUILD_DIR ?= /lib/modules/${KVERSION}/build 15 | 16 | PWD := $(shell pwd) 17 | 18 | all: 19 | $(MAKE) -C $(BUILD_DIR) M=$(PWD) modules 20 | 21 | modules: 22 | $(MAKE) -C $(BUILD_DIR) M=$(PWD) modules 23 | 24 | modules_install: 25 | $(MAKE) -C $(BUILD_DIR) M=$(PWD) modules_install 26 | 27 | clean: 28 | $(MAKE) -C $(BUILD_DIR) M=$(PWD) clean 29 | rm -rf *~ *.o .*.cmd *.mod.c *.ko *.ko.unsigned .depend \ 30 | .tmp_versions modules.order Module.symvers Module.markers 31 | 32 | .PHONY: modules modules_install clean 33 | 34 | else 35 | 36 | # Called from kernel build system -- just declare the module(s). 37 | 38 | obj-m += led-ugreen.o 39 | 40 | endif 41 | -------------------------------------------------------------------------------- /SPECS/kmod-led-ugreen.spec: -------------------------------------------------------------------------------- 1 | # Define the kmod package name here. 2 | %define kmod_name led-ugreen 3 | 4 | # If kmod_kernel_version isn't defined on the rpmbuild line, define it here. 5 | %{!?kmod_kernel_version: %define kmod_kernel_version 5.14.0-427.20.1.el9_4} 6 | 7 | %{!?dist: %define dist .el9} 8 | 9 | Name: kmod-%{kmod_name} 10 | Version: 0.1 11 | Release: 17%{?dist} 12 | Summary: %{kmod_name} kernel module(s) 13 | Group: System Environment/Kernel 14 | License: GPLv2 15 | URL: https://github.com/miskcoo/ugreen_leds_controller 16 | 17 | # Sources. 18 | #tar -czf ugreen_leds_controller.tar.gz ugreen_leds_controller 19 | Source0: %{kmod_name}-%{version}.tar.gz 20 | 21 | # Fix for the SB-signing issue caused by a bug in /usr/lib/rpm/brp-strip 22 | # https://bugzilla.redhat.com/show_bug.cgi?id=1967291 23 | 24 | %define __spec_install_post \ 25 | /usr/lib/rpm/check-buildroot \ 26 | /usr/lib/rpm/redhat/brp-ldconfig \ 27 | /usr/lib/rpm/brp-compress \ 28 | /usr/lib/rpm/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump \ 29 | /usr/lib/rpm/brp-strip-static-archive /usr/bin/strip \ 30 | /usr/lib/rpm/brp-python-bytecompile "" "1" "0" \ 31 | /usr/lib/rpm/brp-python-hardlink \ 32 | /usr/lib/rpm/redhat/brp-mangle-shebangs 33 | 34 | # Source code patches 35 | 36 | %define findpat %( echo "%""P" ) 37 | %define dup_state_dir %{_localstatedir}/lib/rpm-state/kmod-dups 38 | %define kver_state_dir %{dup_state_dir}/kver 39 | %define kver_state_file %{kver_state_dir}/%{kmod_kernel_version}.%{_arch} 40 | %define dup_module_list %{dup_state_dir}/rpm-kmod-%{kmod_name}-modules 41 | %define debug_package %{nil} 42 | 43 | %global _use_internal_dependency_generator 0 44 | %global kernel_source() %{_usrsrc}/kernels/%{kmod_kernel_version}.%{_arch} 45 | 46 | BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) 47 | 48 | ExclusiveArch: x86_64 49 | 50 | BuildRequires: elfutils-libelf-devel 51 | BuildRequires: kernel-devel = %{kmod_kernel_version} 52 | BuildRequires: kernel-abi-stablelists 53 | BuildRequires: kernel-rpm-macros 54 | BuildRequires: redhat-rpm-config 55 | BuildRequires: systemd-units 56 | BuildRequires: gcc-c++ 57 | 58 | Provides: kernel-modules >= %{kmod_kernel_version}.%{_arch} 59 | Provides: kmod-%{kmod_name} = %{?epoch:%{epoch}:}%{version}-%{release} 60 | 61 | Requires(post): %{_sbindir}/weak-modules 62 | Requires(postun): %{_sbindir}/weak-modules 63 | Requires: kernel >= %{kmod_kernel_version} 64 | Requires: kernel-core-uname-r >= %{kmod_kernel_version} 65 | Requires: i2c-tools smartmontools dmidecode 66 | 67 | %description 68 | This package provides the %{kmod_name} kernel module(s) for ugreen_leds_controller. 69 | It is built to depend upon the specific ABI provided by a range of releases 70 | of the same variant of the Linux kernel and not on any one specific build. 71 | 72 | 73 | %prep 74 | %setup -q -n ugreen_leds_controller 75 | echo "override %{kmod_name} * weak-updates/%{kmod_name}" > kmod-%{kmod_name}.conf 76 | 77 | # Apply patch(es) 78 | 79 | %build 80 | pushd kmod 81 | %{__make} -C %{kernel_source} %{?_smp_mflags} modules M=$PWD 82 | popd 83 | 84 | pushd scripts 85 | %{__cxx} -std=c++17 -O2 blink-disk.cpp -o ugreen-blink-disk 86 | %{__cxx} -std=c++17 -O2 check-standby.cpp -o ugreen-check-standby 87 | popd 88 | 89 | whitelist="/lib/modules/kabi-current/kabi_stablelist_%{_target_cpu}" 90 | for modules in $( find . -name "*.ko" -type f -printf "%{findpat}\n" | sed 's|\.ko$||' | sort -u ) ; do 91 | # update greylist 92 | nm -u ./$modules.ko | sed 's/.*U //' | sed 's/^\.//' | sort -u | while read -r symbol; do 93 | grep -q "^\s*$symbol\$" $whitelist || echo "$symbol" >> ./greylist 94 | done 95 | done 96 | sort -u greylist | uniq > greylist.txt 97 | 98 | %install 99 | %{__install} -d %{buildroot}/lib/modules/%{kmod_kernel_version}.%{_arch}/extra/%{kmod_name}/ 100 | %{__install} kmod/%{kmod_name}.ko %{buildroot}/lib/modules/%{kmod_kernel_version}.%{_arch}/extra/%{kmod_name}/ 101 | %{__install} -d %{buildroot}%{_sysconfdir}/depmod.d/ 102 | %{__install} -m 0644 kmod-%{kmod_name}.conf %{buildroot}%{_sysconfdir}/depmod.d/ 103 | %{__install} -d %{buildroot}%{_defaultdocdir}/kmod-%{kmod_name}-%{version}/ 104 | %{__install} -m 0644 greylist.txt %{buildroot}%{_defaultdocdir}/kmod-%{kmod_name}-%{version}/ 105 | %{__install} -m 0644 scripts/ugreen-leds.conf %{buildroot}%{_sysconfdir}/ 106 | 107 | mkdir -p %{buildroot}/etc/modules-load.d/ 108 | %{__install} -m 0644 SPECS/ugreen-led.conf %{buildroot}/etc/modules-load.d/ 109 | 110 | mkdir -p %{buildroot}%{_bindir}/ 111 | 112 | %{__install} -m 0755 scripts/ugreen-diskiomon %{buildroot}%{_bindir}/ 113 | %{__install} -m 0755 scripts/ugreen-netdevmon %{buildroot}%{_bindir}/ 114 | %{__install} -m 0755 scripts/ugreen-probe-leds %{buildroot}%{_bindir}/ 115 | %{__install} -m 0755 scripts/ugreen-blink-disk %{buildroot}%{_bindir}/ 116 | %{__install} -m 0755 scripts/ugreen-check-standby %{buildroot}%{_bindir}/ 117 | 118 | mkdir -p %{buildroot}%{_unitdir}/ 119 | %{__install} -m 0644 scripts/ugreen-netdevmon@.service %{buildroot}%{_unitdir}/ 120 | %{__install} -m 0644 scripts/ugreen-diskiomon.service %{buildroot}%{_unitdir}/ 121 | 122 | # strip the modules(s) 123 | find %{buildroot} -type f -name \*.ko -exec %{__strip} --strip-debug \{\} \; 124 | 125 | # Sign the modules(s) 126 | %if %{?_with_modsign:1}%{!?_with_modsign:0} 127 | # If the module signing keys are not defined, define them here. 128 | %{!?privkey: %define privkey %{_sysconfdir}/pki/SECURE-BOOT-KEY.priv} 129 | %{!?pubkey: %define pubkey %{_sysconfdir}/pki/SECURE-BOOT-KEY.der} 130 | for module in $(find %{buildroot} -type f -name \*.ko); 131 | do %{_usrsrc}/kernels/%{kmod_kernel_version}.%{_arch}/scripts/sign-file \ 132 | sha256 %{privkey} %{pubkey} $module; 133 | done 134 | %endif 135 | 136 | %clean 137 | %{__rm} -rf %{buildroot} 138 | 139 | %post 140 | modules=( $(find /lib/modules/%{kmod_kernel_version}.x86_64/extra/%{kmod_name} | grep '\.ko$') ) 141 | printf '%s\n' "${modules[@]}" | %{_sbindir}/weak-modules --add-modules --no-initramfs 142 | 143 | mkdir -p "%{kver_state_dir}" 144 | touch "%{kver_state_file}" 145 | 146 | echo "systemctl start ugreen-diskiomon.service" 147 | echo "ls /sys/class/leds" 148 | echo "Make sure you can see disk1, netdev, power, etc." 149 | echo "systemctl enable ugreen-diskiomon.service" 150 | echo 151 | echo "to uninstall:" 152 | echo "systemctl stop ugreen-diskiomon.service" 153 | echo "systemctl disable ugreen-diskiomon.service" 154 | 155 | exit 0 156 | 157 | %posttrans 158 | # We have to re-implement part of weak-modules here because it doesn't allow 159 | # calling initramfs regeneration separately 160 | if [ -f "%{kver_state_file}" ]; then 161 | kver_base="%{kmod_kernel_version}" 162 | kvers=$(ls -d "/lib/modules/${kver_base%%.*}"*) 163 | 164 | for k_dir in $kvers; do 165 | k="${k_dir#/lib/modules/}" 166 | 167 | tmp_initramfs="/boot/initramfs-$k.tmp" 168 | dst_initramfs="/boot/initramfs-$k.img" 169 | 170 | # The same check as in weak-modules: we assume that the kernel present 171 | # if the symvers file exists. 172 | if [ -e "/$k_dir/symvers.gz" ]; then 173 | /usr/bin/dracut -f "$tmp_initramfs" "$k" || exit 1 174 | cmp -s "$tmp_initramfs" "$dst_initramfs" 175 | if [ "$?" = 1 ]; then 176 | mv "$tmp_initramfs" "$dst_initramfs" 177 | else 178 | rm -f "$tmp_initramfs" 179 | fi 180 | fi 181 | done 182 | 183 | rm -f "%{kver_state_file}" 184 | rmdir "%{kver_state_dir}" 2> /dev/null 185 | fi 186 | 187 | rmdir "%{dup_state_dir}" 2> /dev/null 188 | 189 | exit 0 190 | 191 | %preun 192 | if rpm -q --filetriggers kmod 2> /dev/null| grep -q "Trigger for weak-modules call on kmod removal"; then 193 | mkdir -p "%{kver_state_dir}" 194 | touch "%{kver_state_file}" 195 | fi 196 | 197 | mkdir -p "%{dup_state_dir}" 198 | rpm -ql kmod-%{kmod_name}-%{version}-%{release}.%{_arch} | grep '\.ko$' > "%{dup_module_list}" 199 | 200 | %postun 201 | if rpm -q --filetriggers kmod 2> /dev/null| grep -q "Trigger for weak-modules call on kmod removal"; then 202 | initramfs_opt="--no-initramfs" 203 | else 204 | initramfs_opt="" 205 | fi 206 | 207 | modules=( $(cat "%{dup_module_list}") ) 208 | rm -f "%{dup_module_list}" 209 | printf '%s\n' "${modules[@]}" | %{_sbindir}/weak-modules --remove-modules $initramfs_opt 210 | 211 | rmdir "%{dup_state_dir}" 2> /dev/null 212 | 213 | exit 0 214 | 215 | %files 216 | %defattr(644,root,root,755) 217 | /lib/modules/%{kmod_kernel_version}.%{_arch}/ 218 | %config /etc/depmod.d/kmod-%{kmod_name}.conf 219 | %config(noreplace) %{_sysconfdir}/ugreen-leds.conf 220 | %config(noreplace) /etc/modules-load.d/ugreen-led.conf 221 | %doc /usr/share/doc/kmod-%{kmod_name}-%{version}/ 222 | %attr(0755, root, root) %{_bindir}/ugreen-diskiomon 223 | %attr(0755, root, root) %{_bindir}/ugreen-netdevmon 224 | %attr(0755, root, root) %{_bindir}/ugreen-probe-leds 225 | %attr(0755, root, root) %{_bindir}/ugreen-blink-disk 226 | %attr(0755, root, root) %{_bindir}/ugreen-check-standby 227 | %{_unitdir}/ugreen-netdevmon@.service 228 | %{_unitdir}/ugreen-diskiomon.service 229 | 230 | %changelog 231 | * Sat Jun 22 2024 Axel Olmos - 0.0-14 232 | - Rewritten to be a ugreen-led kmod spec. Thank you Akemi for making the original template! 233 | 234 | * Fri Nov 26 2021 Akemi Yagi - 0.0-1 235 | - Initial build for RHEL 9 236 | -------------------------------------------------------------------------------- /SPECS/ugreen-led.conf: -------------------------------------------------------------------------------- 1 | i2c-dev 2 | led-ugreen 3 | ledtrig-oneshot 4 | ledtrig-netdev 5 | -------------------------------------------------------------------------------- /build-scripts/debian/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | RUN set -ex \ 4 | && sed -i -- 's/Types: deb/Types: deb deb-src/g' /etc/apt/sources.list.d/debian.sources \ 5 | && apt-get update \ 6 | && apt-get install -y --no-install-recommends \ 7 | build-essential \ 8 | cdbs \ 9 | devscripts \ 10 | equivs \ 11 | fakeroot \ 12 | wget \ 13 | git \ 14 | && apt-get clean \ 15 | && rm -rf /tmp/* /var/tmp/* 16 | 17 | 18 | -------------------------------------------------------------------------------- /build-scripts/debian/README.md: -------------------------------------------------------------------------------- 1 | 2 | Build it in docker: 3 | 4 | ``` 5 | bash docker-run.sh 6 | ``` 7 | 8 | The outputs can be found in `./build` directory. 9 | -------------------------------------------------------------------------------- /build-scripts/debian/build-dkms-deb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | pkgver="0.3" 7 | pkgname="led-ugreen-dkms" 8 | drivername="led-ugreen" 9 | 10 | mkdir -p $pkgname/DEBIAN 11 | 12 | cat < $pkgname/DEBIAN/control 13 | Package: $pkgname 14 | Version: $pkgver 15 | Architecture: amd64 16 | Maintainer: Yuhao Zhou 17 | Depends: dkms 18 | Homepage: https://github.com/miskcoo/ugreen_leds_controller 19 | Description: UGREEN NAS LED driver 20 | A reverse-engineered LED driver of UGREEN NAS. 21 | EOF 22 | 23 | cat < $pkgname/DEBIAN/postinst 24 | #!/usr/bin/bash 25 | 26 | dkms add -m $drivername -v $pkgver 27 | dkms build -m $drivername -v $pkgver && dkms install -m $drivername -v $pkgver || true 28 | 29 | EOF 30 | 31 | cat < $pkgname/DEBIAN/prerm 32 | #!/usr/bin/bash 33 | 34 | dkms remove -m $drivername -v $pkgver --all || true 35 | EOF 36 | 37 | chmod +x $pkgname/DEBIAN/postinst 38 | chmod +x $pkgname/DEBIAN/prerm 39 | 40 | 41 | # dkms files 42 | mkdir -p $pkgname/usr/src/$drivername-$pkgver 43 | 44 | kmod_files=(kmod/Makefile kmod/dkms.conf kmod/led-ugreen.c kmod/led-ugreen.h kmod/Makefile) 45 | for f in ${kmod_files[@]}; do 46 | cp -rv $f $pkgname/usr/src/$drivername-$pkgver/ 47 | done 48 | 49 | # change to root 50 | chown -R root:root $pkgname/ 51 | 52 | dpkg -b $pkgname 53 | 54 | rm -rv $pkgname 55 | 56 | -------------------------------------------------------------------------------- /build-scripts/debian/build-utils-deb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | pkgver="0.3" 7 | pkgname="led-ugreen-utils" 8 | drivername="led-ugreen" 9 | 10 | mkdir -p $pkgname/DEBIAN 11 | 12 | cat < $pkgname/DEBIAN/control 13 | Package: $pkgname 14 | Version: $pkgver 15 | Architecture: amd64 16 | Maintainer: Yuhao Zhou 17 | Depends: dmidecode, smartmontools 18 | Homepage: https://github.com/miskcoo/ugreen_leds_controller 19 | Description: UGREEN NAS LED tools 20 | A reverse-engineered LED tools of UGREEN NAS. 21 | EOF 22 | 23 | 24 | # scripts 25 | mkdir -p $pkgname/usr/bin/ 26 | 27 | script_files=(ugreen-probe-leds ugreen-netdevmon ugreen-diskiomon) 28 | 29 | for f in ${script_files[@]}; do 30 | cp scripts/$f $pkgname/usr/bin/ 31 | chmod +x $pkgname/usr/bin/$f 32 | done 33 | 34 | # systemd file 35 | mkdir -p $pkgname/etc/systemd/system 36 | cp scripts/systemd/*.service $pkgname/etc/systemd/system/ 37 | # cp scripts/ugreen-ledmon@.service $pkgname/etc/systemd/system/ 38 | 39 | # example config file 40 | cp scripts/ugreen-leds.conf $pkgname/etc/ugreen-leds.example.conf 41 | 42 | # compile the disk activities monitor 43 | g++ -std=c++17 -O2 scripts/blink-disk.cpp -o ugreen-blink-disk 44 | cp ugreen-blink-disk $pkgname/usr/bin 45 | 46 | # compile the disk standby monitor 47 | #g++ -std=c++17 -O2 scripts/check-standby.cpp -o ugreen-check-standby 48 | #cp ugreen-check-standby $pkgname/usr/bin 49 | 50 | # change to root 51 | chown -R root:root $pkgname/ 52 | 53 | # cli 54 | cd cli && make -j 4 55 | cd .. 56 | cp cli/ugreen_leds_cli $pkgname/usr/bin 57 | # cp cli/ugreen_daemon $pkgname/usr/bin 58 | chmod +x $pkgname/usr/bin 59 | 60 | dpkg -b $pkgname 61 | 62 | rm -rv $pkgname 63 | 64 | -------------------------------------------------------------------------------- /build-scripts/debian/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -x 4 | 5 | git clone https://github.com/miskcoo/ugreen_leds_controller.git 6 | cd ugreen_leds_controller 7 | 8 | if [ ! -z $1 ]; then 9 | git checkout $1 10 | fi 11 | 12 | bash build-scripts/debian/build-dkms-deb.sh 13 | bash build-scripts/debian/build-utils-deb.sh 14 | dpkg-name led-ugreen-dkms.deb 15 | dpkg-name led-ugreen-utils.deb 16 | mv *.deb .. 17 | -------------------------------------------------------------------------------- /build-scripts/debian/docker-run.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | set -x 4 | 5 | if [[ ! -d ./build ]]; then 6 | mkdir build 7 | fi 8 | 9 | docker build --tag bookworm-build . 10 | docker run \ 11 | --rm \ 12 | --mount type=bind,source=$(pwd)/build,target=/build \ 13 | --mount type=bind,source=$(pwd)/build.sh,target=/build.sh \ 14 | bookworm-build \ 15 | bash -c "cd /build && bash /build.sh $1" 16 | -------------------------------------------------------------------------------- /build-scripts/truenas/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | RUN set -ex \ 4 | && sed -i -- 's/Types: deb/Types: deb deb-src/g' /etc/apt/sources.list.d/debian.sources \ 5 | && apt-get update \ 6 | && apt-get install -y --no-install-recommends \ 7 | build-essential \ 8 | cdbs \ 9 | devscripts \ 10 | equivs \ 11 | fakeroot \ 12 | wget \ 13 | git \ 14 | && apt-get clean \ 15 | && rm -rf /tmp/* /var/tmp/* 16 | 17 | 18 | -------------------------------------------------------------------------------- /build-scripts/truenas/README.md: -------------------------------------------------------------------------------- 1 | 2 | Run `bash build.sh TrueNAS-SCALE-Dragonfish/24.04.1` 3 | 4 | ### Important Note 5 | When you find errors like this: 6 | ``` 7 | --2025-01-19 09:03:41-- https://download.truenas.com/TrueNAS-SCALE-ElectricEel/24.10.1/packages/Packages.gz 8 | Resolving download.truenas.com (download.truenas.com)... 185.244.226.2 9 | Connecting to download.truenas.com (download.truenas.com)|185.244.226.2|:443... connected. 10 | HTTP request sent, awaiting response... 404 Not Found 11 | 2025-01-19 09:03:42 ERROR 404: Not Found. 12 | ``` 13 | 14 | You can try to use previous versions. See Issue #28. 15 | -------------------------------------------------------------------------------- /build-scripts/truenas/build-all.sh: -------------------------------------------------------------------------------- 1 | 2 | truenas_versions=$(curl https://download.truenas.com/ | grep -Po "(?<=href=\")TrueNAS-SCALE-\w*?/\d+(\.\d+)*" | uniq) 3 | 4 | for version in ${truenas_versions}; do 5 | # only build for TrueNAS-SCALE-Dragonfish/24.04.0 and later versions 6 | if [ $(echo $version | grep -Po "(?<=/)\d+") -ge 24 ] && [ ! -d "build/$version" ]; then 7 | echo Building "$version" 8 | bash build.sh "$version" 9 | fi 10 | done 11 | 12 | find -name *.ko 13 | -------------------------------------------------------------------------------- /build-scripts/truenas/build-truenas-kmod.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | 3 | 4 | url_prefix="https://download.truenas.com/$1/packages/" 5 | 6 | mkdir truenas_working 7 | cd truenas_working 8 | 9 | if ! wget "${url_prefix}Packages.gz"; then 10 | cd .. 11 | rm -rf truenas_working 12 | exit 0 13 | fi 14 | 15 | gzip -d Packages.gz 16 | deb_name=$(grep linux-headers-truenas-production Packages | grep -Po "(?<=Filename: ./).*deb") 17 | wget "${url_prefix}${deb_name}" 18 | mkdir tmp 19 | dpkg-deb -R ${deb_name} tmp 20 | 21 | git clone https://github.com/miskcoo/ugreen_dx4600_leds_controller.git 22 | cd ugreen_dx4600_leds_controller/kmod 23 | 24 | cat < Makefile 25 | TARGET = led-ugreen 26 | obj-m += led-ugreen.o 27 | ccflags-y := -std=gnu11 28 | 29 | all: 30 | make -C ../../tmp/usr/src/$(ls ../../tmp/usr/src/) M=$(pwd) modules 31 | 32 | EOF 33 | 34 | make 35 | 36 | cd ../../../ 37 | mkdir -p $1 38 | cp truenas_working/ugreen_dx4600_leds_controller/kmod/*.ko $1 39 | rm -fr truenas_working 40 | -------------------------------------------------------------------------------- /build-scripts/truenas/build.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | 3 | if [[ ! -d ./build ]]; then 4 | mkdir build 5 | fi 6 | 7 | docker build --tag bookworm-build . 8 | docker run \ 9 | --rm \ 10 | --mount type=bind,source=$(pwd)/build,target=/build \ 11 | --mount type=bind,source=$(pwd)/build-truenas-kmod.sh,target=/build.sh \ 12 | bookworm-build \ 13 | bash -c "cd /build && bash /build.sh $1" 14 | -------------------------------------------------------------------------------- /cli/.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: [-std=c++17] 3 | -------------------------------------------------------------------------------- /cli/Makefile: -------------------------------------------------------------------------------- 1 | 2 | CC = g++ 3 | CFLAGS = -I. -O2 -Wall -static 4 | DEPS = i2c.h ugreen_leds.h 5 | OBJ = i2c.o ugreen_leds.o 6 | 7 | %.o: %.cpp $(DEPS) 8 | $(CC) -c -o $@ $< $(CFLAGS) 9 | 10 | ugreen_leds_cli: $(OBJ) ugreen_leds_cli.o 11 | $(CC) -o $@ $^ $(CFLAGS) 12 | -------------------------------------------------------------------------------- /cli/i2c.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include "i2c.h" 10 | 11 | 12 | i2c_device_t::~i2c_device_t() { 13 | if (_fd) close(_fd); 14 | } 15 | 16 | int i2c_device_t::start(const char *filename, uint16_t addr) { 17 | _fd = open(filename, O_RDWR); 18 | 19 | if (_fd < 0) { 20 | int rc = _fd; 21 | _fd = 0; 22 | return rc; 23 | } 24 | 25 | int rc = ioctl(_fd, I2C_SLAVE, addr); 26 | if (rc < 0) { 27 | close(_fd); 28 | _fd = 0; 29 | return rc; 30 | } 31 | 32 | return 0; 33 | }; 34 | 35 | std::vector i2c_device_t::read_block_data(uint8_t command, uint32_t size) { 36 | if (!_fd) return { }; 37 | 38 | if (size > I2C_SMBUS_BLOCK_MAX) 39 | return { }; 40 | 41 | i2c_smbus_data smbus_data; 42 | smbus_data.block[0] = size; 43 | 44 | i2c_smbus_ioctl_data ioctl_data; 45 | ioctl_data.size = I2C_SMBUS_I2C_BLOCK_DATA; 46 | ioctl_data.read_write = I2C_SMBUS_READ; 47 | ioctl_data.command = command; 48 | ioctl_data.data = &smbus_data; 49 | 50 | int rc = ioctl(_fd, I2C_SMBUS, &ioctl_data); 51 | 52 | if (rc < 0) return { }; 53 | 54 | std::vector data; 55 | for (uint32_t i = 0; i < size; ++i) 56 | data.push_back(smbus_data.block[i + 1]); 57 | 58 | return data; 59 | } 60 | 61 | int i2c_device_t::write_block_data(uint8_t command, std::vector data) { 62 | if (!_fd) return -1; 63 | 64 | uint32_t size = data.size(); 65 | if (size > I2C_SMBUS_BLOCK_MAX) 66 | size = I2C_SMBUS_BLOCK_MAX; 67 | 68 | i2c_smbus_data smbus_data; 69 | smbus_data.block[0] = size; 70 | for (uint32_t i = 0; i < size; ++i) 71 | smbus_data.block[i + 1] = data[i]; 72 | 73 | i2c_smbus_ioctl_data ioctl_data; 74 | ioctl_data.size = I2C_SMBUS_I2C_BLOCK_DATA; 75 | ioctl_data.read_write = I2C_SMBUS_WRITE; 76 | ioctl_data.command = command; 77 | ioctl_data.data = &smbus_data; 78 | 79 | int rc = ioctl(_fd, I2C_SMBUS, &ioctl_data); 80 | 81 | return rc; 82 | } 83 | 84 | uint8_t i2c_device_t::read_byte_data(uint8_t command) { 85 | if (!_fd) return { }; 86 | 87 | i2c_smbus_data smbus_data; 88 | 89 | i2c_smbus_ioctl_data ioctl_data; 90 | ioctl_data.size = I2C_SMBUS_BYTE_DATA; 91 | ioctl_data.read_write = I2C_SMBUS_READ; 92 | ioctl_data.command = command; 93 | ioctl_data.data = &smbus_data; 94 | 95 | int rc = ioctl(_fd, I2C_SMBUS, &ioctl_data); 96 | 97 | if (rc < 0) return { }; 98 | 99 | return smbus_data.byte & 0xff; 100 | } 101 | -------------------------------------------------------------------------------- /cli/i2c.h: -------------------------------------------------------------------------------- 1 | #ifndef __UGREEN_I2C_H__ 2 | #define __UGREEN_I2C_H__ 3 | 4 | #include 5 | #include 6 | 7 | class i2c_device_t { 8 | 9 | private: 10 | int _fd; 11 | 12 | public: 13 | ~i2c_device_t(); 14 | 15 | int start(const char *filename, uint16_t addr); 16 | std::vector read_block_data(uint8_t command, uint32_t size); 17 | int write_block_data(uint8_t command, std::vector data); 18 | uint8_t read_byte_data(uint8_t command); 19 | 20 | }; 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /cli/ugreen_leds.cpp: -------------------------------------------------------------------------------- 1 | #include "ugreen_leds.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define I2C_DEV_PATH "/sys/class/i2c-dev/" 8 | 9 | int ugreen_leds_t::start() { 10 | namespace fs = std::filesystem; 11 | 12 | if (!fs::exists(I2C_DEV_PATH)) 13 | return -1; 14 | 15 | for (const auto& entry : fs::directory_iterator(I2C_DEV_PATH)) { 16 | if (entry.is_directory()) { 17 | std::ifstream ifs(entry.path() / "device/name"); 18 | std::string line; 19 | std::getline(ifs, line); 20 | 21 | if (line.rfind("SMBus I801 adapter", 0) == 0) { 22 | const auto i2c_dev = "/dev/" + entry.path().filename().string(); 23 | return _i2c.start(i2c_dev.c_str(), UGREEN_LED_I2C_ADDR); 24 | } 25 | } 26 | } 27 | 28 | return -1; 29 | } 30 | 31 | static int compute_checksum(const std::vector& data, int size) { 32 | if (size < 2 || size > (int)data.size()) 33 | return 0; 34 | 35 | int sum = 0; 36 | for (int i = 0; i < size; ++i) 37 | sum += (int)data[i]; 38 | 39 | return sum; 40 | } 41 | 42 | static bool verify_checksum(const std::vector& data) { 43 | int size = data.size(); 44 | if (size < 2) return false; 45 | int sum = compute_checksum(data, size - 2); 46 | return sum != 0 && sum == (data[size - 1] | (((int)data[size - 2]) << 8)); 47 | } 48 | 49 | static void append_checksum(std::vector& data) { 50 | int size = data.size(); 51 | int sum = compute_checksum(data, size); 52 | data.push_back((sum >> 8) & 0xff); 53 | data.push_back(sum & 0xff); 54 | } 55 | 56 | ugreen_leds_t::led_data_t ugreen_leds_t::get_status(led_type_t id) { 57 | led_data_t data { }; 58 | data.is_available = false; 59 | 60 | auto raw_data = _i2c.read_block_data(0x81 + (uint8_t)id, 0xb); 61 | if (raw_data.size() != 0xb || !verify_checksum(raw_data)) 62 | return data; 63 | 64 | switch (raw_data[0]) { 65 | case 0: data.op_mode = op_mode_t::off; break; 66 | case 1: data.op_mode = op_mode_t::on; break; 67 | case 2: data.op_mode = op_mode_t::blink; break; 68 | case 3: data.op_mode = op_mode_t::breath; break; 69 | default: return data; 70 | }; 71 | 72 | 73 | data.brightness = raw_data[1]; 74 | data.color_r = raw_data[2]; 75 | data.color_g = raw_data[3]; 76 | data.color_b = raw_data[4]; 77 | int t_hight = (((int)raw_data[5]) << 8) | raw_data[6]; 78 | int t_low = (((int)raw_data[7]) << 8) | raw_data[8]; 79 | data.t_on = t_low; 80 | data.t_off = t_hight - t_low; 81 | data.is_available = true; 82 | 83 | return data; 84 | } 85 | 86 | int ugreen_leds_t::_change_status(led_type_t id, uint8_t command, std::array, 4> params) { 87 | std::vector data { 88 | // 3c 3b 3a 89 | 0x00, 0xa0, 0x01, 90 | // 39 38 37 91 | 0x00, 0x00, command, 92 | // 36 - 33 93 | params[0].value_or(0x00), 94 | params[1].value_or(0x00), 95 | params[2].value_or(0x00), 96 | params[3].value_or(0x00), 97 | }; 98 | 99 | append_checksum(data); 100 | data[0] = (uint8_t)id; 101 | return _i2c.write_block_data((uint8_t)id, data); 102 | } 103 | 104 | int ugreen_leds_t::set_onoff(led_type_t id, uint8_t status) { 105 | if (status >= 2) return -1; 106 | return _change_status(id, 0x03, { status } ); 107 | } 108 | 109 | int ugreen_leds_t::_set_blink_or_breath(uint8_t command, led_type_t id, uint16_t t_on, uint16_t t_off) { 110 | uint16_t t_hight = t_on + t_off; 111 | uint16_t t_low = t_on; 112 | return _change_status(id, command, { 113 | (uint8_t)(t_hight >> 8), 114 | (uint8_t)(t_hight & 0xff), 115 | (uint8_t)(t_low >> 8), 116 | (uint8_t)(t_low & 0xff), 117 | } ); 118 | } 119 | 120 | int ugreen_leds_t::set_rgb(led_type_t id, uint8_t r, uint8_t g, uint8_t b) { 121 | return _change_status(id, 0x02, { r, g, b } ); 122 | } 123 | 124 | int ugreen_leds_t::set_brightness(led_type_t id, uint8_t brightness) { 125 | return _change_status(id, 0x01, { brightness } ); 126 | } 127 | 128 | bool ugreen_leds_t::is_last_modification_successful() { 129 | return _i2c.read_byte_data(0x80) == 1; 130 | } 131 | 132 | int ugreen_leds_t::set_blink(led_type_t id, uint16_t t_on, uint16_t t_off) { 133 | return _set_blink_or_breath(0x04, id, t_on, t_off); 134 | } 135 | 136 | int ugreen_leds_t::set_breath(led_type_t id, uint16_t t_on, uint16_t t_off) { 137 | return _set_blink_or_breath(0x05, id, t_on, t_off); 138 | } 139 | -------------------------------------------------------------------------------- /cli/ugreen_leds.h: -------------------------------------------------------------------------------- 1 | #ifndef __UGREEN_LEDS_H__ 2 | #define __UGREEN_LEDS_H__ 3 | 4 | #include 5 | #include 6 | 7 | #include "i2c.h" 8 | 9 | #define UGREEN_LED_POWER ugreen_leds_t::led_type_t::power 10 | #define UGREEN_LED_NETDEV ugreen_leds_t::led_type_t::netdev 11 | #define UGREEN_LED_DISK1 ugreen_leds_t::led_type_t::disk1 12 | #define UGREEN_LED_DISK2 ugreen_leds_t::led_type_t::disk2 13 | #define UGREEN_LED_DISK3 ugreen_leds_t::led_type_t::disk3 14 | #define UGREEN_LED_DISK4 ugreen_leds_t::led_type_t::disk4 15 | #define UGREEN_LED_DISK5 ugreen_leds_t::led_type_t::disk5 16 | #define UGREEN_LED_DISK6 ugreen_leds_t::led_type_t::disk6 17 | #define UGREEN_LED_DISK7 ugreen_leds_t::led_type_t::disk7 18 | #define UGREEN_LED_DISK8 ugreen_leds_t::led_type_t::disk8 19 | 20 | // #define UGREEN_LED_I2C_DEV "/dev/i2c-1" 21 | #define UGREEN_LED_I2C_ADDR 0x3a 22 | 23 | class ugreen_leds_t { 24 | 25 | i2c_device_t _i2c; 26 | 27 | public: 28 | 29 | enum class op_mode_t : uint8_t { 30 | off = 0, on, blink, breath 31 | }; 32 | 33 | enum class led_type_t : uint8_t { 34 | power = 0, netdev, disk1, disk2, disk3, disk4, disk5, disk6, disk7, disk8 35 | }; 36 | 37 | struct led_data_t { 38 | bool is_available; 39 | op_mode_t op_mode; 40 | uint8_t brightness; 41 | uint8_t color_r, color_g, color_b; 42 | uint16_t t_on, t_off; 43 | 44 | }; 45 | 46 | public: 47 | int start(); 48 | 49 | led_data_t get_status(led_type_t id); 50 | int set_onoff(led_type_t id, uint8_t status); 51 | int set_rgb(led_type_t id, uint8_t r, uint8_t g, uint8_t b); 52 | int set_brightness(led_type_t id, uint8_t brightness); 53 | int set_blink(led_type_t id, uint16_t t_on, uint16_t t_off); 54 | int set_breath(led_type_t id, uint16_t t_on, uint16_t t_off); 55 | 56 | bool is_last_modification_successful(); 57 | 58 | private: 59 | int _set_blink_or_breath(uint8_t command, led_type_t id, uint16_t t_on, uint16_t t_off); 60 | int _change_status(led_type_t id, uint8_t command, std::array, 4> params); 61 | }; 62 | 63 | 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /cli/ugreen_leds_cli.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "ugreen_leds.h" 11 | 12 | #define MAX_RETRY_COUNT 5 13 | #define USLEEP_READ_STATUS_INTERVAL 8000 14 | #define USLEEP_READ_STATUS_RETRY_INTERVAL 3000 15 | #define USLEEP_MODIFICATION_INTERVAL 500 16 | #define USLEEP_MODIFICATION_RETRY_INTERVAL 3000 17 | #define USLEEP_MODIFICATION_QUERY_RESULT_INTERVAL 2000 18 | 19 | static std::map led_name_map = { 20 | { "power", UGREEN_LED_POWER }, 21 | { "netdev", UGREEN_LED_NETDEV }, 22 | { "disk1", UGREEN_LED_DISK1 }, 23 | { "disk2", UGREEN_LED_DISK2 }, 24 | { "disk3", UGREEN_LED_DISK3 }, 25 | { "disk4", UGREEN_LED_DISK4 }, 26 | { "disk5", UGREEN_LED_DISK5 }, 27 | { "disk6", UGREEN_LED_DISK6 }, 28 | { "disk7", UGREEN_LED_DISK7 }, 29 | { "disk8", UGREEN_LED_DISK8 }, 30 | }; 31 | 32 | using led_type_pair = std::pair; 33 | 34 | ugreen_leds_t::led_data_t get_status_robust(ugreen_leds_t &leds_controller, ugreen_leds_t::led_type_t led) { 35 | usleep(USLEEP_READ_STATUS_INTERVAL); 36 | auto data = leds_controller.get_status(led); 37 | 38 | for (int retry_cnt = 1; !data.is_available && retry_cnt < MAX_RETRY_COUNT; ++retry_cnt) { 39 | usleep(USLEEP_READ_STATUS_RETRY_INTERVAL); 40 | data = leds_controller.get_status(led); 41 | } 42 | 43 | return data; 44 | } 45 | 46 | void show_leds_info(ugreen_leds_t &leds_controller, const std::vector& leds) { 47 | 48 | for (auto led : leds) { 49 | 50 | auto data = get_status_robust(leds_controller, led.second); 51 | 52 | if (!data.is_available) { 53 | std::printf("%s: unavailable or non-existent\n", led.first.c_str()); 54 | continue; 55 | } 56 | 57 | std::string op_mode_txt = "unknown"; 58 | 59 | switch(data.op_mode) { 60 | case ugreen_leds_t::op_mode_t::off: 61 | op_mode_txt = "off"; break; 62 | case ugreen_leds_t::op_mode_t::on: 63 | op_mode_txt = "on"; break; 64 | case ugreen_leds_t::op_mode_t::blink: 65 | op_mode_txt = "blink"; break; 66 | case ugreen_leds_t::op_mode_t::breath: 67 | op_mode_txt = "breath"; break; 68 | }; 69 | 70 | std::printf("%s: status = %s, brightness = %d, color = RGB(%d, %d, %d)", 71 | led.first.c_str(), op_mode_txt.c_str(), (int)data.brightness, 72 | (int)data.color_r, (int)data.color_g, (int)data.color_b); 73 | 74 | if (data.op_mode == ugreen_leds_t::op_mode_t::blink) { 75 | std::printf(", blink_on = %d ms, blink_off = %d ms", 76 | (int)data.t_on, (int)data.t_off); 77 | } 78 | 79 | std::puts(""); 80 | } 81 | } 82 | 83 | void show_help() { 84 | std::cerr 85 | << "Usage: ugreen_leds_cli [LED-NAME...] [-on] [-off] [-(blink|breath) T_ON T_OFF]\n" 86 | " [-color R G B] [-brightness BRIGHTNESS] [-status]\n\n" 87 | " LED_NAME: separated by white space, possible values are\n" 88 | " { power, netdev, disk[1-8], all }.\n" 89 | " -on / -off: turn on / off corresponding LEDs.\n" 90 | " -blink / -breath: set LED to the blink / breath mode. This \n" 91 | " mode keeps the LED on for T_ON millseconds and then\n" 92 | " keeps it off for T_OFF millseconds. \n" 93 | " T_ON and T_OFF should belong to [0, 65535].\n" 94 | " -color: set the color of corresponding LEDs.\n" 95 | " R, G and B should belong to [0, 255].\n" 96 | " -brightness: set the brightness of corresponding LEDs.\n" 97 | " BRIGHTNESS should belong to [0, 255].\n" 98 | " -status: display the status of corresponding LEDs.\n" 99 | << std::endl; 100 | } 101 | 102 | void show_help_and_exit() { 103 | show_help(); 104 | std::exit(-1); 105 | } 106 | 107 | ugreen_leds_t::led_type_t get_led_type(const std::string& name) { 108 | if (led_name_map.find(name) == led_name_map.end()) { 109 | std::cerr << "Err: unknown LED name " << name << std::endl; 110 | show_help_and_exit(); 111 | } 112 | 113 | return led_name_map[name]; 114 | } 115 | 116 | int parse_integer(const std::string& str, int low = 0, int high = 0xffff) { 117 | std::size_t size; 118 | int x = std::stoi(str, &size); 119 | 120 | if (size != str.size()) { 121 | std::cerr << "Err: " << str << " is not an integer." << std::endl; 122 | show_help_and_exit(); 123 | } 124 | 125 | if (x < low || x > high) { 126 | std::cerr << "Err: " << str << " is not in [" << low << ", " << high << "]" << std::endl; 127 | show_help_and_exit(); 128 | } 129 | 130 | return x; 131 | } 132 | 133 | int main(int argc, char *argv[]) 134 | { 135 | 136 | if (argc < 2) { 137 | show_help(); 138 | return 0; 139 | } 140 | 141 | ugreen_leds_t leds_controller; 142 | if (leds_controller.start() != 0) { 143 | std::cerr << "Err: fail to open the I2C device." << std::endl; 144 | std::cerr << "Please check that (1) you have the root permission; " << std::endl; 145 | std::cerr << " and (2) the i2c-dev module is loaded. " << std::endl; 146 | return -1; 147 | } 148 | 149 | std::deque args; 150 | for (int i = 1; i < argc; ++i) 151 | args.emplace_back(argv[i]); 152 | 153 | // parse LED names 154 | std::vector leds; 155 | 156 | while (!args.empty() && args.front().front() != '-') { 157 | if (args.front() == "all") { 158 | for (const auto &v : led_name_map) { 159 | if (get_status_robust(leds_controller, v.second).is_available) 160 | leds.push_back(v); 161 | } 162 | } else { 163 | auto led_type = get_led_type(args.front()); 164 | leds.emplace_back(args.front(), led_type); 165 | } 166 | 167 | args.pop_front(); 168 | } 169 | 170 | // if no additional parameters, display current info 171 | if (args.empty()) { 172 | show_leds_info(leds_controller, leds); 173 | return 0; 174 | } 175 | 176 | // (is_modification, callback) 177 | using ops_pair = std::pair>; 178 | std::vector ops_seq; 179 | 180 | while (!args.empty()) { 181 | if (args.front() == "-on" || args.front() == "-off") { 182 | // turn on / off LEDs 183 | uint8_t status = args.front() == "-on"; 184 | ops_seq.emplace_back(true, [=, &leds_controller](led_type_pair led) { 185 | return leds_controller.set_onoff(led.second, status); 186 | } ); 187 | 188 | args.pop_front(); 189 | } else if(args.front() == "-blink" || args.front() == "-breath") { 190 | // set blink 191 | bool is_blink = (args.front() == "-blink"); 192 | args.pop_front(); 193 | 194 | if (args.size() < 2) { 195 | std::cerr << "Err: -blink / -breath requires 2 parameters" << std::endl; 196 | show_help_and_exit(); 197 | } 198 | 199 | uint16_t t_on = parse_integer(args.front(), 0x0000, 0xffff); 200 | args.pop_front(); 201 | uint16_t t_off = parse_integer(args.front(), 0x0000, 0xffff); 202 | args.pop_front(); 203 | 204 | ops_seq.emplace_back(true, [=, &leds_controller](led_type_pair led) { 205 | if (is_blink) { 206 | return leds_controller.set_blink(led.second, t_on, t_off); 207 | } else { 208 | return leds_controller.set_breath(led.second, t_on, t_off); 209 | } 210 | } ); 211 | } else if(args.front() == "-color") { 212 | // set color 213 | args.pop_front(); 214 | 215 | if (args.size() < 3) { 216 | std::cerr << "Err: -color requires 3 parameters" << std::endl; 217 | show_help_and_exit(); 218 | } 219 | 220 | uint8_t R = parse_integer(args.front(), 0x00, 0xff); 221 | args.pop_front(); 222 | uint8_t G = parse_integer(args.front(), 0x00, 0xff); 223 | args.pop_front(); 224 | uint8_t B = parse_integer(args.front(), 0x00, 0xff); 225 | args.pop_front(); 226 | ops_seq.emplace_back(true, [=, &leds_controller](led_type_pair led) { 227 | return leds_controller.set_rgb(led.second, R, G, B); 228 | } ); 229 | } else if(args.front() == "-brightness") { 230 | // set brightness 231 | args.pop_front(); 232 | 233 | if (args.size() < 1) { 234 | std::cerr << "Err: -brightness requires 1 parameter" << std::endl; 235 | show_help_and_exit(); 236 | } 237 | 238 | uint8_t brightness = parse_integer(args.front(), 0x00, 0xff); 239 | args.pop_front(); 240 | ops_seq.emplace_back(true, [=, &leds_controller](led_type_pair led) { 241 | return leds_controller.set_brightness(led.second, brightness); 242 | } ); 243 | } else if(args.front() == "-status") { 244 | // display the status 245 | args.pop_front(); 246 | 247 | ops_seq.emplace_back(false, [=, &leds_controller](led_type_pair led) { 248 | show_leds_info(leds_controller, { led } ); 249 | return 0; 250 | } ); 251 | } else { 252 | std::cerr << "Err: unknown parameter " << args.front() << std::endl; 253 | show_help_and_exit(); 254 | } 255 | } 256 | 257 | for (const auto& led : leds) { 258 | for (const auto& fn_pair : ops_seq) { 259 | bool is_modification = fn_pair.first; 260 | const auto &fn = fn_pair.second; 261 | int last_status = -1; 262 | 263 | for (int retry_cnt = 0; retry_cnt < MAX_RETRY_COUNT && last_status != 0; ++retry_cnt) { 264 | 265 | if (retry_cnt == 0) { 266 | if (is_modification) 267 | usleep(USLEEP_MODIFICATION_INTERVAL); // usleep_range(200, 0x5dc) 268 | } else { 269 | usleep(USLEEP_MODIFICATION_RETRY_INTERVAL); 270 | } 271 | 272 | last_status = fn(led); 273 | 274 | if (last_status == 0 && is_modification) { 275 | usleep(USLEEP_MODIFICATION_QUERY_RESULT_INTERVAL); 276 | last_status = !leds_controller.is_last_modification_successful(); 277 | } 278 | } 279 | 280 | if (last_status != 0) { 281 | std::cerr << "failed to change status!" << std::endl; 282 | return -1; 283 | } 284 | } 285 | } 286 | 287 | 288 | return 0; 289 | } 290 | 291 | -------------------------------------------------------------------------------- /kmod/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = led-ugreen 2 | obj-m += led-ugreen.o 3 | ccflags-y := -std=gnu11 4 | 5 | # if KERNELRELEASE isn't set, i.e. not being built w/ DMKS, then use uname -r 6 | KERNELRELEASE ?= $(shell uname -r) 7 | KDIR ?= /lib/modules/$(KERNELRELEASE)/build 8 | INSTALL_MOD_PATH ?= / 9 | 10 | all: 11 | make -C $(KDIR) M=$(PWD) modules 12 | 13 | clean: 14 | make -C $(KDIR) M=$(PWD) clean 15 | 16 | install: 17 | make -C $(KDIR) M=$(PWD) INSTALL_MOD_PATH=$(INSTALL_MOD_PATH) modules_install 18 | -------------------------------------------------------------------------------- /kmod/dkms.conf: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME="ugreen-led" 2 | PACKAGE_VERSION="0.1" 3 | MAKE[0]="make" 4 | CLEAN="make clean" 5 | BUILT_MODULE_NAME[0]="led-ugreen" 6 | DEST_MODULE_LOCATION[0]="/kernel/drivers/leds" 7 | AUTOINSTALL="yes" 8 | -------------------------------------------------------------------------------- /kmod/led-ugreen.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-only 2 | /* 3 | * UGREEN NAS LED driver. 4 | * 5 | * Copyright (C) 2024. 6 | * Author: Yuhao Zhou 7 | */ 8 | 9 | #include "linux/stddef.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include "led-ugreen.h" 21 | 22 | #ifdef pr_fmt 23 | #undef pr_fmt 24 | #endif 25 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt 26 | 27 | static bool verbose = false; 28 | module_param(verbose, bool, 0644); 29 | MODULE_PARM_DESC(verbose, "Enable verbose output"); 30 | 31 | static struct ugreen_led_state *lcdev_to_ugreen_led_state(struct led_classdev *led_cdev) { 32 | return container_of(led_cdev, struct ugreen_led_state, cdev); 33 | } 34 | 35 | static int ugreen_led_change_state( 36 | struct i2c_client *client, 37 | u8 led_id, 38 | u8 command, 39 | u8 param1, 40 | u8 param2, 41 | u8 param3, 42 | u8 param4 43 | ) { 44 | // compute the checksum 45 | u16 cksum = 0xa1 + (u16)command + param1 + param2 + param3 + param4; 46 | 47 | // construct the write buffer 48 | u8 buf[12] = { 49 | led_id, 50 | 0xa0, 0x01, 0x00, 0x00, 51 | command, 52 | param1, param2, param3, param4, 53 | (u8)((cksum >> 8) & 0xff), 54 | (u8)(cksum & 0xff) 55 | }; 56 | 57 | // write the buffer to the I2C device by sending block data 58 | s32 rc = i2c_smbus_write_i2c_block_data(client, led_id, 12, buf); 59 | 60 | // check the return code 61 | if (rc < 0) { 62 | pr_err("%s: i2c_smbus_write_i2c_block_data failed with id %d," 63 | "cmd 0x%x, params (0x%x, 0x%x, 0x%x, 0x%x), err %d", 64 | __func__, led_id, command, param1, param2, param3, param4, rc); 65 | return rc; 66 | } 67 | 68 | return 0; 69 | } 70 | 71 | // get the state of the DX4600 LEDs 72 | static int ugreen_led_get_state( 73 | struct i2c_client *client, 74 | u8 led_id, 75 | struct ugreen_led_state *state 76 | ) { 77 | if (!state) { 78 | pr_err("%s: invalid state buffer", __func__); 79 | return -1; 80 | } 81 | 82 | // read the state of the LED from the I2C device 83 | u8 buf[11]; 84 | s32 rc = i2c_smbus_read_i2c_block_data(client, 0x81 + led_id, 11, (u8 *)buf); 85 | 86 | // check the return code 87 | if (rc < 0) { 88 | pr_err("%s: i2c_smbus_read_i2c_block_data failed with id %d, err %d", 89 | __func__, led_id, rc); 90 | return rc; 91 | } 92 | 93 | // compute the checksum of received data 94 | u16 sum = 0; 95 | for (int i = 0; i < 9; ++i) { 96 | sum += buf[i]; 97 | } 98 | 99 | // check the checksum 100 | if (sum == 0 || (sum != (((u16)buf[9] << 8) | buf[10]))) { 101 | return -1; 102 | } 103 | 104 | // parse the state of the LED 105 | state->status = buf[0]; 106 | state->brightness = buf[1]; 107 | state->r = buf[2]; 108 | state->g = buf[3]; 109 | state->b = buf[4]; 110 | u16 t_hight = (((int)buf[5]) << 8) | buf[6]; 111 | u16 t_low = (((int)buf[7]) << 8) | buf[8]; 112 | state->t_on = t_low; 113 | state->t_cycle = t_hight; 114 | 115 | return 0; 116 | } 117 | 118 | static bool ugreen_led_get_last_command_status(struct i2c_client *client) { 119 | 120 | // read the status byte from the I2C device 121 | s32 rc = i2c_smbus_read_byte_data(client, 0x80); 122 | 123 | // check the return code 124 | if (rc < 0) { 125 | pr_err("%s: i2c_smbus_read_byte_data failed with err %d", __func__, rc); 126 | return false; 127 | } 128 | 129 | return rc == 1; 130 | } 131 | 132 | static int ugreen_led_change_state_robust( 133 | struct i2c_client *client, 134 | u8 led_id, 135 | u8 command, 136 | u8 param1, 137 | u8 param2, 138 | u8 param3, 139 | u8 param4 140 | ) { 141 | int rc = 0; 142 | for (int i = 0; i < UGREEN_LED_CHANGE_STATE_RETRY_COUNT; ++i) { 143 | 144 | if (i == 0) usleep_range(500, 1500); 145 | else msleep(30); 146 | 147 | if (i > 0) pr_debug("retrying %d", i); 148 | 149 | rc = ugreen_led_change_state(client, led_id, command, param1, param2, param3, param4); 150 | if (rc == 0) { 151 | usleep_range(1500, 2500); 152 | if (ugreen_led_get_last_command_status(client)) { 153 | return 0; 154 | } 155 | } 156 | } 157 | 158 | return -1; 159 | } 160 | 161 | static int ugreen_led_get_state_robust( 162 | struct i2c_client *client, 163 | u8 led_id, 164 | struct ugreen_led_state *state 165 | ) { 166 | int rc = 0; 167 | for (int i = 0; i < UGREEN_LED_CHANGE_STATE_RETRY_COUNT; ++i) { 168 | 169 | if (i == 0) usleep_range(500, 1500); 170 | else msleep(30); 171 | 172 | rc = ugreen_led_get_state(client, led_id, state); 173 | if (rc == 0) return 0; 174 | } 175 | 176 | state->status = UGREEN_LED_STATE_INVALID; 177 | 178 | return -1; 179 | } 180 | 181 | static void ugreen_led_turn_on_or_off_unlock(struct ugreen_led_array *priv, u8 led_id, bool on) { 182 | 183 | if (priv->state[led_id].status == (on ? UGREEN_LED_STATE_ON : UGREEN_LED_STATE_OFF)) { 184 | return; 185 | } 186 | 187 | int rc = ugreen_led_change_state_robust(priv->client, led_id, 0x03, on ? 1 : 0, 0, 0, 0); 188 | if (rc == 0) { 189 | priv->state[led_id].status = on ? UGREEN_LED_STATE_ON : UGREEN_LED_STATE_OFF; 190 | } else if (verbose) { 191 | pr_err("failed to turn %d %s", led_id, on ? "on" : "off"); 192 | } 193 | } 194 | 195 | static void ugreen_led_set_brightness_unlock(struct ugreen_led_array *priv, u8 led_id, enum led_brightness brightness) { 196 | 197 | struct ugreen_led_state *state = priv->state + led_id; 198 | 199 | if (brightness == 0) { 200 | ugreen_led_turn_on_or_off_unlock(priv, led_id, false); 201 | } else { 202 | if (state->brightness != brightness) { 203 | int rc = ugreen_led_change_state_robust(priv->client, led_id, 0x01, brightness, 0, 0, 0); 204 | if (rc == 0) { 205 | state->brightness = brightness; 206 | } else if (verbose) { 207 | pr_err("failed to set brightness of %d to %d", led_id, brightness); 208 | } 209 | } 210 | 211 | if (state->status == UGREEN_LED_STATE_OFF) 212 | ugreen_led_turn_on_or_off_unlock(priv, led_id, true); 213 | } 214 | } 215 | 216 | static void ugreen_led_set_color_unlock(struct ugreen_led_array *priv, u8 led_id, u8 r, u8 g, u8 b) { 217 | 218 | struct ugreen_led_state *state = priv->state + led_id; 219 | 220 | if (!r && !g && !b) { 221 | return ugreen_led_turn_on_or_off_unlock(priv, led_id, false); 222 | } 223 | 224 | if (state->r != r || state->g != g || state->b != b) { 225 | int rc = ugreen_led_change_state_robust(priv->client, led_id, 0x02, r, g, b, 0); 226 | if (rc == 0) { 227 | state->r = r; 228 | state->g = g; 229 | state->b = b; 230 | } else if (verbose) { 231 | pr_err("failed to set color of %d to 0x%02x%02x%02x", led_id, r, g, b); 232 | } 233 | } 234 | } 235 | 236 | static void ugreen_led_set_blink_or_breath_unlock(struct ugreen_led_array *priv, u8 led_id, u16 t_on, u16 t_cycle, bool is_blink) { 237 | 238 | int rc; 239 | struct ugreen_led_state *state = priv->state + led_id; 240 | u8 led_status = is_blink ? UGREEN_LED_STATE_BLINK : UGREEN_LED_STATE_BREATH; 241 | 242 | if (state->t_on == t_on && state->t_cycle == t_cycle && state->status == led_status) { 243 | rc = 0; 244 | } else { 245 | rc = ugreen_led_change_state_robust(priv->client, led_id, is_blink ? 0x04 : 0x05, 246 | (u8)(t_cycle >> 8), (u8)(t_cycle & 0xff), 247 | (u8)(t_on >> 8), (u8)(t_on & 0xff) 248 | ); 249 | 250 | if (rc == 0) { 251 | state->t_on = t_on; 252 | state->t_cycle = t_cycle; 253 | state->status = led_status; 254 | } else if (verbose) { 255 | pr_err("failed to set %s of %d to %d %d", is_blink ? "blink" : "breath", led_id, t_on, t_cycle); 256 | } 257 | } 258 | } 259 | 260 | static int ugreen_led_set_brightness_blocking(struct led_classdev *cdev, enum led_brightness brightness) { 261 | 262 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 263 | struct ugreen_led_array *priv = state->priv; 264 | int led_id = state->led_id; 265 | 266 | pr_debug("set brightness of %d to %d\n", led_id, brightness); 267 | 268 | mutex_lock(&priv->mutex); 269 | ugreen_led_set_brightness_unlock(priv, led_id, brightness); 270 | mutex_unlock(&priv->mutex); 271 | 272 | return 0; 273 | } 274 | 275 | static enum led_brightness ugreen_led_get_brightness(struct led_classdev *cdev) { 276 | 277 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 278 | 279 | pr_debug("get brightness of %d\n", state->led_id); 280 | 281 | if (!state->r && !state->g && !state->b) 282 | return LED_OFF; 283 | 284 | return state->status == UGREEN_LED_STATE_OFF ? LED_OFF : state->brightness; 285 | } 286 | 287 | static void truncate_blink_delay_time(unsigned long *delay_on, unsigned long *delay_off) { 288 | 289 | if (*delay_on < 100) *delay_on = 100; 290 | else if (*delay_on > 0x7fff) *delay_on = 0x7fff; 291 | 292 | if (*delay_off < 100) *delay_off = 100; 293 | else if (*delay_off > 0x7fff) *delay_off = 0x7fff; 294 | } 295 | 296 | static int ugreen_led_set_blink(struct led_classdev *cdev, unsigned long *delay_on, unsigned long *delay_off) { 297 | 298 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 299 | struct ugreen_led_array *priv = state->priv; 300 | int led_id = state->led_id; 301 | 302 | truncate_blink_delay_time(delay_on, delay_off); 303 | 304 | pr_debug("set blink of %d to %lu %lu\n", led_id, *delay_on, *delay_off); 305 | 306 | mutex_lock(&priv->mutex); 307 | 308 | ugreen_led_set_blink_or_breath_unlock(priv, led_id, *delay_on, *delay_on + *delay_off, true); 309 | *delay_on = state->t_on; 310 | *delay_off = state->t_cycle - state->t_on; 311 | 312 | mutex_unlock(&priv->mutex); 313 | 314 | return state->status == UGREEN_LED_STATE_BLINK ? 0 : -EINVAL; 315 | } 316 | 317 | static ssize_t color_store(struct device *dev, 318 | struct device_attribute *attr, 319 | const char *buf, size_t size) 320 | { 321 | 322 | struct led_classdev *cdev = dev_get_drvdata(dev); 323 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 324 | struct ugreen_led_array *priv = state->priv; 325 | int led_id = state->led_id; 326 | u8 r, g, b; 327 | 328 | int nrchars; 329 | 330 | if (sscanf(buf, "%hhu %hhu %hhu%n", &r, &g, &b, &nrchars) != 3) { 331 | return -EINVAL; 332 | } 333 | 334 | if (++nrchars < size) { 335 | return -EINVAL; 336 | } 337 | 338 | pr_debug("set color of %d to 0x%02x%02x%02x\n", led_id, r, g, b); 339 | 340 | mutex_lock(&priv->mutex); 341 | ugreen_led_set_color_unlock(priv, led_id, r, g, b); 342 | mutex_unlock(&priv->mutex); 343 | 344 | return size; 345 | } 346 | 347 | static ssize_t color_show(struct device *dev, struct device_attribute *attr, char *buf) { 348 | 349 | struct led_classdev *cdev = dev_get_drvdata(dev); 350 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 351 | return sprintf(buf, "%d %d %d\n", state->r, state->g, state->b); 352 | } 353 | 354 | static DEVICE_ATTR_RW(color); 355 | 356 | static ssize_t blink_type_store(struct device *dev, 357 | struct device_attribute *attr, 358 | const char *buf, size_t size) 359 | { 360 | 361 | struct led_classdev *cdev = dev_get_drvdata(dev); 362 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 363 | 364 | u8 blink_type; 365 | unsigned long delay_on, delay_off; 366 | int nrchars; 367 | 368 | if (sscanf(buf, "blink %lu %lu%n", &delay_on, &delay_off, &nrchars) == 2) { 369 | blink_type = UGREEN_LED_STATE_BLINK; 370 | } else if(sscanf(buf, "breath %lu %lu%n", &delay_on, &delay_off, &nrchars) == 2) { 371 | blink_type = UGREEN_LED_STATE_BREATH; 372 | } else if(strcmp(buf, "none\n") == 0) { 373 | blink_type = UGREEN_LED_STATE_ON; 374 | nrchars = size; 375 | } else return -EINVAL; 376 | 377 | if (++nrchars < size) { 378 | return -EINVAL; 379 | } 380 | 381 | mutex_lock(&state->priv->mutex); 382 | 383 | if (blink_type == UGREEN_LED_STATE_ON) { 384 | ugreen_led_turn_on_or_off_unlock(state->priv, state->led_id, true); 385 | } else { 386 | truncate_blink_delay_time(&delay_on, &delay_off); 387 | ugreen_led_set_blink_or_breath_unlock(state->priv, state->led_id, 388 | (u16)delay_on, (u16)(delay_on + delay_off), 389 | blink_type == UGREEN_LED_STATE_BLINK ? true : false); 390 | } 391 | 392 | mutex_unlock(&state->priv->mutex); 393 | 394 | return size; 395 | } 396 | 397 | static ssize_t blink_type_show(struct device *dev, struct device_attribute *attr, char *buf) { 398 | 399 | struct led_classdev *cdev = dev_get_drvdata(dev); 400 | struct ugreen_led_state *state = lcdev_to_ugreen_led_state(cdev); 401 | 402 | ssize_t size = 0; 403 | 404 | mutex_lock(&state->priv->mutex); 405 | u8 status = state->status; 406 | int delay_on = state->t_on; 407 | int delay_off = state->t_cycle - state->t_on; 408 | mutex_unlock(&state->priv->mutex); 409 | 410 | if (status == UGREEN_LED_STATE_BLINK) { 411 | size += sprintf(buf, "none [blink] breath\n"); 412 | } else if (status == UGREEN_LED_STATE_BREATH) { 413 | size += sprintf(buf, "none blink [breath]\n"); 414 | } else { 415 | size += sprintf(buf, "[none] blink breath\n"); 416 | } 417 | 418 | if (status == UGREEN_LED_STATE_BLINK || status == UGREEN_LED_STATE_BREATH) { 419 | size += sprintf(buf + size, "delay_on: %d, delay_off: %d\n", delay_on, delay_off); 420 | } 421 | 422 | size += sprintf(buf + size, "\nUsage: write \"blink \" to change the state.\n"); 423 | 424 | return size; 425 | } 426 | 427 | static DEVICE_ATTR_RW(blink_type); 428 | 429 | static ssize_t status_show(struct device *dev, struct device_attribute *attr, char *buf) { 430 | 431 | struct led_classdev *cdev = dev_get_drvdata(dev); 432 | struct ugreen_led_state state = *lcdev_to_ugreen_led_state(cdev); 433 | 434 | mutex_lock(&state.priv->mutex); 435 | int status = state.status; 436 | if (status >= ARRAY_SIZE(ugreen_led_state_name)) { 437 | status = UGREEN_LED_STATE_INVALID; 438 | } 439 | ssize_t size = sprintf(buf, "%s %d %d %d %d %d %d\n", 440 | ugreen_led_state_name[state.status], (int)state.brightness, 441 | (int)state.r, (int)state.g, (int)state.b, 442 | (int)state.t_on, (int)(state.t_cycle - state.t_on)); 443 | mutex_unlock(&state.priv->mutex); 444 | 445 | return size; 446 | } 447 | 448 | static DEVICE_ATTR_RO(status); 449 | 450 | static struct attribute *ugreen_led_attrs[] = { 451 | &dev_attr_color.attr, 452 | &dev_attr_status.attr, 453 | &dev_attr_blink_type.attr, 454 | NULL, 455 | }; 456 | 457 | ATTRIBUTE_GROUPS(ugreen_led); 458 | 459 | static int ugreen_led_probe(struct i2c_client *client) { 460 | 461 | pr_info ("i2c probed"); 462 | 463 | struct ugreen_led_array *priv; 464 | 465 | priv = devm_kzalloc(&client->dev, sizeof(struct ugreen_led_array), GFP_KERNEL); 466 | if (!priv) { 467 | return -ENOMEM; 468 | } 469 | 470 | priv->client = client; 471 | 472 | mutex_init(&priv->mutex); 473 | 474 | // probe and initialize leds 475 | for (int i = 0; i < UGREEN_MAX_LED_NUMBER; ++i) { 476 | 477 | priv->state[i].priv = priv; 478 | priv->state[i].led_id = i; 479 | 480 | ugreen_led_get_state_robust(client, i, priv->state + i); 481 | 482 | struct ugreen_led_state *state = priv->state + i; 483 | if (state->status != UGREEN_LED_STATE_INVALID) { 484 | 485 | pr_info("probed led id %d, status %d, rgb 0x%02x%02x%02x, " 486 | "brightness %d, t_on %d, t_cycle %d\n", i, 487 | state->status, state->r, state->g, state->b, 488 | state->brightness, state->t_on, state->t_cycle); 489 | 490 | ugreen_led_set_brightness_unlock(priv, i, 128); 491 | ugreen_led_set_color_unlock(priv, i, 0xff, 0xff, 0xff); 492 | 493 | } 494 | } 495 | 496 | i2c_set_clientdata(client, priv); 497 | 498 | mutex_lock(&priv->mutex); 499 | 500 | // register leds class devices 501 | const char *led_name[] = { 502 | "power", "netdev", "disk1", "disk2", "disk3", "disk4", "disk5", "disk6", "disk7", "disk8" 503 | }; 504 | 505 | for (int i = 0; i < UGREEN_MAX_LED_NUMBER; ++i) { 506 | 507 | struct ugreen_led_state *state = priv->state + i; 508 | if (state->status == UGREEN_LED_STATE_INVALID) 509 | continue; 510 | 511 | // register the brightness control 512 | if (i < ARRAY_SIZE(led_name)) 513 | state->cdev.name = led_name[i]; 514 | else state->cdev.name = "unknown"; 515 | 516 | state->cdev.brightness = state->cdev.brightness; 517 | state->cdev.max_brightness = 0xff; 518 | state->cdev.brightness_set_blocking = ugreen_led_set_brightness_blocking; 519 | state->cdev.brightness_get = ugreen_led_get_brightness; 520 | state->cdev.groups = ugreen_led_groups; 521 | state->cdev.blink_set = ugreen_led_set_blink; 522 | 523 | if (i == 1) { 524 | state->cdev.default_trigger = "netdev"; 525 | } else if (i >= 2) { 526 | state->cdev.default_trigger = "oneshot"; 527 | } 528 | 529 | led_classdev_register(&client->dev, &state->cdev); 530 | } 531 | 532 | mutex_unlock(&priv->mutex); 533 | 534 | return 0; 535 | } 536 | 537 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(6,1,0) 538 | static void 539 | #else 540 | static int 541 | #endif 542 | ugreen_led_remove(struct i2c_client *client) { 543 | 544 | struct ugreen_led_array *priv = i2c_get_clientdata(client); 545 | 546 | for (int i = 0; i < UGREEN_MAX_LED_NUMBER; ++i) { 547 | 548 | struct ugreen_led_state *state = priv->state + i; 549 | if (state->status == UGREEN_LED_STATE_INVALID) 550 | continue; 551 | 552 | led_classdev_unregister(&state->cdev); 553 | } 554 | 555 | mutex_destroy(&priv->mutex); 556 | 557 | pr_info ("i2c removed"); 558 | 559 | #if LINUX_VERSION_CODE < KERNEL_VERSION(6,1,0) 560 | return 0; 561 | #endif 562 | } 563 | 564 | static const struct i2c_device_id ugreen_led_id[] = { 565 | { UGREEN_LED_SLAVE_NAME, 0 }, 566 | { } 567 | }; 568 | 569 | MODULE_DEVICE_TABLE(i2c, ugreen_led_id); 570 | 571 | static struct i2c_driver ugreen_led_driver = { 572 | .driver = { 573 | .name = UGREEN_LED_SLAVE_NAME, 574 | .owner = THIS_MODULE, 575 | }, 576 | 577 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(6,3,0) 578 | .probe = ugreen_led_probe, 579 | #else 580 | .probe_new = ugreen_led_probe, 581 | #endif 582 | .remove = ugreen_led_remove, 583 | .id_table = ugreen_led_id, 584 | }; 585 | 586 | 587 | static int __init ugreen_led_init(void) { 588 | pr_info ("initializing"); 589 | i2c_add_driver(&ugreen_led_driver); 590 | return 0; 591 | } 592 | 593 | static void __exit ugreen_led_exit(void) { 594 | i2c_del_driver(&ugreen_led_driver); 595 | pr_info ("exited"); 596 | } 597 | 598 | module_init(ugreen_led_init); 599 | module_exit(ugreen_led_exit); 600 | 601 | 602 | // Module metadata 603 | MODULE_AUTHOR("Yuhao Zhou "); 604 | MODULE_DESCRIPTION("UGREEN NAS LED driver"); 605 | MODULE_LICENSE("GPL v2"); 606 | -------------------------------------------------------------------------------- /kmod/led-ugreen.h: -------------------------------------------------------------------------------- 1 | #ifndef __UGREEN_LED_H 2 | #define __UGREEN_LED_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | #define MODULE_NAME ( "led-ugreen" ) 10 | 11 | #define UGREEN_LED_SLAVE_ADDR ( 0x3a ) 12 | #define UGREEN_LED_SLAVE_NAME ( "led-ugreen" ) 13 | 14 | #define UGREEN_MAX_LED_NUMBER ( 10 ) 15 | #define UGREEN_LED_CHANGE_STATE_RETRY_COUNT ( 5 ) 16 | 17 | #define UGREEN_LED_STATE_OFF ( 0 ) 18 | #define UGREEN_LED_STATE_ON ( 1 ) 19 | #define UGREEN_LED_STATE_BLINK ( 2 ) 20 | #define UGREEN_LED_STATE_BREATH ( 3 ) 21 | #define UGREEN_LED_STATE_INVALID ( 4 ) 22 | 23 | static const char *ugreen_led_state_name[] = { "off", "on", "blink", "breath", "unknown" }; 24 | 25 | struct ugreen_led_array; 26 | 27 | struct ugreen_led_state { 28 | u8 status; 29 | u8 r, g, b; 30 | u8 brightness; 31 | u16 t_on, t_cycle; 32 | 33 | u8 led_id; 34 | struct led_classdev cdev; 35 | struct ugreen_led_array *priv; 36 | }; 37 | 38 | struct ugreen_led_array { 39 | struct i2c_client *client; 40 | struct mutex mutex; 41 | struct ugreen_led_state state[UGREEN_MAX_LED_NUMBER]; 42 | }; 43 | 44 | 45 | #endif // __UGREEN_LED_H 46 | -------------------------------------------------------------------------------- /scripts/blink-disk.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char *argv[]) { 9 | if (argc < 4 || argc % 2 != 0) { 10 | std::cerr << "Usage: " << argv[0] << " " 11 | << " ...\n"; 12 | return 1; 13 | } 14 | 15 | int num_devices = (argc - 2) / 2; 16 | int sleep_time_ms = int(std::stod(argv[1]) * 1000); 17 | 18 | std::vector block_device_stat_paths; 19 | std::vector led_device_shot_paths; 20 | 21 | for (int i = 0; i < num_devices; i++) { 22 | std::string block_device = argv[2 + 2 * i]; 23 | std::string led_device = argv[3 + 2 * i]; 24 | 25 | block_device_stat_paths.push_back( 26 | "/sys/block/" + block_device + "/stat"); 27 | led_device_shot_paths.push_back( 28 | "/sys/class/leds/" + led_device + "/shot"); 29 | } 30 | 31 | std::vector device_status(num_devices, true); 32 | std::vector old_lines(num_devices); 33 | 34 | while (true) { 35 | for (int i = 0; i < num_devices; i++) { 36 | 37 | if (!device_status[i]) { 38 | continue; 39 | } 40 | 41 | std::ifstream stat_file(block_device_stat_paths[i]); 42 | 43 | if (!stat_file.is_open()) { 44 | std::cerr << "Failed to open " << block_device_stat_paths[i] << "\n"; 45 | device_status[i] = false; 46 | continue; 47 | } 48 | 49 | std::string line; 50 | std::getline(stat_file, line); 51 | stat_file.close(); 52 | 53 | if (line != old_lines[i]) { 54 | 55 | std::ofstream led_file(led_device_shot_paths[i]); 56 | if (!led_file.is_open()) { 57 | std::cerr << "Failed to open " << led_device_shot_paths[i] << "\n"; 58 | device_status[i] = false; 59 | continue; 60 | } 61 | 62 | led_file << "1"; 63 | led_file.close(); 64 | 65 | old_lines[i] = std::move(line); 66 | } 67 | } 68 | 69 | std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time_ms)); 70 | } 71 | 72 | return 0; 73 | } 74 | -------------------------------------------------------------------------------- /scripts/check-standby.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | std::optional is_standby_mode(const std::string &device) { 15 | 16 | int fd = open(device.c_str(), O_RDONLY | O_NONBLOCK); 17 | if (fd == -1) { 18 | std::cerr << "Failed to open device: " << device << std::endl; 19 | return std::nullopt; 20 | } 21 | 22 | unsigned char args[4] = { WIN_CHECKPOWERMODE1, 0, 0, 0 }; 23 | 24 | if (ioctl(fd, HDIO_DRIVE_CMD, args) == -1) { 25 | std::cerr << "ioctl failed in checking power mode of " << device << std::endl; 26 | close(fd); 27 | return std::nullopt; 28 | } 29 | 30 | close(fd); 31 | 32 | return args[2] == 0x00; 33 | } 34 | 35 | int main(int argc, char *argv[]) { 36 | 37 | constexpr int preleading_args = 4; 38 | 39 | if (argc < preleading_args + 2 || (argc - preleading_args) % 2 != 0) { 40 | std::cerr << "Usage: " << argv[0] 41 | << " " 42 | << " " 43 | << " " 44 | << " ...\n"; 45 | return 1; 46 | } 47 | 48 | int num_devices = (argc - preleading_args) / 2; 49 | int checker_sleep_ms = int(std::stod(argv[1]) * 1000); 50 | 51 | std::string standby_color = argv[2]; 52 | std::string normal_color = argv[3]; 53 | std::vector block_device_paths; 54 | std::vector led_device_color_paths; 55 | 56 | std::cout << normal_color; 57 | 58 | for (int i = 0; i < num_devices; i++) { 59 | std::string block_device = argv[preleading_args + 2 * i]; 60 | std::string led_device = argv[preleading_args + 2 * i + 1]; 61 | 62 | 63 | block_device_paths.push_back( 64 | "/dev/" + block_device); 65 | led_device_color_paths.push_back( 66 | "/sys/class/leds/" + led_device + "/color"); 67 | } 68 | 69 | std::vector device_valid(num_devices, true); 70 | std::vector device_standby(num_devices, false); 71 | 72 | while (true) { 73 | for (int i = 0; i < num_devices; i++) { 74 | 75 | if (!device_valid[i]) { 76 | continue; 77 | } 78 | 79 | auto is_standby = is_standby_mode(block_device_paths[i]); 80 | 81 | if (!is_standby.has_value()) { 82 | device_valid[i] = false; 83 | continue; 84 | } 85 | 86 | if (is_standby.value() != device_standby[i]) { 87 | std::fstream led_color(led_device_color_paths[i], std::ios::in | std::ios::out); 88 | if (!led_color) { 89 | std::cerr << "Failed to open led color file: " 90 | << led_device_color_paths[i] << std::endl; 91 | device_valid[i] = false; 92 | continue; 93 | } 94 | 95 | std::string current_color; 96 | std::getline(led_color, current_color); 97 | 98 | if (device_standby[i] && current_color == standby_color) { 99 | led_color << normal_color << std::endl; 100 | } else if (!device_standby[i] && current_color == normal_color) { 101 | led_color << standby_color << std::endl; 102 | } 103 | 104 | device_standby[i] = is_standby.value(); 105 | } 106 | } 107 | 108 | std::this_thread::sleep_for(std::chrono::milliseconds(checker_sleep_ms)); 109 | } 110 | 111 | return 0; 112 | } 113 | -------------------------------------------------------------------------------- /scripts/systemd/ugreen-diskiomon.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=UGREEN LEDs daemon for monitoring diskio and blinking corresponding LEDs 3 | After=ugreen-probe-leds.service 4 | Requires=ugreen-probe-leds.service 5 | 6 | [Service] 7 | ExecStart=/usr/bin/ugreen-diskiomon 8 | StandardOutput=journal 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /scripts/systemd/ugreen-netdevmon@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=UGREEN LEDs daemon for monitoring netio (of %i) and blinking corresponding LEDs 3 | After=ugreen-probe-leds.service 4 | Requires=ugreen-probe-leds.service 5 | 6 | [Service] 7 | ExecStart=/usr/bin/ugreen-netdevmon %i 8 | StandardOutput=journal 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /scripts/systemd/ugreen-probe-leds.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=UGREEN LED initial hardware probing service 3 | After=systemd-modules-load.service 4 | Requires=systemd-modules-load.service 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/bin/ugreen-probe-leds 9 | RemainAfterExit=true 10 | StandardOutput=journal 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /scripts/ugreen-diskiomon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # function for removing lockfile 4 | exit-ugreen-diskiomon() { 5 | if [[ -f "/var/run/ugreen-diskiomon.lock" ]]; then 6 | rm "/var/run/ugreen-diskiomon.lock" 7 | fi 8 | kill $smart_check_pid 2>/dev/null 9 | kill $zpool_check_pid 2>/dev/null 10 | kill $disk_online_check_pid 2>/dev/null 11 | kill $standby_checker_pid 2>/dev/null 12 | } 13 | 14 | # trap exit and remove lockfile 15 | trap 'exit-ugreen-diskiomon' EXIT 16 | 17 | # check if script is already running 18 | if [[ -f "/var/run/ugreen-diskiomon.lock" ]]; then 19 | echo "ugreen-diskiomon already running!" 20 | exit 1 21 | fi 22 | touch /var/run/ugreen-diskiomon.lock 23 | 24 | # use variables from config file (unRAID specific) 25 | if [[ -f /boot/config/plugins/ugreenleds-driver/settings.cfg ]]; then 26 | source /boot/config/plugins/ugreenleds-driver/settings.cfg 27 | fi 28 | 29 | # load environment variables 30 | if [[ -f /etc/ugreen-leds.conf ]]; then 31 | source /etc/ugreen-leds.conf 32 | fi 33 | 34 | # led-disk mapping (see https://github.com/miskcoo/ugreen_dx4600_leds_controller/pull/4) 35 | MAPPING_METHOD=${MAPPING_METHOD:=ata} # ata, hctl, serial 36 | led_map=(disk1 disk2 disk3 disk4 disk5 disk6 disk7 disk8) 37 | 38 | # hctl, $> lsblk -S -x hctl -o hctl,serial,name 39 | # NOTE: It is reported that the order below should be adjusted for each model. 40 | # Please check the disk mapping section in https://github.com/miskcoo/ugreen_dx4600_leds_controller/blob/master/README.md. 41 | hctl_map=("0:0:0:0" "1:0:0:0" "2:0:0:0" "3:0:0:0" "4:0:0:0" "5:0:0:0" "6:0:0:0" "7:0:0:0") 42 | # serial number, $> lsblk -S -x hctl -o hctl,serial,name 43 | serial_map=(${DISK_SERIAL}) 44 | # ata number, $> ls /sys/block | egrep ata\d 45 | ata_map=("ata1" "ata2" "ata3" "ata4" "ata5" "ata6" "ata7" "ata8") 46 | 47 | if which dmidecode > /dev/null; then 48 | product_name=$(dmidecode --string system-product-name) 49 | case "${product_name}" in 50 | DXP6800*) # tested on DXP6800 Pro 51 | echo "Found UGREEN DXP6800 series" 52 | hctl_map=("2:0:0:0" "3:0:0:0" "4:0:0:0" "5:0:0:0" "0:0:0:0" "1:0:0:0") 53 | ata_map=("ata3" "ata4" "ata5" "ata6" "ata1" "ata2") 54 | ;; 55 | DX4600*) # tested on DX4600 Pro 56 | echo "Found UGREEN DX4600 series" 57 | ;; 58 | DX4700*) 59 | echo "Found UGREEN DX4700 series" 60 | ;; 61 | DXP2800*) # see issue #19 62 | echo "Found UGREEN DXP2800 series" 63 | ;; 64 | DXP4800*) 65 | echo "Found UGREEN DXP4800 series" 66 | ;; 67 | DXP8800*) # tested on DXP8800 Plus 68 | echo "Found UGREEN DXP8800 series" 69 | # using the default mapping 70 | ;; 71 | *) 72 | if [[ "${MAPPING_METHOD}" == "hctl" || "${MAPPING_METHOD}" == "ata" ]]; then 73 | echo -e "\033[0;31mUsing the default HCTL order. Please check it maps to your disk slots correctly." 74 | echo -e "If you confirm that the HCTL order is correct, or find it is different, you can " 75 | echo -e "submit an issue to let us know, so we can update the script." 76 | echo -e "Please read the disk mapping section in the link below for more details. " 77 | echo -e " https://github.com/miskcoo/ugreen_dx4600_leds_controller/blob/master/README.md\033[0m" 78 | fi 79 | ;; 80 | esac 81 | elif [[ "${MAPPING_METHOD}" == "hctl" || "${MAPPING_METHOD}" == "ata" ]]; then 82 | echo -e "\033[0;31minstalling the tool `dmidecode` is suggested; otherwise the script cannot detect your device and adjust the hctl/ata_map\033[0m" 83 | fi 84 | declare -A devices 85 | 86 | # set monitor SMART information to true by default if not running unRAID 87 | if [[ -f /etc/unraid-version ]]; then 88 | CHECK_SMART=false 89 | else 90 | CHECK_SMART=${CHECK_SMART:=true} 91 | fi 92 | # polling rate for smartctl. 360 seconds by default 93 | CHECK_SMART_INTERVAL=${CHECK_SMART_INTERVAL:=360} 94 | # refresh interval from disk leds 95 | LED_REFRESH_INTERVAL=${LED_REFRESH_INTERVAL:=0.1} 96 | 97 | # whether to check zpool health 98 | CHECK_ZPOOL=${CHECK_ZPOOL:=false} 99 | # polling rate for checking zpool health. 5 seconds by default 100 | CHECK_ZPOOL_INTERVAL=${CHECK_ZPOOL_INTERVAL:=5} 101 | 102 | # polling rate for checking disk online. 5 seconds by default 103 | CHECK_DISK_ONLINE_INTERVAL=${CHECK_DISK_ONLINE_INTERVAL:=5} 104 | 105 | COLOR_DISK_HEALTH=${COLOR_DISK_HEALTH:="255 255 255"} 106 | COLOR_DISK_UNAVAIL=${COLOR_DISK_UNAVAIL:="255 0 0"} 107 | COLOR_DISK_STANDBY=${COLOR_DISK_STANDBY:="0 0 255"} 108 | COLOR_ZPOOL_FAIL=${COLOR_ZPOOL_FAIL:="255 0 0"} 109 | COLOR_SMART_FAIL=${COLOR_SMART_FAIL:="255 0 0"} 110 | BRIGHTNESS_DISK_LEDS=${BRIGHTNESS_DISK_LEDS:="255"} 111 | 112 | 113 | { lsmod | grep ledtrig_oneshot > /dev/null; } || { modprobe -v ledtrig_oneshot && sleep 2; } 114 | 115 | function is_disk_healthy_or_standby() { 116 | if [[ "$1" == "$COLOR_DISK_HEALTH" || "$1" == "$COLOR_DISK_STANDBY" ]]; then 117 | return 0 # 0 means successful 118 | else 119 | return 1 120 | fi 121 | } 122 | 123 | function disk_enumerating_string() { 124 | if [[ $MAPPING_METHOD == ata ]]; then 125 | ls -ahl /sys/block | sed 's/\/$//' | awk '{ 126 | if (match($0, /ata[0-9]+/)) { 127 | ata = substr($0, RSTART, RLENGTH); 128 | if (match($0, /[^\/]+$/)) { 129 | basename = substr($0, RSTART, RLENGTH); 130 | print basename, ata; 131 | } 132 | } 133 | }' 134 | elif [[ $MAPPING_METHOD == hctl || $MAPPING_METHOD == serial ]]; then 135 | lsblk -S -o name,${MAPPING_METHOD},tran | grep sata 136 | else 137 | echo Unsupported mapping method: ${MAPPING_METHOD} 138 | exit 1 139 | fi 140 | } 141 | 142 | echo Enumerating disks based on $MAPPING_METHOD... 143 | declare -A dev_map 144 | while read line 145 | do 146 | blk_line=($line) 147 | key=${blk_line[1]} 148 | val=${blk_line[0]} 149 | dev_map[${key}]=${val} 150 | echo $MAPPING_METHOD ${key} ">>" ${dev_map[${key}]} 151 | done <<< "$(disk_enumerating_string)" 152 | 153 | # initialize LEDs 154 | declare -A dev_to_led_map 155 | for i in "${!led_map[@]}"; do 156 | led=${led_map[i]} 157 | if [[ -d /sys/class/leds/$led ]]; then 158 | echo oneshot > /sys/class/leds/$led/trigger 159 | echo 1 > /sys/class/leds/$led/invert 160 | echo 100 > /sys/class/leds/$led/delay_on 161 | echo 100 > /sys/class/leds/$led/delay_off 162 | echo "$COLOR_DISK_HEALTH" > /sys/class/leds/$led/color 163 | echo "$BRIGHTNESS_DISK_LEDS" > /sys/class/leds/$led/brightness 164 | 165 | # find corresponding device 166 | _tmp_str=${MAPPING_METHOD}_map[@] 167 | _tmp_arr=(${!_tmp_str}) 168 | 169 | if [[ -v "dev_map[${_tmp_arr[i]}]" ]]; then 170 | dev=${dev_map[${_tmp_arr[i]}]} 171 | 172 | if [[ -f /sys/class/block/${dev}/stat ]]; then 173 | devices[$led]=${dev} 174 | dev_to_led_map[$dev]=$led 175 | else 176 | # turn off the led if no disk installed on this slot 177 | echo 0 > /sys/class/leds/$led/brightness 178 | echo none > /sys/class/leds/$led/trigger 179 | fi 180 | else 181 | # turn off the led if no disk installed on this slot 182 | echo 0 > /sys/class/leds/$led/brightness 183 | echo none > /sys/class/leds/$led/trigger 184 | fi 185 | fi 186 | done 187 | 188 | # construct zpool device mapping 189 | declare -A zpool_ledmap 190 | if [ "$CHECK_ZPOOL" = true ]; then 191 | echo Enumerating zpool devices... 192 | while read line 193 | do 194 | zpool_dev_line=($line) 195 | zpool_dev_name=${zpool_dev_line[0]} 196 | zpool_scsi_dev_name="unknown" 197 | # zpool_dev_state=${zpool_dev_line[1]} 198 | case "$zpool_dev_name" in 199 | sd*) 200 | # remove the trailing partition number 201 | zpool_scsi_dev_name=$(echo $zpool_dev_name | sed 's/[0-9]*$//') 202 | ;; 203 | dm*) 204 | # find the underlying block device of the encrypted device 205 | dm_slaves=($(ls /sys/block/${zpool_dev_name}/slaves)) 206 | zpool_scsi_dev_name=${dm_slaves[0]} 207 | ;; 208 | *) 209 | echo Unsupported zpool device type ${zpool_dev_name}. 210 | ;; 211 | esac 212 | 213 | # if the detected scsi device can be found in the mapping array 214 | #echo zpool $zpool_dev_name ">>" $zpool_scsi_dev_name ">>" ${dev_to_led_map[${zpool_scsi_dev_name}]} 215 | if [[ -v "dev_to_led_map[${zpool_scsi_dev_name}]" ]]; then 216 | zpool_ledmap[$zpool_dev_name]=${dev_to_led_map[${zpool_scsi_dev_name}]} 217 | echo "zpool device" $zpool_dev_name ">>" $zpool_scsi_dev_name ">> LED:"${zpool_ledmap[$zpool_dev_name]} 218 | fi 219 | done <<< "$(zpool status -L | egrep ^\\s*\(sd\|dm\))" 220 | 221 | function zpool_check_loop() { 222 | while true; do 223 | while read line 224 | do 225 | zpool_dev_line=($line) 226 | zpool_dev_name=${zpool_dev_line[0]} 227 | zpool_dev_state=${zpool_dev_line[1]} 228 | 229 | # TODO: do something if the pool is unhealthy? 230 | 231 | if [[ -v "zpool_ledmap[${zpool_dev_name}]" ]]; then 232 | led=${zpool_ledmap[$zpool_dev_name]} 233 | led_color=$(cat /sys/class/leds/$led/color) 234 | 235 | if ! is_disk_healthy_or_standby "$led_color"; then 236 | continue; 237 | fi 238 | 239 | if [[ "${zpool_dev_state}" != "ONLINE" ]]; then 240 | echo "$COLOR_ZPOOL_FAIL" > /sys/class/leds/$led/color 241 | echo Disk failure detected on /dev/$zpool_dev_name at $(date +%Y-%m-%d' '%H:%M:%S) 242 | fi 243 | 244 | # ==== To recover from an error, you should restart the script ==== 245 | ## case "${zpool_dev_state}" in 246 | ## ONLINE) 247 | ## # echo "$COLOR_DISK_HEALTH" > /sys/class/leds/$led/color 248 | ## ;; 249 | ## *) 250 | ## echo "255 0 0" > /sys/class/leds/$led/color 251 | ## ;; 252 | ## esac 253 | fi 254 | done <<< "$(zpool status -L | egrep ^\\s*\(sd\|dm\))" 255 | 256 | sleep ${CHECK_ZPOOL_INTERVAL}s 257 | done 258 | } 259 | 260 | zpool_check_loop & 261 | zpool_check_pid=$! 262 | fi 263 | 264 | # check disk health if enabled 265 | if [ "$CHECK_SMART" = true ]; then 266 | ( 267 | while true; do 268 | for led in "${!devices[@]}"; do 269 | 270 | led_color=$(cat /sys/class/leds/$led/color) 271 | if ! is_disk_healthy_or_standby "$led_color"; then 272 | continue; 273 | fi 274 | 275 | dev=${devices[$led]} 276 | 277 | /usr/sbin/smartctl -H /dev/${dev} -n standby,1 &> /dev/null 278 | RET=$? 279 | 280 | # check return code for critical errors (any bit set except bit 5) 281 | # possible return bits set: https://invent.kde.org/system/kpmcore/-/merge_requests/28 282 | if (( $RET & ~32 )); then 283 | echo "$COLOR_SMART_FAIL" > /sys/class/leds/$led/color 284 | echo Disk failure detected on /dev/$dev at $(date +%Y-%m-%d' '%H:%M:%S) 285 | continue 286 | fi 287 | done 288 | sleep ${CHECK_SMART_INTERVAL}s 289 | done 290 | ) & 291 | smart_check_pid=$! 292 | fi 293 | 294 | # check disk online status 295 | ( 296 | while true; do 297 | for led in "${!devices[@]}"; do 298 | dev=${devices[$led]} 299 | 300 | led_color=$(cat /sys/class/leds/$led/color) 301 | if ! is_disk_healthy_or_standby "$led_color"; then 302 | continue; 303 | fi 304 | 305 | if [[ ! -f /sys/class/block/${dev}/stat ]]; then 306 | echo "$COLOR_DISK_UNAVAIL" > /sys/class/leds/$led/color 2>/dev/null 307 | echo Disk /dev/$dev went offline at $(date +%Y-%m-%d' '%H:%M:%S) 308 | continue 309 | fi 310 | done 311 | sleep ${CHECK_DISK_ONLINE_INTERVAL}s 312 | done 313 | ) & 314 | disk_online_check_pid=$! 315 | 316 | 317 | diskiomon_parameters() { 318 | for led in "${!devices[@]}"; do 319 | echo ${devices[$led]} $led 320 | done 321 | } 322 | 323 | # monitor disk standby modes 324 | STANDBY_MON_PATH=${STANDBY_MON_PATH:=/usr/bin/ugreen-check-standby} 325 | STANDBY_CHECK_INTERVAL=${STANDBY_CHECK_INTERVAL:=1} 326 | if [ -f "${STANDBY_MON_PATH}" ]; then 327 | ${STANDBY_MON_PATH} ${STANDBY_CHECK_INTERVAL} "${COLOR_DISK_STANDBY}" "${COLOR_DISK_HEALTH}" $(diskiomon_parameters) & 328 | standby_checker_pid=$1 329 | fi 330 | 331 | # monitor disk activities 332 | BLINK_MON_PATH=${BLINK_MON_PATH:=/usr/bin/ugreen-blink-disk} 333 | if [ -f "${BLINK_MON_PATH}" ]; then 334 | 335 | ${BLINK_MON_PATH} ${LED_REFRESH_INTERVAL} $(diskiomon_parameters) 336 | 337 | else 338 | declare -A diskio_data_rw 339 | while true; do 340 | for led in "${!devices[@]}"; do 341 | 342 | # if $dev does not exist, diskio_new_rw="", which will be safe 343 | diskio_new_rw="$(cat /sys/block/${devices[$led]}/stat 2>/dev/null)" 344 | 345 | if [ "${diskio_data_rw[$led]}" != "${diskio_new_rw}" ]; then 346 | echo 1 > /sys/class/leds/$led/shot 347 | fi 348 | 349 | diskio_data_rw[$led]=$diskio_new_rw 350 | done 351 | 352 | sleep ${LED_REFRESH_INTERVAL}s 353 | 354 | done 355 | fi 356 | -------------------------------------------------------------------------------- /scripts/ugreen-leds.conf: -------------------------------------------------------------------------------- 1 | 2 | # configuration file for ugreen-diskiomon and ugreen-netdevmon 3 | # it should be put in /etc/ugreen-leds.conf 4 | 5 | # =========== parameters of disk activities monitoring =========== 6 | 7 | # The method of mapping disks to LEDs: ata, hctl, serial 8 | # ata: default, also used in UGOS 9 | # $> ls -ahl /sys/block | grep ata[0-9] --color 10 | # * you should check whether it will change after reboot 11 | # 12 | # hctl: mapping by HCTL, 13 | # $> lsblk -S -x hctl -o hctl,serial,name 14 | # it will fail in some devices if you have USB disks inserted, but works well otherwise 15 | # * you should check whether it will change after reboot 16 | # ** see https://github.com/miskcoo/ugreen_dx4600_leds_controller/issues/14 17 | # 18 | # serial: suggested, mapping by serial 19 | # this method requires the user to check the disks' serial numbers 20 | # and fill the DISK_SERIAL array below (see the comments therein). 21 | MAPPING_METHOD=ata 22 | 23 | # The path of the compiled diskio monitor (OPTIONAL) 24 | BLINK_MON_PATH=/usr/bin/ugreen-blink-disk 25 | 26 | # The path of the compiled standby monitor (OPTIONAL) 27 | STANDBY_MON_PATH=/usr/bin/ugreen-check-standby 28 | 29 | # The sleep time between disk standby checks (default: 1 seconds) 30 | STANDBY_CHECK_INTERVAL=1 31 | 32 | # The serial numbers of disks (used only when MAPPING_METHOD=serial) 33 | # You need to record them before inserting to your NAS, and the corresponding disk slots. 34 | # If you have 4 disks, with serial numbers: SN1 SN2 SN3 SN4, 35 | # then the config below leads to the following mapping: 36 | # SN1 -- disk1 37 | # SN2 -- disk2 38 | # SN3 -- disk3 39 | # SN4 -- disk4 40 | DISK_SERIAL="SN1 SN2 SN3 SN4" 41 | 42 | # The sleep time between two disk activities checks (default: 0.1 seconds) 43 | LED_REFRESH_INTERVAL=0.1 44 | 45 | # brightness of disk LEDs, taking value from 1 to 255 (default: 255) 46 | BRIGHTNESS_DISK_LEDS="255" 47 | 48 | # color of a healthy disk (default: 255 255 255) 49 | COLOR_DISK_HEALTH="255 255 255" 50 | 51 | # color of an unavailable disk (default: 255 0 0) 52 | COLOR_DISK_UNAVAIL="255 0 0" 53 | 54 | # color of a disk in standby mode (default: 0 0 255) 55 | COLOR_DISK_STANDBY="0 0 255" 56 | 57 | # color of a failed zpool device (default: 255 0 0) 58 | COLOR_ZPOOL_FAIL="255 0 0" 59 | 60 | # color of a device with unhealthy smart info (default: 255 0 0) 61 | COLOR_SMART_FAIL="255 0 0" 62 | 63 | # Check the disk health by smartctl (default: true) 64 | CHECK_SMART=true 65 | 66 | # The sleep time between two smart checks (default: 360 seconds) 67 | CHECK_SMART_INTERVAL=360 68 | 69 | # Check the zpool health (default: false) 70 | CHECK_ZPOOL=false 71 | 72 | # The sleep time between two zpool checks (default: 5 seconds) 73 | CHECK_ZPOOL_INTERVAL=5 74 | 75 | # The sleep time between two disk online checks (default: 5 seconds) 76 | CHECK_DISK_ONLINE_INTERVAL=5 77 | 78 | 79 | # =========== parameters of network activities monitoring =========== 80 | 81 | # Blink the netdev light when sending data (default: 1) 82 | NETDEV_BLINK_TX=1 83 | 84 | # Blink the netdev light when receiving data (default: 1) 85 | NETDEV_BLINK_RX=1 86 | 87 | # A cycle of netdev blinking (default: 200 milliseconds) 88 | NETDEV_BLINK_INTERVAL=200 89 | 90 | # color of the netdev under the normal state (for CHECK_LINK_SPEED=false) 91 | COLOR_NETDEV_NORMAL="255 165 0" 92 | 93 | # The sleep time between two netdev connectivity / link speed monitoring (default: 60 seconds) 94 | CHECK_NETDEV_INTERVAL=60 95 | 96 | # Monitor the gateway connectivity (default: false) 97 | CHECK_GATEWAY_CONNECTIVITY=false 98 | 99 | # Monitor the link speed (default: false) 100 | CHECK_LINK_SPEED=false 101 | 102 | # brightness of the netdev LED, taking value from 1 to 255 (default: 255) 103 | BRIGHTNESS_NETDEV_LED="255" 104 | 105 | # color of the netdev under different link speeds (for CHECK_LINK_SPEED=true) 106 | COLOR_NETDEV_LINK_100="0 255 0" 107 | COLOR_NETDEV_LINK_1000="0 0 255" 108 | COLOR_NETDEV_LINK_2500="255 255 0" 109 | COLOR_NETDEV_LINK_10000="255 255 255" 110 | 111 | # color of the netdev when unable to ping the gateway 112 | COLOR_NETDEV_GATEWAY_UNREACHABLE="255 0 0" 113 | -------------------------------------------------------------------------------- /scripts/ugreen-netdevmon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # function for removing lockfile 4 | exit-ugreen-netdevmon() { 5 | if [[ -f "/var/run/ugreen-netdevmon.lock" ]]; then 6 | rm "/var/run/ugreen-netdevmon.lock" 7 | fi 8 | } 9 | 10 | # trap exit and remove lockfile 11 | trap 'exit-ugreen-netdevmon' EXIT 12 | 13 | # check if script is already running 14 | if [[ -f "/var/run/ugreen-netdevmon.lock" ]]; then 15 | echo "ugreen-netdevmon already running!" 16 | exit 1 17 | fi 18 | touch /var/run/ugreen-netdevmon.lock 19 | 20 | { lsmod | grep ledtrig_netdev > /dev/null; } || { modprobe -v ledtrig_netdev && sleep 2; } 21 | 22 | # load environment variables 23 | if [[ -f /etc/ugreen-leds.conf ]]; then 24 | source /etc/ugreen-leds.conf 25 | fi 26 | 27 | COLOR_NETDEV_NORMAL=${COLOR_NETDEV_NORMAL:="255 255 255"} 28 | COLOR_NETDEV_GATEWAY_UNREACHABLE=${COLOR_NETDEV_GATEWAY_UNREACHABLE:="255 0 0"} 29 | 30 | BRIGHTNESS_NETDEV_LED=${BRIGHTNESS_NETDEV_LED:="255"} 31 | 32 | CHECK_NETDEV_INTERVAL=${CHECK_NETDEV_INTERVAL:=60} 33 | CHECK_GATEWAY_CONNECTIVITY=${CHECK_GATEWAY_CONNECTIVITY:=false} 34 | CHECK_LINK_SPEED=${CHECK_LINK_SPEED:=false} 35 | 36 | led="netdev" 37 | netdev_name=$1 38 | echo netdev > /sys/class/leds/$led/trigger 39 | echo $netdev_name > /sys/class/leds/$led/device_name 40 | echo 1 > /sys/class/leds/$led/link 41 | echo ${NETDEV_BLINK_TX:=1} > /sys/class/leds/$led/tx 42 | echo ${NETDEV_BLINK_RX:=1} > /sys/class/leds/$led/rx 43 | echo ${NETDEV_BLINK_INTERVAL:=200} > /sys/class/leds/$led/interval 44 | echo $COLOR_NETDEV_NORMAL > /sys/class/leds/$led/color 45 | echo $BRIGHTNESS_NETDEV_LED > /sys/class/leds/$led/brightness 46 | 47 | function set_netdev_normal_color() { 48 | color=$COLOR_NETDEV_NORMAL 49 | 50 | if [[ $CHECK_LINK_SPEED == true ]]; then 51 | case $(cat /sys/class/net/$netdev_name/speed) in 52 | 100) color=${COLOR_NETDEV_LINK_100:=$COLOR_NETDEV_NORMAL};; 53 | 1000) color=${COLOR_NETDEV_LINK_1000:=$COLOR_NETDEV_NORMAL};; 54 | 2500) color=${COLOR_NETDEV_LINK_2500:=$COLOR_NETDEV_NORMAL};; 55 | 10000) color=${COLOR_NETDEV_LINK_10000:=$COLOR_NETDEV_NORMAL};; 56 | esac 57 | fi 58 | 59 | echo $color > /sys/class/leds/$led/color 60 | } 61 | 62 | if [[ $CHECK_GATEWAY_CONNECTIVITY == false && $CHECK_LINK_SPEED == false ]]; then 63 | exit 0 64 | fi 65 | 66 | gw_conn=1 67 | 68 | while true; do 69 | 70 | if [[ $CHECK_GATEWAY_CONNECTIVITY == true ]]; then 71 | gw=$(ip route | awk '/default/ { print $3 }') 72 | if ping -q -c 1 -W 1 $gw >/dev/null; then 73 | gw_conn=1 74 | else 75 | gw_conn=0 76 | fi 77 | fi 78 | 79 | if [[ $gw_conn == 1 ]]; then 80 | set_netdev_normal_color 81 | else 82 | echo $COLOR_NETDEV_GATEWAY_UNREACHABLE > /sys/class/leds/$led/color 83 | fi 84 | 85 | 86 | sleep ${CHECK_NETDEV_INTERVAL}s 87 | 88 | done 89 | -------------------------------------------------------------------------------- /scripts/ugreen-probe-leds: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -e 4 | 5 | { lsmod | grep i2c_dev ; } || modprobe -v i2c-dev 6 | { lsmod | grep led_ugreen ; } || modprobe -v led-ugreen 7 | 8 | i2c_dev=$(i2cdetect -l | grep "SMBus I801 adapter" | grep -Po "i2c-\d+") 9 | 10 | if [ $? = 0 ]; then 11 | echo "Found I2C device /dev/${i2c_dev}" 12 | dev_path=/sys/bus/i2c/devices/$i2c_dev/${i2c_dev/i2c-/}-003a 13 | if [ ! -d $dev_path ]; then 14 | echo "led-ugreen 0x3a" > /sys/bus/i2c/devices/${i2c_dev}/new_device 15 | elif [ "$(cat $dev_path/name)" != "led-ugreen" ]; then 16 | echo "ERROR: the device ${i2c_dev/i2c-/}-003a has been registered as $(cat $dev_path/name)" 17 | exit 1 18 | fi 19 | else 20 | echo "I2C device not found!" 21 | fi 22 | --------------------------------------------------------------------------------