diff --git a/README.md b/README.md index 2a575d2..64aa067 100755 --- a/README.md +++ b/README.md @@ -7,30 +7,38 @@ A simple node.js module for reading temperature and relative humidity using a co [![npm](https://img.shields.io/npm/dm/node-dht-sensor.svg)](https://www.npmjs.com/package/node-dht-sensor) [![LICENSE](https://img.shields.io/github/license/momenso/node-dht-sensor.svg)](https://github.com/momenso/node-dht-sensor/blob/master/LICENSE) -## Installation +## Installation & Hardware Requirements -```shell session -$ npm install node-dht-sensor +For most standard Linux systems and older Raspberry Pi models (Pi 1 through 4), you can install the module directly via npm: + +```sh +npm install node-dht-sensor ``` -### Installing on Raspberry Pi 5 (libgpiod requirement) +### Modern Raspberry Pi (Pi 5 / Debian 12+) — libgpiod + +When running `node-dht-sensor` on a modern architecture like the Raspberry Pi 5, the legacy BCM2835 library is physically incompatible with the new RP1 southbridge chip. You **must** compile the module using `libgpiod`. -When running node-dht-sensor on a Raspberry Pi 5 (or newer), you must install libgpiod (and its development headers) before you build. If you try to install the module with --use_libgpiod=true without having libgpiod-dev installed, the build will fail. +**`libgpiod` v2 is the highly preferred library.** The v2 API allows this module to use an `OPEN_DRAIN` configuration, enabling fast, user-space polling of the sensor. -For Raspberry Pi OS (Debian-based), use: +Before installing the module, you must install the `libgpiod` development headers and `pkg-config`. Our build system uses `pkg-config` to auto-detect whether your OS provides `libgpiod` v1 or v2 at build time and compiles the correct C++ code paths automatically. + +For Debian-based systems (like Raspberry Pi OS): ```sh sudo apt-get update -sudo apt-get install -y libgpiod-dev +sudo apt-get install -y libgpiod-dev pkg-config ``` -After installing libgpiod-dev, build node-dht-sensor with: +Then, explicitly tell npm to use the libgpiod API during installation: ```sh npm install node-dht-sensor --use_libgpiod=true ``` -> Note: Specifying --use_libgpiod=true compiles and links against libgpiod for GPIO access, because the BCM2835 library does not work on Raspberry Pi 5’s architecture. If you omit --use_libgpiod=true, node-dht-sensor defaults to using BCM2835, which is compatible with older Raspberry Pi models. +### Older Raspberry Pi (Pi 1 through Pi 4) — BCM2835 + +If you omit the `--use_libgpiod=true` flag, `node-dht-sensor` defaults to using the direct-memory BCM2835 library. This bypasses the Linux kernel entirely for nanosecond-level read speeds and is highly recommended for older boards where the CPU manages the GPIO pins directly. ## Usage @@ -186,72 +194,63 @@ sensor.read(22, 4, function(err, temperature, humidity) { And the result will always be the configured readout value defined at initialization. -```shell session -$ node examples/fake-test.js +```sh +node examples/fake-test.js temp: 21.0°C, humidity: 60.0% -$ node examples/fake-test.js +node examples/fake-test.js temp: 21.0°C, humidity: 60.0% ``` You can find a complete source code example in [examples/fake-test.js](https://github.com/momenso/node-dht-sensor/blob/master/examples/fake-test.js). -### Reference for building from source +## Manual Compilation & Debugging -Standard node-gyp commands are used to build the module. So, just make sure you have node and node-gyp as well as the Broadcom library to build the project. +Standard node-gyp commands are used to build the module. So, just make sure you have `node` and `node-gyp`, and the appropriate C++ libraries (`bcm2835` or `libgpiod-dev`) to build the project. -1. In case, you don't have node-gyp, install it first: +1. In case you don't have node-gyp, install it first: - ```shell session - $ sudo npm install -g node-gyp - $ sudo update-alternatives --install /usr/bin/node-gyp node-gyp /opt/node-v10.15.3-linux-armv7l/bin/node-gyp 1 + ```sh + sudo npm install -g node-gyp ``` -2. Generate the configuration files +2. To configure and build the component manually with libgpiod auto-detection enabled: - ```shell session - $ node-gyp configure + ```sh + node-gyp configure --use_libgpiod=true + node-gyp build ``` -3. Build the component - ```shell session - $ node-gyp build - ``` - -### Tracing and Debugging +## Tracing and Debugging Verbose output from the module can be enabled by specifying the `--dht_verbose=true` flag when installing the node via npm. -```shell session -$ npm install node-dht-sensor --dht_verbose=true +```sh +npm install node-dht-sensor --dht_verbose=true ``` -if you are interested in enabling trace when building directly from source you can enable the `-Ddht_verbose` flag when running node-gyp configure. +If you are interested in enabling trace when building directly from source you can enable the `-Ddht_verbose` flag when running node-gyp configure. -```shell session -$ node-gyp configure -- -Ddht_verbose=true +```sh +node-gyp configure -- -Ddht_verbose=true ``` -### Appendix A: Quick Node.js installation guide +## Appendix A: Quick Node.js installation guide -There are many ways you can get Node.js installed on your Raspberry Pi. Here is just one way you can do it. +There are many ways you can get Node.js installed on your Raspberry Pi. For modern installations (including Raspberry Pi 5), the officially recommended approach is using the NodeSource package repository. -```shell session -$ wget https://nodejs.org/dist/v14.15.4/node-v14.15.4-linux-armv7l.tar.xz -$ tar xvfJ node-v14.15.4-linux-armv7l.tar.xz -$ sudo mv node-v14.15.4-linux-armv7l /opt -$ sudo update-alternatives --install /usr/bin/node node /opt/node-v14.15.4-linux-armv7l/bin/node 1 -$ sudo update-alternatives --set node /opt/node-v14.15.4-linux-armv7l/bin/node -$ sudo update-alternatives --install /usr/bin/npm npm /opt/node-v14.15.4-linux-armv7l/bin/npm 1 +```sh +curl -fsSL [https://deb.nodesource.com/setup_20.x](https://deb.nodesource.com/setup_20.x) | sudo -E bash - +sudo apt-get install -y nodejs ``` -Please note that you may have to use armv6l instead of arm7l if you have an early Raspberry Pi model. +_(This automatically installs the correct architecture build (e.g., `arm64`/`aarch64` for Pi 5 or `armhf` for older models) and includes npm)._ -### References +## References [1]: Node.js download - https://nodejs.org/en/download/ [2]: BCM2835 - http://www.airspayce.com/mikem/bcm2835/ -[3]: Node.js native addon build tool - https://github.com/TooTallNate/node-gyp +[3]: libgpiod - https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/ -[4]: GPIO: Raspbery Pi Models A and B - https://www.raspberrypi.org/documentation/usage/gpio/ +[4]: Node.js native addon build tool - https://github.com/TooTallNate/node-gyp diff --git a/binding.gyp b/binding.gyp index f666188..25fae36 100755 --- a/binding.gyp +++ b/binding.gyp @@ -1,35 +1,55 @@ { + "variables": { + "use_libgpiod%": "false", + "dht_verbose%": "false", + "libgpiod_version": "= 2 ? '2' : '1'); } catch(e) { console.log('1'); }\")" + }, "targets": [ { - "variables": { - "dht_verbose%": "false", - "use_libgpiod%" : "false" - }, "target_name": "node_dht_sensor", "sources": [ "src/bcm2835/bcm2835.c", - "src/node-dht-sensor.cpp", "src/dht-sensor.cpp", + "src/node-dht-sensor.cpp", "src/util.cpp", - "src/abstract-gpio.cpp", - ], - "include_dirs": [ - " -#include "bcm2835/bcm2835.h" #include #include #include #include #ifdef USE_LIBGPIOD +#include #include +#else +#include "bcm2835/bcm2835.h" #endif -static bool useGpiod = false; -static GpioDirection lastDirection[MAX_GPIO_PIN_NUMBER + 1]; #ifdef USE_LIBGPIOD static gpiod_chip *theChip = NULL; +static GpioDirection lastDirection[MAX_GPIO_PIN_NUMBER + 1]; + +#ifdef GPIOD_V2 +static gpiod_line_request *requests[MAX_GPIO_PIN_NUMBER + 1]; +#else static gpiod_line *lines[MAX_GPIO_PIN_NUMBER + 1]; #endif static void gpiodCleanUp() { - #ifdef USE_LIBGPIOD for (int i = 1; i <= MAX_GPIO_PIN_NUMBER; ++i) { + #ifdef GPIOD_V2 + if (requests[i] != NULL) + { + gpiod_line_request_release(requests[i]); + } + #else if (lines[i] != NULL) { gpiod_line_release(lines[i]); } + #endif } - gpiod_chip_close(theChip); - #endif + if (theChip != NULL) + { + gpiod_chip_close(theChip); + } } -bool gpioInitialize() +static gpiod_chip* findGpioChip() { - bool isPi5 = false; - std::ifstream file("/proc/cpuinfo"); - std::string line; + gpiod_chip* chip = NULL; - if (file.is_open()) + // 1. Safely enumerate up to 10 chips looking for the RP1 southbridge (Pi 5) + for (int i = 0; i < 10; i++) { - std::regex pattern(R"(Model\s+:\s+Raspberry Pi (\d+))"); - std::smatch match; + char path[32]; + snprintf(path, sizeof(path), "/dev/gpiochip%d", i); - while (std::getline(file, line)) + #ifdef GPIOD_V2 + chip = gpiod_chip_open(path); + if (chip != NULL) { - if (std::regex_search(line, match, pattern) && std::stoi(match[1]) > 4) + gpiod_chip_info* info = gpiod_chip_get_info(chip); + if (info != NULL) { - #ifdef USE_LIBGPIOD - isPi5 = true; - #endif - break; + const char* label = gpiod_chip_info_get_label(info); + bool is_rp1 = (label != NULL && strstr(label, "pinctrl-rp1") != NULL); + gpiod_chip_info_free(info); + + if (is_rp1) return chip; } + gpiod_chip_close(chip); } - - file.close(); + #else + chip = gpiod_chip_open(path); + if (chip != NULL) + { + const char* label = gpiod_chip_label(chip); + if (label != NULL && strstr(label, "pinctrl-rp1") != NULL) + { + return chip; + } + gpiod_chip_close(chip); + } + #endif } - if (isPi5) + // 2. Fallback for older Pi's using libgpiod or default setups + #ifdef GPIOD_V2 + chip = gpiod_chip_open("/dev/gpiochip4"); + if (chip == NULL) chip = gpiod_chip_open("/dev/gpiochip0"); + #else + chip = gpiod_chip_open_by_name("gpiochip4"); + if (chip == NULL) chip = gpiod_chip_open_by_name("gpiochip0"); + #endif + + return chip; +} + +#ifdef GPIOD_V2 +static gpiod_line_request* getLineRequest(int pin) +{ + if (requests[pin] == NULL) { - #ifdef USE_LIBGPIOD - theChip = gpiod_chip_open_by_name("gpiochip0"); + gpiod_line_settings *settings = gpiod_line_settings_new(); + gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT); + gpiod_line_settings_set_drive(settings, GPIOD_LINE_DRIVE_OPEN_DRAIN); + gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE); - if (theChip == NULL) - { - #ifdef VERBOSE - puts("libgpiod initialization failed."); - #endif - return false; - } - else + gpiod_line_config *line_cfg = gpiod_line_config_new(); + unsigned int offset = pin; + gpiod_line_config_add_line_settings(line_cfg, &offset, 1, settings); + + gpiod_request_config *req_cfg = gpiod_request_config_new(); + gpiod_request_config_set_consumer(req_cfg, "node-dht-sensor"); + + requests[pin] = gpiod_chip_request_lines(theChip, req_cfg, line_cfg); + + gpiod_request_config_free(req_cfg); + gpiod_line_config_free(line_cfg); + gpiod_line_settings_free(settings); + + if (requests[pin] != NULL) { - #ifdef VERBOSE - puts("libgpiod initialized."); - #endif - std::fill(lines, lines + MAX_GPIO_PIN_NUMBER + 1, (gpiod_line*) NULL); - std::fill(lastDirection, lastDirection + MAX_GPIO_PIN_NUMBER + 1, GPIO_UNSET); - useGpiod = true; - std::atexit(gpiodCleanUp); - return true; + lastDirection[pin] = GPIO_OUT; } + } + + return requests[pin]; +} +#else +static gpiod_line* getLine(int pin, GpioDirection direction) +{ + if (lines[pin] != NULL && lastDirection[pin] != direction) + { + gpiod_line_release(lines[pin]); + lines[pin] = NULL; + } + + if (lines[pin] == NULL) + { + lines[pin] = gpiod_chip_get_line(theChip, pin); + if (lines[pin] == NULL) return NULL; + + if (direction == GPIO_IN) { gpiod_line_request_input(lines[pin], "node-dht-sensor"); } + else { gpiod_line_request_output(lines[pin], "node-dht-sensor", 0); } + lastDirection[pin] = direction; + } + return lines[pin]; +} +#endif // GPIOD_V2 + +// --- bcm2835 State --- +#else +static GpioDirection lastDirection[MAX_GPIO_PIN_NUMBER + 1]; +#endif // USE_LIBGPIOD + +// --- Public API --- + +bool gpioInitialize() +{ +#ifdef USE_LIBGPIOD + theChip = findGpioChip(); + + if (theChip == NULL) + { + #ifdef VERBOSE + puts("libgpiod initialization failed: Could not find RP1 or fallback chips."); #endif + return false; } - else if (!bcm2835_init()) + else + { + #ifdef VERBOSE + puts("libgpiod initialized."); + #endif + + #ifdef GPIOD_V2 + std::fill(requests, requests + MAX_GPIO_PIN_NUMBER + 1, (gpiod_line_request*) NULL); + #else + std::fill(lines, lines + MAX_GPIO_PIN_NUMBER + 1, (gpiod_line*) NULL); + #endif + + std::fill(lastDirection, lastDirection + MAX_GPIO_PIN_NUMBER + 1, GPIO_UNSET); + std::atexit(gpiodCleanUp); + return true; + } +#else + if (!bcm2835_init()) { #ifdef VERBOSE puts("BCM2835 initialization failed."); @@ -95,80 +198,77 @@ bool gpioInitialize() #ifdef VERBOSE puts("BCM2835 initialized."); #endif + std::fill(lastDirection, lastDirection + MAX_GPIO_PIN_NUMBER + 1, GPIO_UNSET); return true; } +#endif } -#ifdef USE_LIBGPIOD -static gpiod_line* getLine(int pin, GpioDirection direction) +void gpioWrite(int pin, GpioPinState state) { - if (lines[pin] == NULL || lastDirection[pin] != direction) +#ifdef USE_LIBGPIOD + #ifdef GPIOD_V2 + gpiod_line_request *req = getLineRequest(pin); + if (req != NULL) { - if (lines[pin] != NULL) - { - gpiod_line_release(lines[pin]); - lines[pin] = NULL; - } - - lines[pin] = gpiod_chip_get_line(theChip, pin); + gpiod_line_request_set_value(req, pin, state == GPIO_LOW ? GPIOD_LINE_VALUE_INACTIVE : GPIOD_LINE_VALUE_ACTIVE); + lastDirection[pin] = GPIO_OUT; } - - gpiod_line* line = lines[pin]; - - if (lastDirection[pin] != direction) + #else + gpiod_line *line = getLine(pin, GPIO_OUT); + if (line != NULL) { - if (direction == GPIO_IN) - { - gpiod_line_request_input(line, "Consumer"); - } - else - { - gpiod_line_request_output(line, "Consumer", 0); - } - - lastDirection[pin] = direction; + gpiod_line_set_value(line, state == GPIO_LOW ? 0 : 1); } - - return line; -} + #endif +#else + if (lastDirection[pin] != GPIO_OUT) + { + bcm2835_gpio_fsel(pin, BCM2835_GPIO_FSEL_OUTP); + lastDirection[pin] = GPIO_OUT; + } + bcm2835_gpio_write(pin, state == GPIO_LOW ? LOW : HIGH); #endif +} -void gpioWrite(int pin, GpioPinState state) +GpioPinState gpioRead(int pin) { - if (useGpiod) +#ifdef USE_LIBGPIOD + #ifdef GPIOD_V2 + gpiod_line_request *req = getLineRequest(pin); + if (req == NULL) return GPIO_HIGH; + + if (lastDirection[pin] == GPIO_OUT) { - #ifdef USE_LIBGPIOD - gpiod_line_set_value(getLine(pin, GPIO_OUT), state == GPIO_LOW ? 0 : 1); - #endif + gpiod_line_request_set_value(req, pin, GPIOD_LINE_VALUE_ACTIVE); + lastDirection[pin] = GPIO_IN; } - else - { - if (lastDirection[pin] != GPIO_OUT) - { - bcm2835_gpio_fsel(pin, BCM2835_GPIO_FSEL_OUTP); - lastDirection[pin] = GPIO_OUT; - } - bcm2835_gpio_write(pin, state == GPIO_LOW ? LOW : HIGH); + int val = gpiod_line_request_get_value(req, pin); + + if (val == -1) + { + return GPIO_HIGH; } -} + return val == GPIOD_LINE_VALUE_INACTIVE ? GPIO_LOW : GPIO_HIGH; -GpioPinState gpioRead(int pin) -{ - if (useGpiod) + #else + gpiod_line *line = getLine(pin, GPIO_IN); + if (line == NULL) return GPIO_HIGH; + + int val = gpiod_line_get_value(line); + if (val == -1) { - #ifdef USE_LIBGPIOD - return gpiod_line_get_value(getLine(pin, GPIO_IN)) == 0 ? GPIO_LOW : GPIO_HIGH; - #endif + return GPIO_HIGH; } - else + return val == 0 ? GPIO_LOW : GPIO_HIGH; + #endif +#else + if (lastDirection[pin] != GPIO_IN) { - if (lastDirection[pin] != GPIO_IN) - { - bcm2835_gpio_fsel(pin, BCM2835_GPIO_FSEL_INPT); - lastDirection[pin] = GPIO_IN; - } - - return bcm2835_gpio_lev(pin) == LOW ? GPIO_LOW : GPIO_HIGH; + bcm2835_gpio_fsel(pin, BCM2835_GPIO_FSEL_INPT); + lastDirection[pin] = GPIO_IN; } + return bcm2835_gpio_lev(pin) == LOW ? GPIO_LOW : GPIO_HIGH; +#endif }