├── .travis.yml ├── LICENSE ├── README.md ├── fanshim_driver.cpp ├── json.hpp └── rpi_monit_eg.png /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: cpp 4 | compiler: clang 5 | 6 | addons: 7 | apt: 8 | source: 9 | - ubuntu-toolchain-r-test 10 | - llvm-toolchain-bionic-7 11 | packages: 12 | - libgpiod-dev 13 | - clang-7 14 | - libc++abi-7-dev 15 | - libc++-7-dev 16 | - g++-8 17 | 18 | before_script: 19 | - wget http://mirrors.kernel.org/ubuntu/pool/universe/libg/libgpiod/libgpiod2_1.2-3_amd64.deb -O /tmp/libgpiod2_1.2-3_amd64.deb 20 | - wget http://mirrors.kernel.org/ubuntu/pool/universe/libg/libgpiod/libgpiod-dev_1.2-3_amd64.deb -O /tmp/libgpiod-dev_1.2-3_amd64.deb 21 | - sudo dpkg -i --force-conflicts /tmp/libgpiod2_1.2-3_amd64.deb 22 | - sudo dpkg -i --force-conflicts /tmp/libgpiod-dev_1.2-3_amd64.deb 23 | 24 | script: 25 | - clang++ fanshim_driver.cpp -o fanshim_driver -O3 -std=c++17 -lstdc++fs -lgpiodcxx 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 daviehh 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 | # fanshim-cpp 2 | 3 | [![Build Status](https://travis-ci.com/daviehh/fanshim-cpp.svg?branch=master)](https://travis-ci.com/daviehh/fanshim-cpp) 4 | 5 | C++ driver code for the fanshim on raspberry pi using `libgpiod` 6 | 7 | [pimoroni link](https://shop.pimoroni.com/products/fan-shim) 8 | 9 | **Warning/disclaimer:** 10 | 11 | **very experimental code, for testing purposes only.** 12 | 13 | **[may not work at any time or even cause hardware damage (e.g. from causing the fan to be constantly running)].** 14 | 15 | **Only proceed if you know what the code is doing.** 16 | 17 | ## Credits 18 | - official fanshim controller code https://github.com/pimoroni/fanshim-python 19 | - nlohmann/json https://github.com/nlohmann/json/ 20 | 21 | 22 | ## Build 23 | - If not installed: get the `libgpiod-dev` library 24 | - Put the `json.hpp` file from https://github.com/nlohmann/json/releases in the same folder as the source code, tested with `3.7.0` 25 | - Compile with `clang++ fanshim_driver.cpp -o fanshim_driver -O3 -std=c++17 -lstdc++fs -lgpiodcxx` (may also work with `g++`) 26 | 27 | 28 | ## Example systemd service file 29 | Change `/path/to/compiled/binary` to the compiled binary path. 30 | 31 | ``` 32 | [Unit] 33 | Description=Fanshim C++ driver 34 | 35 | [Service] 36 | ExecStart=/path/to/compiled/binary 37 | Restart=always 38 | RestartSec=5 39 | 40 | [Install] 41 | WantedBy=multi-user.target 42 | 43 | ``` 44 | 45 | ## Configuration 46 | 47 | The configuration file is in `/usr/local/etc/fanshim.json`, example: 48 | ```json 49 | { 50 | "on-threshold": 60, 51 | "off-threshold": 50, 52 | "delay": 10 53 | } 54 | ``` 55 | 56 | Will use the value in the file to override the defaults, no need to specify all keys, just the ones you want to change. Keys used: 57 | 58 | - `on-threshold`/`off-threshold`: temperature, in Celsius, the threshold for turing on (off) the fan. Default to 60 and 50 respectively. 59 | 60 | - `delay`: in seconds, the program will wait this amount of time before checking the temperature again. Default to 10. 61 | 62 | - `budget`: an integer n, the program will only turn on/off the fan if the temperature is consecutively above (below) the on (off) threshold for the last n temperature measurements. Defaults to 3. 63 | 64 | - `brightness`: an integer from 0 to 31, LED brightness, 0 means no LED (default). 65 | 66 | - `blink`: an integer in [0, 1 , 2], where 67 | - 0: no blink (default); 68 | - 1: the LED will blink when the fan is not spinning; 69 | - 2: the LED will "breath" when the fan is not spinning, the max brightness in this mode is `breath_brgt` (default 10). 70 | 71 | 72 | ## Notes/todo 73 | 74 | - No button support (I think given the small size of the button, it'll be easier to force the fan on/off through software based on e.g. whether a certain file exists. Currently, the file is hard-coded as `/usr/local/etc/.force_fanshim`: fan will be on if this file exists) 75 | 76 | 77 | 78 | ## Additional features 79 | 80 | - Will output current status to the file `/usr/local/etc/node_exp_txt/cpu_fan.prom` so that it can be used with external programs to monitor, e.g. node_exporter + prometheus + grafana: 81 | 82 | ![screen](https://raw.githubusercontent.com/daviehh/fanshim-cpp/master/rpi_monit_eg.png) 83 | -------------------------------------------------------------------------------- /fanshim_driver.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | // #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "json.hpp" 16 | #include 17 | // clang++ fanshim_driver.cpp -O3 -std=c++17 -lstdc++fs -lgpiodcxx -o out_binary 18 | 19 | using json = nlohmann::json; 20 | using namespace std; 21 | 22 | const int led_clck_pin = 14; 23 | const int led_dat_pin = 15; 24 | const int led_write_wait = 5; 25 | const int fanshim_pin = 18; 26 | 27 | const int LOW = 0; 28 | const int HIGH =1; 29 | 30 | int br_counter = 0; 31 | 32 | gpiod::chip rchip; 33 | gpiod::line ln_fan, ln_led_clk, ln_led_dat; 34 | 35 | //only for <1 sec 36 | int nano_usleep_frac(long msec) 37 | { 38 | struct timespec req; 39 | req.tv_sec = 0; 40 | req.tv_nsec = (long)(msec * 1000); 41 | return nanosleep(&req , NULL); 42 | } 43 | 44 | 45 | // hue: using 0 to 1/3 => red to green. 46 | double tmp2hue(double tmp, double hi, double lo) 47 | { 48 | double hue = 0; 49 | if (tmp < lo) 50 | return 1.0/3.0; 51 | else if (tmp > hi) 52 | return 0.0; 53 | else 54 | return (hi-tmp)/(hi-lo)/3.0; 55 | } 56 | 57 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 58 | ////https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB 59 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 60 | 61 | double hsv_k(int n, double hue) 62 | { 63 | return fmod(n + hue/60.0, 6); 64 | } 65 | 66 | double hsv_f(int n, double hue,double s, double v) 67 | { 68 | double k = hsv_k(n,hue); 69 | return v - v * s * max( { min( {k, 4-k, 1.0} ), 0.0 } ); 70 | } 71 | 72 | vector hsv2rgb(double h, double s, double v) 73 | { 74 | double hue = h * 360; 75 | int r = int(hsv_f(5,hue, s, v)*255); 76 | int g = int(hsv_f(3,hue, s, v)*255); 77 | int b = int(hsv_f(1,hue, s, v)*255); 78 | vector rgb; 79 | rgb.push_back(r); 80 | rgb.push_back(g); 81 | rgb.push_back(b); 82 | return rgb; 83 | } 84 | 85 | ////////////////////////////////////////////////////////////////////////////////////////// 86 | //https://github.com/pimoroni/fanshim-python/issues/19#issuecomment-517478717 87 | ////////////////////////////////////////////////////////////////////////////////////////// 88 | inline static void write_byte(uint8_t byte) 89 | { 90 | for (int n = 0; n < 8; n++) 91 | { 92 | ln_led_dat.set_value((byte & (1 << (7 - n))) > 0); 93 | ln_led_clk.set_value(HIGH); 94 | // nano_usleep_frac(CLCK_STRETCH); 95 | ln_led_clk.set_value(LOW); 96 | // nano_usleep_frac(CLCK_STRETCH); 97 | } 98 | } 99 | 100 | void set_led(double tmp, int br,int hi, int lo, bool off = false) 101 | { 102 | 103 | int r = 0, g = 0, b = 0; 104 | 105 | if (off) { 106 | r = 0; 107 | g = 0; 108 | b = 190; 109 | } 110 | else if (br != 0) 111 | { 112 | double s, v; 113 | s = 1; 114 | v = br/31.0; 115 | //// hsv: hue from temperature; s set to 1, v set to brightness like the official code https://github.com/pimoroni/fanshim-python/blob/5841386d252a80eeac4155e596d75ef01f86b1cf/examples/automatic.py#L44 116 | 117 | vector rgb = hsv2rgb(tmp2hue(tmp, hi, lo), s, v); 118 | r = rgb.at(0); 119 | g = rgb.at(1); 120 | b = rgb.at(2); 121 | } 122 | 123 | 124 | //start frame 125 | ln_led_dat.set_value(LOW); 126 | for (int i = 0; i < 32; ++i) 127 | { 128 | ln_led_clk.set_value(HIGH); 129 | // nano_usleep_frac(CLCK_STRETCH); 130 | ln_led_clk.set_value(LOW); 131 | // nano_usleep_frac(CLCK_STRETCH); 132 | } 133 | 134 | // A 32 bit LED frame for each LED in the string (<0xE0+brightness> ) 135 | write_byte(0b11100000 | br); // in range of 0..31 for the fanshim 136 | write_byte(b); // b 137 | write_byte(g); // g 138 | write_byte(r); // r 139 | 140 | // An end frame consisting of at least (n/2) bits of 1, where n is the number of LEDs in the string 141 | ln_led_dat.set_value(HIGH); 142 | for (int i = 0; i < 1; ++i) 143 | { 144 | ln_led_clk.set_value(HIGH); 145 | // nano_usleep_frac(CLCK_STRETCH); 146 | ln_led_clk.set_value(LOW); 147 | // nano_usleep_frac(CLCK_STRETCH); 148 | } 149 | 150 | } 151 | 152 | ////////////////////////////////////////////////////////////////////////////////////////// 153 | 154 | 155 | map get_fs_conf() 156 | { 157 | map fs_conf_default { 158 | {"on-threshold", 60}, 159 | {"off-threshold", 50}, 160 | {"budget",3}, 161 | {"delay", 10}, 162 | {"brightness",0}, 163 | {"blink", 0}, 164 | {"breath_brgt",10} 165 | }; 166 | 167 | map fs_conf = fs_conf_default; 168 | 169 | try 170 | { 171 | ifstream fs_cfg_file("/usr/local/etc/fanshim.json"); 172 | json fs_cfg_custom; 173 | fs_cfg_file >> fs_cfg_custom; 174 | 175 | for (auto& el : fs_cfg_custom.items()) { 176 | fs_conf[el.key()] = el.value(); 177 | } 178 | 179 | if ( (fs_conf["on-threshold"] <= fs_conf["off-threshold"]) 180 | || (fs_conf["budget"] <= 0) || (fs_conf["delay"] <= 0) 181 | || (fs_conf["breath_brgt"]<=0) || (fs_conf["breath_brgt"]>31) 182 | || fs_conf["blink"]<0 || fs_conf["blink"]>2 ) 183 | { 184 | throw runtime_error("sanity check"); 185 | } 186 | 187 | } 188 | catch (exception &e) 189 | { 190 | cout<<"error parsing config file: "<::iterator it=fs_conf.begin(); it!=fs_conf.end(); ++it) 195 | cout << it->first << " => " << it->second << endl; 196 | 197 | return fs_conf; 198 | } 199 | 200 | 201 | void blk_led(double tmp, int br, int on_threshold, int off_threshold,int delay) 202 | { 203 | struct timespec ti; 204 | ti.tv_sec = 0; 205 | ti.tv_nsec = 500*1000*1000; 206 | for (int i = 1; i <= delay; i++) 207 | { 208 | // set_led(tmp, ( (i % 2) * br ), on_threshold, off_threshold); 209 | set_led(tmp, br, on_threshold, off_threshold); 210 | nanosleep(&ti, NULL); 211 | set_led(tmp, 0, on_threshold, off_threshold); 212 | nanosleep(&ti, NULL); 213 | } 214 | } 215 | 216 | void breath_led(double tmp, int brth, int on_threshold, int off_threshold, int delay, int* brs) 217 | { 218 | struct timespec req; 219 | req.tv_sec = 0; 220 | req.tv_nsec = (long)(100 * 1000 * 1000); 221 | 222 | for (int i=0; i<10*delay; i++) 223 | { 224 | // cout<= 2*brth -1) 227 | br_counter = 0; 228 | else 229 | br_counter++; 230 | nanosleep(&req , NULL); 231 | } 232 | } 233 | 234 | void signalHandler( int signum ) { 235 | cout << "Signal: " << signum << endl; 236 | if (signum == SIGTERM || signum == SIGINT) 237 | { 238 | set_led(1, 3, 1, 1, true); 239 | cout<<"closed"< fs_conf = get_fs_conf(); 271 | 272 | 273 | const int delay_sec = fs_conf["delay"]; 274 | const int on_threshold = fs_conf["on-threshold"]; 275 | const int off_threshold = fs_conf["off-threshold"]; 276 | const int budget = fs_conf["budget"]; 277 | 278 | const struct timespec sleep_delay{delay_sec,0L}; 279 | 280 | int read_fs_pin = 0; 281 | 282 | const string node_hdr = "# HELP cpu_fanshim text file output: fan state.\n# TYPE cpu_fanshim gauge\ncpu_fanshim "; 283 | const string node_hdr_t = "# HELP cpu_temp_fanshim text file output: temp.\n# TYPE cpu_temp_fanshim gauge\ncpu_temp_fanshim "; 284 | string nodex_out = ""; 285 | 286 | fstream tmp_file; 287 | float tmp = 0; 288 | deque tmp_q (budget, 0.0); 289 | int j; 290 | bool all_low,all_high; 291 | 292 | tmp_file.open("/sys/class/thermal/thermal_zone0/temp", ios_base::in); 293 | 294 | 295 | ///override file 296 | filesystem::path override_fp("/usr/local/etc/.force_fanshim"); 297 | 298 | 299 | ///led 300 | int br = fs_conf["brightness"]; 301 | int brt_br = fs_conf["breath_brgt"]; 302 | int *brs = new int[brt_br*2]; 303 | 304 | for (int i=0;i 31) { 314 | br = 31; 315 | cout<<"brightness exceeds max = 31, set to max"<> tmp; 333 | tmp_file.seekg(0, tmp_file.beg); 334 | tmp = tmp/1000; 335 | tmp_q.push_back(int(tmp)); 336 | tmp_q.pop_front(); 337 | deque (tmp_q).swap(tmp_q); 338 | 339 | cout<<"Temp: "<on_threshold;}); 348 | 349 | cout<<"all low: "<< boolalpha << all_low <<"; "; 350 | cout<<"all high: "<< boolalpha << all_high <