diff --git a/.github/workflows/AutoCreateRelease.yml b/.github/workflows/AutoCreateRelease.yml new file mode 100644 index 0000000000..40ba81e891 --- /dev/null +++ b/.github/workflows/AutoCreateRelease.yml @@ -0,0 +1,81 @@ +name: Create Release + +on: + push: + tags: + - '3.*-release_*' + +jobs: + + build_luac_cross_win: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Build luac.cross.exe + run: | + set + "%programfiles%\git\usr\bin\xargs" + cd msvc + "%programfiles(x86)%\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" /p:Configuration=Release /p:Platform=x64 + mv luac-cross/x64/Release/luac.cross.exe .. + shell: cmd + - name: Upload luac.cross + if: ${{ success() }} + uses: actions/upload-artifact@v2 + with: + name: luac.cross_51_float_win + path: luac.cross.exe + + + Create_Release: + name: Create Release + needs: build_luac_cross_win + runs-on: ubuntu-latest + + steps: + - name: Set release name + run: | + echo "RELEASE_NAME=${GITHUB_REF/*\/}" >> $GITHUB_ENV + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{env.RELEASE_NAME }} + release_name: ${{env.RELEASE_NAME }} + body: | + Please note that as per #3164 this project switched the default branch from `master` to `release` with the previous release. For the time being both are kept in sync as to ease the transition for our community. However, expect `master` to disappear sooner or later. + + ## Breaking Changes + - Description - # + + ## New Modules + - [wiegand](https://nodemcu.readthedocs.io/en/latest/modules/wiegand/) C module - #3203 + + ## Bug Fixes + Please see [the release milestone](https://github.com/nodemcu/nodemcu-firmware/milestone/16?closed=1) for details. + + ## Deprecation + + prerelease: false + draft: true + - name: Download luac.cross + uses: actions/download-artifact@v1 + with: + name: luac.cross_51_float_win + path: ./ + - name: upload luac.cross to release + id: upload-luac-cross + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./luac.cross.exe + asset_name: luac.cross_${{env.RELEASE_NAME }}_x64_float_Lua51.exe + asset_content_type: application/x-msdownload diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..b0dcee5a32 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,272 @@ +name: CI + +on: + push: + branches: [ release ] + pull_request: + branches: [ dev, release ] + +jobs: + + + build: + strategy: + fail-fast: false + matrix: + lua_ver: [51, 53] + numbers: ['float'] + include: + - lua_ver: 51 + numbers: 'integral' + - lua_ver: 53 + numbers: '64bit' + runs-on: ubuntu-16.04 + + env: + LUA: ${{ matrix.lua_ver }} + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - run: pip install pyserial + shell: bash + - run: sudo apt install srecord + shell: bash + - name: Build firmware + if: matrix.numbers == 'float' + run: make + shell: bash + - name: Build integral firmware + if: ${{ matrix.numbers == 'integral' }} + run: | + make EXTRA_CCFLAGS="-DLUA_NUMBER_INTEGRAL" + mv luac.cross.int luac.cross + shell: bash + - name: Build 64bit firmware + if: ${{ matrix.numbers == '64bit' }} + run: | + make EXTRA_CCFLAGS="-DLUA_NUMBER_64BITS" + shell: bash + - name: Upload luac.cross + if: ${{ success() }} + uses: actions/upload-artifact@v2 + with: + name: luac.cross_${{ matrix.lua_ver }}_${{ matrix.numbers }} + path: luac.cross + + + build_luac_cross_win: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Build luac.cross.exe + run: | + set + "%programfiles%\git\usr\bin\xargs" + cd msvc + "%programfiles(x86)%\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" /p:Configuration=Release /p:Platform=x64 + mv luac-cross/x64/Release/luac.cross.exe .. + shell: cmd + - name: Upload luac.cross + if: ${{ success() }} + uses: actions/upload-artifact@v2 + with: + name: luac.cross_51_float_win + path: luac.cross.exe + + + compile_lua: + + strategy: + fail-fast: false + matrix: + lua_ver: [51, 53] + numbers: ['float'] + filter: [ 'cat' ] + include: + - lua_ver: 51 + numbers: 'integral' + filter: 'grep -v "lua_modules/lm92/lm92.lua\|lua_modules/hdc1000/HDC1000.lua\|lua_examples/u8g2/graphics_test.lua"' + - lua_ver: 53 + numbers: '64bit' + filter: 'cat' + needs: build + runs-on: ubuntu-16.04 + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + with: + submodules: false + - name: Download luac.cross + uses: actions/download-artifact@v1 + with: + name: luac.cross_${{ matrix.lua_ver }}_${{ matrix.numbers }} + path: ./ + - name: Fix file permission + run: chmod +x luac.cross + - name: compile Lua + run: | + find lua_modules lua_examples tests/NTest* -iname "*.lua" | ${{ matrix.filter }} | xargs --delimiter="\n" echo + find lua_modules lua_examples tests/NTest* -iname "*.lua" | ${{ matrix.filter }} | xargs --delimiter="\n" ./luac.cross -p + shell: bash + + + compile_lua_win: + + strategy: + fail-fast: false + matrix: + lua_ver: [51] + numbers: ['float'] + filter: [ 'cat' ] + needs: build_luac_cross_win + runs-on: windows-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + with: + submodules: false + - name: Download luac.cross + uses: actions/download-artifact@v1 + with: + name: luac.cross_${{ matrix.lua_ver }}_${{ matrix.numbers }}_win + path: ./ + - name: compile Lua + run: | + PATH="/C/Program\ Files/Git/usr/bin:${PATH}" + find lua_modules lua_examples tests/NTest* -iname "*.lua" | ${{ matrix.filter }} | xargs --delimiter="\n" echo + find lua_modules lua_examples tests/NTest* -iname "*.lua" | ${{ matrix.filter }} | xargs --delimiter="\n" ./luac.cross -p + shell: bash + + + NTest: + + strategy: + fail-fast: false + matrix: + lua_ver: [51, 53] + numbers: ['float'] + include: + - lua_ver: 51 + numbers: 'integral' + - lua_ver: 53 + numbers: '64bit' + needs: build + runs-on: ubuntu-16.04 + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + with: + submodules: false + - name: Download luac.cross + uses: actions/download-artifact@v1 + with: + name: luac.cross_${{ matrix.lua_ver }}_${{ matrix.numbers }} + path: ./ + - name: Fix file permission + run: chmod +x luac.cross + - name: NTest selfcheck + run: | + cd tests/NTest + ../../luac.cross -e ../NTest/NTest_NTest.lua | tee log + grep "failed. 0" log + shell: bash + - name: NTest hosttests + run: | + cd tests + cp NTest/NTest.lua . + ../luac.cross -e NTest_lua.lua | tee log + (if grep " ==> " log ; then exit 1 ; fi) + shell: bash + + + NTest_win: + + strategy: + fail-fast: false + matrix: + lua_ver: [51] + numbers: ['float'] + needs: build_luac_cross_win + runs-on: windows-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + with: + submodules: false + - name: Download luac.cross + uses: actions/download-artifact@v1 + with: + name: luac.cross_${{ matrix.lua_ver }}_${{ matrix.numbers }}_win + path: ./ + - name: NTest selfcheck + run: | + cd tests/NTest + ../../luac.cross.exe -e ../NTest/NTest_NTest.lua | tee log + grep "failed. 0" log + shell: bash + - name: NTest hosttests + run: | + cd tests + cp NTest/NTest.lua . + ../luac.cross.exe -e NTest_lua.lua | tee log + (if grep " ==> " log ; then exit 1 ; fi) + shell: bash + + + luacheck: + + strategy: + fail-fast: false + matrix: + include: + - os: 'linux' + vm: 'ubuntu-16.04' + - os: 'windows' + vm: 'windows-latest' + runs-on: ${{ matrix.vm }} + + steps: + - uses: actions/checkout@v2 + with: + submodules: false + - run: sudo apt install luarocks + if : matrix.os == 'linux' + shell: bash + - name: get luacheck.exe # is also done in the travis script but in this action it does not run in bash + if : matrix.os == 'windows' + run: | + mkdir cache + C:msys64\usr\bin\wget.exe --tries=5 --timeout=10 --waitretry=10 --read-timeout=10 --retry-connrefused -O cache/luacheck.exe https://github.com/mpeterv/luacheck/releases/download/0.23.0/luacheck.exe + shell: cmd + - name: luacheck + run: | + PATH="/C/Program\ Files/Git/usr/bin:${PATH}" + ./tools/travis/run-luacheck-${{ matrix.os }}.sh + shell: bash + + + + doc_check: + + strategy: + fail-fast: false + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v2 + with: + submodules: false + - name: all_modules_linked + run: ./tools/check_docs_module_linkage.sh + shell: bash + diff --git a/.travis.yml b/.travis.yml index 6a4f2ae70c..54ae80e62b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,10 @@ script: - if [ "$OS" = "linux" -a "$TRAVIS_PULL_REQUEST" != "false" ]; then bash "$TRAVIS_BUILD_DIR"/tools/travis/pr-build.sh; fi - cd "$TRAVIS_BUILD_DIR" - echo "checking:" -- find lua_modules lua_examples -iname "*.lua" -print0 | xargs -0 echo -- find lua_modules lua_examples -iname "*.lua" -print0 | xargs -0 $LUACC -p +- find lua_modules lua_examples tests/NTest* -iname "*.lua" -print0 | xargs -0 echo +- find lua_modules lua_examples tests/NTest* -iname "*.lua" -print0 | xargs -0 $LUACC -p +- cd tests/NTest +- if [ "$OS" = "linux" ]; then ../../$LUACC -e ../NTest/NTest_NTest.lua; fi +- cd "$TRAVIS_BUILD_DIR" - if [ "$OS" = "linux" ]; then bash "$TRAVIS_BUILD_DIR"/tools/travis/run-luacheck-linux.sh; fi - if [ "$OS" = "windows" ]; then bash "$TRAVIS_BUILD_DIR"/tools/travis/run-luacheck-windows.sh; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2dba89b508..5ad6a2774f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,7 +78,7 @@ As a Windows or Mac user you could also resort to [GitHub Desktop](https://deskt You need to sync your fork with the NodeMCU upstream repository from time to time, latest before you rebase (see flow above). 1. `git fetch upstream` -1. `git checkout dev` but you may do this for `master` as well +1. `git checkout dev` but you may do this for `release` as well 1. `git merge upstream/dev` ### Commit messages @@ -114,8 +114,8 @@ Don't forget to [reference affected issues](https://help.github.com/articles/clo - Add notes to the description of the milestone in the course of the ~2 months it lives. - Be careful and reluctant to merge PRs once we're past the 6-weeks mark of a milestone. Ideally, we don't merge anything in the last 2 weeks. - Cutting a release - - Create a PR for the `master` branch for collaborators to approve. + - Create a PR for the `release` branch for collaborators to approve. - Once approved merge it. :exclamation::boom::exclamation: Make sure you do NOT "squash and merge" but make a regular merge commit! - - Fetch the changes into your local clone and create an annotated tag like so: `git tag -a -master_ -m ""`, `git push --tags` + - Fetch the changes into your local clone and create an annotated tag like so: `git tag -a -release_ -m ""`, `git push --tags` - Create a new [release](https://github.com/nodemcu/nodemcu-firmware/releases) based on the tag you just pushed. The version name is the same as the tag name. - Write release notes. Mention breaking changes explicitly. Since every PR that went into this release is linked to from the milestone it should be fairly easy to include important changes in the release notes. diff --git a/README.md b/README.md index 243b668e20..c8b74ff73a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![Join the chat at https://gitter.im/nodemcu/nodemcu-firmware](https://img.shields.io/gitter/room/badges/shields.svg)](https://gitter.im/nodemcu/nodemcu-firmware?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/nodemcu/nodemcu-firmware.svg)](https://travis-ci.org/nodemcu/nodemcu-firmware) -[![Documentation Status](https://img.shields.io/badge/docs-master-yellow.svg?style=flat)](http://nodemcu.readthedocs.io/en/master/) -[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/nodemcu/nodemcu-firmware/blob/master/LICENSE) +[![Documentation Status](https://img.shields.io/badge/docs-release-yellow.svg?style=flat)](http://nodemcu.readthedocs.io/en/release/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/nodemcu/nodemcu-firmware/blob/release/LICENSE) ### A Lua based firmware for ESP8266 WiFi SOC @@ -14,9 +14,9 @@ The firmware was initially developed as is a companion project to the popular ES # Summary - Easy to program wireless node and/or access point -- Based on Lua 5.1.4 but without `debug`, `io`, `os` and (most of the) `math` modules +- Based on Lua 5.1.4 or Lua 5.3 but without `debug`, `io`, `os` and (most of the) `math` modules - Asynchronous event-driven programming model -- more than **65 built-in modules** +- more than **70 built-in C modules** and **close to 20 Lua modules** - Firmware available with or without floating point support (integer-only uses less memory) - Up-to-date documentation at [https://nodemcu.readthedocs.io](https://nodemcu.readthedocs.io) @@ -48,23 +48,23 @@ wifi.sta.config{ssid="SSID", pwd="password"} The entire [NodeMCU documentation](https://nodemcu.readthedocs.io) is maintained right in this repository at [/docs](docs). The fact that the API documentation is maintained in the same repository as the code that *provides* the API ensures consistency between the two. With every commit the documentation is rebuilt by Read the Docs and thus transformed from terse Markdown into a nicely browsable HTML site at [https://nodemcu.readthedocs.io](https://nodemcu.readthedocs.io). -- How to [build the firmware](https://nodemcu.readthedocs.io/en/master/en/build/) -- How to [flash the firmware](https://nodemcu.readthedocs.io/en/master/en/flash/) -- How to [upload code and NodeMCU IDEs](https://nodemcu.readthedocs.io/en/master/en/upload/) +- How to [build the firmware](https://nodemcu.readthedocs.io/en/release/build/) +- How to [flash the firmware](https://nodemcu.readthedocs.io/en/release/flash/) +- How to [upload code and NodeMCU IDEs](https://nodemcu.readthedocs.io/en/release/upload/) - API documentation for every module # Releases -Due to the ever-growing number of modules available within NodeMCU, pre-built binaries are no longer made available. Use the automated [custom firmware build service](http://nodemcu-build.com/) to get the specific firmware configuration you need, or consult the [documentation](http://nodemcu.readthedocs.io/en/master/en/build/) for other options to build your own firmware. +Due to the ever-growing number of modules available within NodeMCU, pre-built binaries are no longer made available. Use the automated [custom firmware build service](http://nodemcu-build.com/) to get the specific firmware configuration you need, or consult the [documentation](http://nodemcu.readthedocs.io/en/release/build/) for other options to build your own firmware. This project uses two main branches, `release` and `dev`. `dev` is actively worked on and it's also where PRs should be created against. `release` thus can be considered "stable" even though there are no automated regression tests. The goal is to merge back to `release` roughly every 2 months. Depending on the current "heat" (issues, PRs) we accept changes to `dev` for 5-6 weeks and then hold back for 2-3 weeks before the next snap is completed. -A new tag is created every time `dev` is merged back to `master`. They are listed in the [releases section here on GitHub](https://github.com/nodemcu/nodemcu-firmware/releases). Tag names follow the \-master_yyyymmdd pattern. +A new tag is created every time `dev` is merged back to `release`. They are listed in the [releases section here on GitHub](https://github.com/nodemcu/nodemcu-firmware/releases). Tag names follow the \-release_yyyymmdd pattern. # Support -See [https://nodemcu.readthedocs.io/en/master/en/support/](https://nodemcu.readthedocs.io/en/master/en/support/). +See [https://nodemcu.readthedocs.io/en/release/support/](https://nodemcu.readthedocs.io/en/release/support/). # License -[MIT](https://github.com/nodemcu/nodemcu-firmware/blob/master/LICENSE) © [zeroday](https://github.com/NodeMCU)/[nodemcu.com](http://nodemcu.com/index_en.html) +[MIT](https://github.com/nodemcu/nodemcu-firmware/blob/release/LICENSE) © [zeroday](https://github.com/NodeMCU)/[nodemcu.com](http://nodemcu.com/index_en.html) diff --git a/app/Makefile b/app/Makefile index 7bd19b950b..e85f1427c0 100644 --- a/app/Makefile +++ b/app/Makefile @@ -16,10 +16,15 @@ TARGET = eagle FLAVOR = debug # Handle Lua Directory selector -ifeq ("$(LUA)","53") +ifeq ("$(LUA)","") + LUA_DIR := lua +else ifeq ("$(LUA)","51") + LUA_DIR := lua +else ifeq ("$(LUA)","53") LUA_DIR := lua53 else - LUA_DIR := lua + $(error Unsupported value "$(LUA)" for variable "LUA", \ + expected "51", "53" or unset/empty) endif ifndef PDIR # { @@ -158,7 +163,7 @@ DDEFINES += \ # Required for each makefile to inherit from the parent # INCLUDES := -I $(PDIR)libc -I $(PDIR)$(LUA_DIR) -I $(PDIR)platform \ - $(INCLUDES) -I $(PDIR) -I $(PDIR)include + $(INCLUDES) -I $(PDIR) -I $(PDIR)include PDIR := ../$(PDIR) sinclude $(PDIR)Makefile diff --git a/app/dht/dht.c b/app/dht/dht.c index 0b804138b2..317b339d96 100644 --- a/app/dht/dht.c +++ b/app/dht/dht.c @@ -26,6 +26,7 @@ // // Released to the public domain // +// #define NODE_DEBUG #include "user_interface.h" #include "platform.h" @@ -40,21 +41,14 @@ #define HIGH 1 #endif /* ifndef HIGH */ +#define COMBINE_HIGH_AND_LOW_BYTE(byte_high, byte_low) ((uint16_t)((byte_high) << 8) | (byte_low)) + static double dht_humidity; static double dht_temperature; static uint8_t dht_bytes[5]; // buffer to receive data -typedef enum { - Humidity = 0, - Temperature, - Humidity8, - Temperature8 -} dht_Signal; - static int dht_readSensor(uint8_t pin, uint8_t wakeupDelay); -static double getValue(dht_Signal s); -static bool verifyChecksum(); ///////////////////////////////////////////////////// // @@ -79,10 +73,15 @@ double dht_getTemperature(void) // DHTLIB_OK // DHTLIB_ERROR_CHECKSUM // DHTLIB_ERROR_TIMEOUT -int dht_read_universal(uint8_t pin) +int dht_read(uint8_t pin, dht_type type) { // READ VALUES - int rv = dht_readSensor(pin, DHTLIB_DHT_UNI_WAKEUP); + int rv = dht_readSensor(pin, + type == DHT22 ? DHTLIB_DHT_WAKEUP : + type == DHT11 ? DHTLIB_DHT11_WAKEUP : + DHTLIB_DHT_UNI_WAKEUP + ); + if (rv != DHTLIB_OK) { dht_humidity = DHTLIB_INVALID_VALUE; // invalid value, or is NaN prefered? @@ -90,129 +89,73 @@ int dht_read_universal(uint8_t pin) return rv; // propagate error value } -#if defined(DHT_DEBUG_BYTES) - int i; - for (i = 0; i < 5; i++) - { - DHT_DEBUG("%02X\n", dht_bytes[i]); - } -#endif // defined(DHT_DEBUG_BYTES) + NODE_DBG("DHT registers: %x\t%x\t%x\t%x\t%x == %x\n", dht_bytes[0], dht_bytes[1], dht_bytes[2], dht_bytes[3], dht_bytes[4], (uint8_t)(dht_bytes[0] + dht_bytes[1] + dht_bytes[2] + dht_bytes[3])); - // Assume it is DHT11 + // Assume it is special case of DHT11, + // i.e. positive temperature and dht_bytes[3] == 0 ((dht_bytes[3] & 0x0f) * 0.1 to be added to temperature readout) // If it is DHT11, both temp and humidity's decimal + dht_humidity = dht_bytes[0]; + dht_temperature = dht_bytes[2]; if ((dht_bytes[1] == 0) && (dht_bytes[3] == 0)) { // It may DHT11 // CONVERT AND STORE - DHT_DEBUG("DHT11 method\n"); - dht_humidity = getValue(Humidity8); - dht_temperature = getValue(Temperature8); + NODE_DBG("DHT11 method\n"); // TEST CHECKSUM - if (!verifyChecksum()) - { - // It may not DHT11 - dht_humidity = DHTLIB_INVALID_VALUE; // invalid value, or is NaN prefered? - dht_temperature = DHTLIB_INVALID_VALUE; // invalid value - // Do nothing - } - else + uint8_t sum = dht_bytes[0] + dht_bytes[2]; + if (dht_bytes[4] == sum) { return DHTLIB_OK; } } - // Assume it is not DHT11 + // Assume it is not DHT11 special case // CONVERT AND STORE - DHT_DEBUG("DHTxx method\n"); - dht_humidity = getValue(Humidity); - dht_temperature = getValue(Temperature); - - // TEST CHECKSUM - if (!verifyChecksum()) - { - return DHTLIB_ERROR_CHECKSUM; + NODE_DBG("DHTxx method\n"); + + switch (type) { + case DHT11: + case DHT12: + dht_humidity += dht_bytes[1] * 0.1; + break; + default: + dht_humidity = COMBINE_HIGH_AND_LOW_BYTE(dht_bytes[0], dht_bytes[1]) * 0.1; + break; } - return DHTLIB_OK; -} -// return values: -// DHTLIB_OK -// DHTLIB_ERROR_CHECKSUM -// DHTLIB_ERROR_TIMEOUT -int dht_read11(uint8_t pin) -{ - // READ VALUES - int rv = dht_readSensor(pin, DHTLIB_DHT11_WAKEUP); - if (rv != DHTLIB_OK) - { - dht_humidity = DHTLIB_INVALID_VALUE; // invalid value, or is NaN prefered? - dht_temperature = DHTLIB_INVALID_VALUE; // invalid value - return rv; - } - - // CONVERT AND STORE - dht_humidity = getValue(Humidity8); - dht_temperature = getValue(Temperature8); - - // TEST CHECKSUM - if (!verifyChecksum()) return DHTLIB_ERROR_CHECKSUM; - - return DHTLIB_OK; -} - - -// return values: -// DHTLIB_OK -// DHTLIB_ERROR_CHECKSUM -// DHTLIB_ERROR_TIMEOUT -int dht_read(uint8_t pin) -{ - // READ VALUES - int rv = dht_readSensor(pin, DHTLIB_DHT_WAKEUP); - if (rv != DHTLIB_OK) - { - dht_humidity = DHTLIB_INVALID_VALUE; // invalid value, or is NaN prefered? - dht_temperature = DHTLIB_INVALID_VALUE; // invalid value - return rv; // propagate error value + switch (type) { + case DHT11: + if (dht_bytes[3] & 0x80) { + dht_temperature = -1 - dht_temperature; + } + dht_temperature += (dht_bytes[3] & 0x0f) * 0.1; + break; + case DHT12: + dht_temperature += (dht_bytes[3] & 0x0f) * 0.1; + if (dht_bytes[2] & 0x80) // negative dht_temperature + { + dht_temperature *= -1; + } + break; + default: // DHT22, DHT_NON11 + dht_temperature = COMBINE_HIGH_AND_LOW_BYTE(dht_bytes[2] & 0x7F, dht_bytes[3]) * 0.1; + if (dht_bytes[2] & 0x80) // negative dht_temperature + { + dht_temperature *= -1; + } + break; } - // CONVERT AND STORE - dht_humidity = getValue(Humidity); - dht_temperature = getValue(Temperature); - // TEST CHECKSUM - if (!verifyChecksum()) + uint8_t sum = dht_bytes[0] + dht_bytes[1] + dht_bytes[2] + dht_bytes[3]; + if (dht_bytes[4] != sum) { return DHTLIB_ERROR_CHECKSUM; } return DHTLIB_OK; } -// return values: -// DHTLIB_OK -// DHTLIB_ERROR_CHECKSUM -// DHTLIB_ERROR_TIMEOUT -int dht_read21(uint8_t pin) __attribute__((alias("dht_read"))); - -// return values: -// DHTLIB_OK -// DHTLIB_ERROR_CHECKSUM -// DHTLIB_ERROR_TIMEOUT -int dht_read22(uint8_t pin) __attribute__((alias("dht_read"))); - -// return values: -// DHTLIB_OK -// DHTLIB_ERROR_CHECKSUM -// DHTLIB_ERROR_TIMEOUT -int dht_read33(uint8_t pin) __attribute__((alias("dht_read"))); - -// return values: -// DHTLIB_OK -// DHTLIB_ERROR_CHECKSUM -// DHTLIB_ERROR_TIMEOUT -int dht_read44(uint8_t pin) __attribute__((alias("dht_read"))); - ///////////////////////////////////////////////////// // // PRIVATE @@ -236,7 +179,7 @@ int dht_readSensor(uint8_t pin, uint8_t wakeupDelay) // volatile uint8_t *PIR = portInputRegister(port); // EMPTY BUFFER - memset(dht_bytes, sizeof(uint8_t)*5, 0); + memset(dht_bytes, 0, sizeof(uint8_t)*5); // REQUEST SAMPLE // pinMode(pin, OUTPUT); @@ -309,32 +252,6 @@ int dht_readSensor(uint8_t pin, uint8_t wakeupDelay) return DHTLIB_OK; } -// Assembles the high and low byte in a signed 16bit value -static double getValue(dht_Signal s) -{ - uint8_t high=0, low=0; - - // the '8' variants leave the low byte set to 0 - switch(s){ - case Humidity: - low = dht_bytes[1]; - case Humidity8: - high = dht_bytes[0]; - break; - case Temperature: - low = dht_bytes[3]; - case Temperature8: - high = dht_bytes[2]; - break; - } - return ((high << 8) | low) * 0.1; -} - -static bool verifyChecksum(){ - uint8_t sum = dht_bytes[0] + dht_bytes[1] + dht_bytes[2] + dht_bytes[3]; - return (dht_bytes[4] == sum); -} - // // END OF FILE // diff --git a/app/dht/dht.h b/app/dht/dht.h index 8c78909c44..ca7c39ef3c 100644 --- a/app/dht/dht.h +++ b/app/dht/dht.h @@ -30,8 +30,6 @@ #define DHTLIB_DHT_WAKEUP 1 #define DHTLIB_DHT_UNI_WAKEUP 18 -#define DHT_DEBUG - // max timeout is 100 usec. // For a 16 Mhz proc 100 usec is 1600 clock cycles // loops using DHTLIB_TIMEOUT use at least 4 clock cycli @@ -48,19 +46,18 @@ #define DIRECT_WRITE_LOW(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 0)) #define DIRECT_WRITE_HIGH(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 1)) +typedef enum { + DHT11 = 0, + DHT12, + DHT22, + DHT_NON11 +} dht_type; + // return values: // DHTLIB_OK // DHTLIB_ERROR_CHECKSUM // DHTLIB_ERROR_TIMEOUT -int dht_read_universal(uint8_t pin); -int dht_read11(uint8_t pin); -int dht_read(uint8_t pin); - -int dht_read21(uint8_t pin); -int dht_read22(uint8_t pin); -int dht_read33(uint8_t pin); -int dht_read44(uint8_t pin); - +int dht_read(uint8_t pin, dht_type type); double dht_getHumidity(void); double dht_getTemperature(void); diff --git a/app/include/lwip/app/espconn_tcp.h b/app/include/lwip/app/espconn_tcp.h index 76aa4b8c1a..3da9dabde0 100644 --- a/app/include/lwip/app/espconn_tcp.h +++ b/app/include/lwip/app/espconn_tcp.h @@ -16,6 +16,9 @@ #define espconn_manual_recv_disabled(espconn) (((espconn)->pcommon.espconn_opt & ESPCONN_MANUALRECV) != 0) #define espconn_manual_recv_enabled(espconn) (((espconn)->pcommon.espconn_opt & ESPCONN_MANUALRECV) == 0) +extern int ets_task(); +extern int ets_post(); + /****************************************************************************** * FunctionName : espconn_kill_oldest_pcb * Description : A oldest incoming connection has been killed. diff --git a/app/include/lwip/mem.h b/app/include/lwip/mem.h index 825d222611..52362f3acc 100644 --- a/app/include/lwip/mem.h +++ b/app/include/lwip/mem.h @@ -45,6 +45,14 @@ extern "C" { typedef size_t mem_size_t; +void *pvPortMalloc (size_t sz, const char *, unsigned, bool); +void vPortFree (void *p, const char *, unsigned); +void *pvPortZalloc (size_t sz, const char *, unsigned); +void *pvPortRealloc (void *p, size_t n, const char *, unsigned); +void* pvPortCalloc(size_t count,size_t size,const char *,unsigned); +void* pvPortCallocIram(size_t count,size_t size,const char *,unsigned); +void *pvPortZallocIram (size_t sz, const char *, unsigned); + /* aliases for C library malloc() */ #define mem_init() /* in case C library malloc() needs extra protection, diff --git a/app/include/mbedtls/platform.h b/app/include/mbedtls/platform.h index 1f907d6fed..3a164897e2 100644 --- a/app/include/mbedtls/platform.h +++ b/app/include/mbedtls/platform.h @@ -58,6 +58,9 @@ extern "C" { * \{ */ +void *pvPortCalloc(unsigned int count, unsigned int size, const char*, unsigned); +void vPortFree (void *p, const char *, unsigned); + #if !defined(MBEDTLS_PLATFORM_NO_STD_FUNCTIONS) #include #include diff --git a/app/include/sys/socket.h b/app/include/sys/socket.h index a57378dc3e..937cacc042 100644 --- a/app/include/sys/socket.h +++ b/app/include/sys/socket.h @@ -63,6 +63,9 @@ typedef enum{ NETCONN_STATE_SUMNUM }netconn_state; +extern int __attribute__((weak)) espconn_mbedtls_parse_internal(int socket, sint8 error); +extern int __attribute__((weak)) espconn_mbedtls_parse_thread(int socket, int event, int error); + #if (!defined(lwIP_unlikely)) #define lwIP_unlikely(Expression) !!(Expression) #endif @@ -330,4 +333,8 @@ uint32_t lwip_getul(char *str); #define close(s) lwip_close(s) #define getul(s) lwip_getul(s) +extern int system_overclock(void); +extern int system_restoreclock(void); +extern char *sys_itoa(int n); + #endif /* ESPCONN_SOCKT_H_ */ diff --git a/app/include/user_config.h b/app/include/user_config.h index 084910e02d..4ba657869e 100644 --- a/app/include/user_config.h +++ b/app/include/user_config.h @@ -42,21 +42,34 @@ //#define DISABLE_STARTUP_BANNER -// Three separate build variants are now supported. The main difference is in the -// processing of numeric data types. If LUA_NUMBER_INTEGRAL is defined, then +// When using Lua 5.1, two different builds are now supported. +// The main difference is in the // processing of numeric data types. +// If LUA_NUMBER_INTEGRAL is defined, then // all numeric calculations are done in integer, with divide being an integer -// operations, and decimal fraction constants are illegal. Otherwise all -// numeric operations use floating point, though they are exact for integer -// expressions < 2^53. - -// The main advantage of INTEGRAL builds is that the basic internal storage unit, -// the TValue, is 8 bytes long. We have now reduced the size of FP TValues to -// 12 bytes rather than the previous 16 as this gives a material RAM saving with -// no performance loss. However, you can define LUA_DWORD_ALIGNED_TVALUES and -// this will force 16 byte TValues on FP builds. +// operation, and decimal fraction constants are illegal. +// Otherwise all floating point operations use doubles. All integer values +// can be represented exactly in floating point. //#define LUA_NUMBER_INTEGRAL -//#define LUA_DWORD_ALIGNED_TVALUES + +// When using Lua 5.3, two different builds are now supported. +// The main difference is in the processing of numeric data types. +// If LUA_NUMBER_64BITS is defined, then doubles are used to hold floating +// point numbers. Integers under 2^53 are representable exactly in doubles. +// Integers are held in 64-bit variables. +// Otherwise all floating point operations use floats. Only integers under 2^24 +// can be represented exactly in floating point. Integers are represented in 32 bit variables. +// Note that Lua 5.3 also supports Integers natively, but you have to be careful +// not to promote an integer to a floating point variable if you are using a float build +// as you can lose precision. + +//#define LUA_NUMBER_64BITS + +// The main advantage of INTEGRAL builds and non 64BITS builds is that the basic internal +// storage unit, the TValue, is 8 bytes long. For 64BITS builds, we have now reduced +// the size of FP TValues to 12 bytes rather than the previous 16 as this gives a +// material RAM saving with no performance loss. +// // The Lua Flash Store (LFS) allows you to store Lua code in Flash memory and diff --git a/app/include/user_modules.h b/app/include/user_modules.h index 814b835ada..3718027ae6 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -45,6 +45,7 @@ //#define LUA_USE_MODULES_PCM //#define LUA_USE_MODULES_PERF //#define LUA_USE_MODULES_PIPE +//#define LUA_USE_MODULES_PIXBUF //#define LUA_USE_MODULES_PWM //#define LUA_USE_MODULES_PWM2 //#define LUA_USE_MODULES_RFSWITCH diff --git a/app/lua/Makefile b/app/lua/Makefile index aeb66d1755..d3a9e76e5a 100644 --- a/app/lua/Makefile +++ b/app/lua/Makefile @@ -18,6 +18,20 @@ endif STD_CFLAGS=-std=gnu11 -Wimplicit -Wall + +# Validate LUA setting +ifeq ("$(LUA)","") +else ifeq ("$(LUA)","51") + # ok +else ifeq ("$(LUA)","53") + $(error Your variable LUA="$(LUA)" looks like you probably want \ + app/lua53/Makefile instead) +else + $(error Unsupported value "$(LUA)" for variable "LUA", \ + expected empty/unset (recommended) or "51") +endif + + ############################################################# # Configuration i.e. compile options etc. # Target specific stuff (defines etc.) goes in here! diff --git a/app/lua/lauxlib.c b/app/lua/lauxlib.c index ff7bee2db9..efb89843d4 100644 --- a/app/lua/lauxlib.c +++ b/app/lua/lauxlib.c @@ -214,7 +214,7 @@ LUALIB_API int luaL_argerror (lua_State *L, int narg, const char *extramsg) { LUALIB_API int luaL_typerror (lua_State *L, int narg, const char *tname) { const char *msg = lua_pushfstring(L, "%s expected, got %s", - tname, lua_typename(L, narg)); + tname, luaL_typename(L, narg)); return luaL_argerror(L, narg, msg); } @@ -285,21 +285,25 @@ LUALIB_API int luaL_rometatable (lua_State *L, const char* tname, const ROTable return 1; } -LUALIB_API void *luaL_checkudata (lua_State *L, int ud, const char *tname) { +LUALIB_API void *luaL_testudata (lua_State *L, int ud, const char *tname) { void *p = lua_touserdata(L, ud); if (p != NULL) { /* value is a userdata? */ if (lua_getmetatable(L, ud)) { /* does it have a metatable? */ lua_getfield(L, LUA_REGISTRYINDEX, tname); /* get correct metatable */ - if (lua_rawequal(L, -1, -2)) { /* does it have the correct mt? */ - lua_pop(L, 2); /* remove both metatables */ - return p; - } + if (!lua_rawequal(L, -1, -2)) /* not the same? */ + p = NULL; /* value is a userdata with wrong metatable */ + lua_pop(L, 2); /* remove both metatables */ + return p; } } - luaL_typerror(L, ud, tname); /* else error */ - return NULL; /* to avoid warnings */ + return NULL; /* value is not a userdata with a metatable */ } +LUALIB_API void *luaL_checkudata (lua_State *L, int ud, const char *tname) { + void *p = luaL_testudata(L, ud, tname); + if (p == NULL) luaL_typerror(L, ud, tname); + return p; +} LUALIB_API void luaL_checkstack (lua_State *L, int space, const char *mes) { if (!lua_checkstack(L, space)) diff --git a/app/lua/lauxlib.h b/app/lua/lauxlib.h index 6708aaecd1..83ecd9329e 100644 --- a/app/lua/lauxlib.h +++ b/app/lua/lauxlib.h @@ -66,6 +66,7 @@ LUALIB_API void (luaL_checkany) (lua_State *L, int narg); LUALIB_API int (luaL_newmetatable) (lua_State *L, const char *tname); LUALIB_API int (luaL_rometatable) (lua_State *L, const char* tname, const ROTable *p); +LUALIB_API void *(luaL_testudata) (lua_State *L, int ud, const char *tname); LUALIB_API void *(luaL_checkudata) (lua_State *L, int ud, const char *tname); LUALIB_API void (luaL_where) (lua_State *L, int lvl); diff --git a/app/lua/luac_cross/Makefile b/app/lua/luac_cross/Makefile index 3835e11aa3..e617da739f 100644 --- a/app/lua/luac_cross/Makefile +++ b/app/lua/luac_cross/Makefile @@ -15,6 +15,20 @@ CCFLAGS += -Wall TARGET = host + +# Validate LUA setting +ifeq ("$(LUA)","") +else ifeq ("$(LUA)","51") + # ok +else ifeq ("$(LUA)","53") + $(error Your variable LUA="$(LUA)" looks like you probably want \ + app/lua53/host/Makefile instead) +else + $(error Unsupported value "$(LUA)" for variable "LUA", \ + expected empty/unset (recommended) or "51") +endif + + VERBOSE ?= V ?= $(VERBOSE) ifeq ("$(V)","1") diff --git a/app/lua/luaconf.h b/app/lua/luaconf.h index 0a21d0be88..9f3b8bfaa6 100644 --- a/app/lua/luaconf.h +++ b/app/lua/luaconf.h @@ -104,7 +104,7 @@ //## Modified for eLua //## Defaults search modules path to our ROM File System #ifndef LUA_RPC -#define LUA_PATH_DEFAULT "/rfs/?.lua;/rfs/?.lc;/mmc/?.lua;/mmc/?.lc;/rom/?.lua;/rom/?.lc" +#define LUA_PATH_DEFAULT "?.lc;?.lua" #define LUA_CPATH_DEFAULT "" #else // #ifndef LUA_RPC #define LUA_PATH_DEFAULT \ @@ -181,6 +181,10 @@ #endif // #if !defined LUA_INTEGRAL_LONGLONG #endif // #if !defined LUA_NUMBER_INTEGRAL +#ifdef LUA_NUMBER_64BITS +#error Lua 5.1 does not support 64 bit inetegers. +#endif + /* @@ LUA_API is a mark for all core API functions. @@ LUALIB_API is a mark for all standard library functions. diff --git a/app/lua53/Makefile b/app/lua53/Makefile index f907aebb70..47c39abb93 100644 --- a/app/lua53/Makefile +++ b/app/lua53/Makefile @@ -18,6 +18,18 @@ endif STD_CFLAGS=-std=gnu11 -Wimplicit -Wall + +# Validate LUA setting +ifeq ("$(LUA)","53") + # ok +else ifeq ("$(LUA)","51") + $(error Your variable LUA="$(LUA)" looks like you probably want \ + app/lua/Makefile instead) +else + $(error Expected environment variable "LUA" to be "53", not "$(LUA)") +endif + + ############################################################# # Configuration i.e. compile options etc. # Target specific stuff (defines etc.) goes in here! diff --git a/app/lua53/host/Makefile b/app/lua53/host/Makefile index 5667b3c43f..b83ef275c3 100644 --- a/app/lua53/host/Makefile +++ b/app/lua53/host/Makefile @@ -23,6 +23,18 @@ else # MAKEFLAGS += --silent -w endif # $(V)==1 + +# Validate LUA setting +ifeq ("$(LUA)","53") + # ok +else ifeq ("$(LUA)","51") + $(error Your variable LUA="$(LUA)" looks like you probably want \ + app/lua/luac_cross/Makefile instead) +else + $(error Expected environment variable "LUA" to be "53", not "$(LUA)") +endif + + DEBUG ?= ifeq ("$(DEBUG)","1") FLAVOR = debug diff --git a/app/lua53/ldump.c b/app/lua53/ldump.c index 394cdd84c5..90bbdfaaab 100644 --- a/app/lua53/ldump.c +++ b/app/lua53/ldump.c @@ -102,7 +102,7 @@ static void DumpNumber (lua_Number x, DumpState *D) { ** 0TTTNNNN or 1TTTNNNN (1NNNNNNN)* 0NNNNNNN */ static void DumpIntTT (lu_byte tt, lua_Integer y, DumpState *D) { - int x = y < 0 ? -(y + 1) : y; + lua_Integer x = y < 0 ? -(y + 1) : y; lu_byte buf[sizeof(lua_Integer) + 3]; lu_byte *b = buf + sizeof(buf) - 1; *b-- = x & 0x7f; x >>= 7; diff --git a/app/lua53/lobject.h b/app/lua53/lobject.h index c83efb23fd..5441f81ae8 100644 --- a/app/lua53/lobject.h +++ b/app/lua53/lobject.h @@ -134,12 +134,17 @@ typedef union Value { #define TValuefields Value value_; int tt_ +#ifdef LUA_USE_ESP +# pragma pack(4) +#endif typedef struct lua_TValue { TValuefields; } TValue; - +#ifdef LUA_USE_ESP +# pragma pack() +#endif /* macro defining a nil value */ #define NILCONSTANT {NULL}, LUA_TNIL diff --git a/app/lua53/luaconf.h b/app/lua53/luaconf.h index 0bb4bef811..c4b91d0d66 100644 --- a/app/lua53/luaconf.h +++ b/app/lua53/luaconf.h @@ -85,9 +85,17 @@ # define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE #endif #endif -# define LUA_INT_TYPE LUA_INT_INT +#ifdef LUA_NUMBER_64BITS +# define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE +# define LUA_INT_TYPE LUA_INT_LONGLONG +#else # define LUA_FLOAT_TYPE LUA_FLOAT_FLOAT -//# define LUA_FLOAT_TYPE LUA_FLOAT_DOUBLE +# define LUA_INT_TYPE LUA_INT_INT +#endif + +#ifdef LUA_NUMBER_INTEGRAL +#error LUA_NUMBER_INTEGRAL is not supported in LUA5.3 builds +#endif /* ** Configuration for Paths. diff --git a/app/mbedtls/app/Makefile b/app/mbedtls/app/Makefile index fa5dc92fd6..55c0baa5ae 100644 --- a/app/mbedtls/app/Makefile +++ b/app/mbedtls/app/Makefile @@ -17,6 +17,7 @@ GEN_LIBS = libapp.a endif +STD_CFLAGS=-std=gnu11 ############################################################# # Configuration i.e. compile options etc. diff --git a/app/mbedtls/app/espconn_mbedtls.c b/app/mbedtls/app/espconn_mbedtls.c index 82beaec9e6..15282b7bb4 100644 --- a/app/mbedtls/app/espconn_mbedtls.c +++ b/app/mbedtls/app/espconn_mbedtls.c @@ -42,6 +42,8 @@ static const char mem_debug_file[] ICACHE_RODATA_ATTR = __FILE__; #include "sys/socket.h" #include "sys/espconn_mbedtls.h" +#include "lwip/app/espconn_tcp.h" + static os_event_t lwIPThreadQueue[lwIPThreadQueueLen]; static bool lwIPThreadFlag = false; diff --git a/app/mbedtls/app/lwIPSocket.c b/app/mbedtls/app/lwIPSocket.c index 8c798f1c8f..4b35141398 100644 --- a/app/mbedtls/app/lwIPSocket.c +++ b/app/mbedtls/app/lwIPSocket.c @@ -34,6 +34,7 @@ #include "ets_sys.h" #include "os_type.h" +#include #include "lwip/mem.h" #include "sys/socket.h" @@ -42,6 +43,9 @@ static const char mem_debug_file[] ICACHE_RODATA_ATTR = __FILE__; #endif +void *pvPortZalloc (size_t sz, const char *, unsigned); +void vPortFree (void *p, const char *, unsigned); + /** The global array of available sockets */ static lwIP_sock sockets[NUM_SOCKETS]; diff --git a/app/modules/apa102.c b/app/modules/apa102.c index b17b67ac94..cc832757d6 100644 --- a/app/modules/apa102.c +++ b/app/modules/apa102.c @@ -5,6 +5,8 @@ #include "platform.h" #include "user_interface.h" +#include "pixbuf.h" + #define NOP asm volatile(" nop \n\t") @@ -79,12 +81,25 @@ static int apa102_write(lua_State* L) { MOD_CHECK_ID(gpio, clock_pin); uint32_t alt_clock_pin = pin_num[clock_pin]; - size_t buf_len; - const char *buf = luaL_checklstring(L, 3, &buf_len); - uint32_t nbr_frames = buf_len / 4; - - if (nbr_frames > 100000) { - return luaL_error(L, "The supplied buffer is too long, and might cause the callback watchdog to bark."); + const char *buf; + uint32_t nbr_frames; + + switch(lua_type(L, 3)) { + case LUA_TSTRING: { + size_t buf_len; + buf = luaL_checklstring(L, 3, &buf_len); + nbr_frames = buf_len / 4; + break; + } + case LUA_TUSERDATA: { + pixbuf *buffer = pixbuf_from_lua_arg(L, 3); + luaL_argcheck(L, buffer->nchan == 4, 3, "Pixbuf not 4-channel"); + buf = (const char *)buffer->values; + nbr_frames = buffer->npix; + break; + } + default: + return luaL_argerror(L, 3, "String or pixbuf expected"); } // Initialize the output pins diff --git a/app/modules/dht.c b/app/modules/dht.c index e6e9868349..803a41f7fe 100644 --- a/app/modules/dht.c +++ b/app/modules/dht.c @@ -32,7 +32,7 @@ static int dht_lapi_read( lua_State *L ) { unsigned id = luaL_checkinteger( L, 1 ); MOD_CHECK_ID( dht, id ); - lua_pushinteger( L, dht_read_universal(id) ); + lua_pushinteger( L, dht_read(id, DHT_NON11) ); aux_read( L ); return 5; } @@ -42,7 +42,17 @@ static int dht_lapi_read11( lua_State *L ) { unsigned id = luaL_checkinteger( L, 1 ); MOD_CHECK_ID( dht, id ); - lua_pushinteger( L, dht_read11(id) ); + lua_pushinteger( L, dht_read(id, DHT11) ); + aux_read( L ); + return 5; +} + +// Lua: status, temp, humi, tempdec, humidec = dht.read12( id )) +static int dht_lapi_read12( lua_State *L ) +{ + unsigned id = luaL_checkinteger( L, 1 ); + MOD_CHECK_ID( dht, id ); + lua_pushinteger( L, dht_read(id, DHT12) ); aux_read( L ); return 5; } @@ -52,48 +62,17 @@ static int dht_lapi_readxx( lua_State *L ) { unsigned id = luaL_checkinteger( L, 1 ); MOD_CHECK_ID( dht, id ); - lua_pushinteger( L, dht_read(id) ); + lua_pushinteger( L, dht_read(id, DHT22) ); aux_read( L ); return 5; } -// // Lua: result = dht.humidity() -// static int dht_lapi_humidity( lua_State *L ) -// { -// lua_pushnumber( L, dht_getHumidity() ); -// return 1; -// } - -// // Lua: result = dht.humiditydecimal() -// static int dht_lapi_humiditydecimal( lua_State *L ) -// { -// double value = dht_getHumidity(); -// int result = (int)((value - (int)value) * 1000); -// lua_pushnumber( L, result ); -// return 1; -// } - -// // Lua: result = dht.temperature() -// static int dht_lapi_temperature( lua_State *L ) -// { -// lua_pushnumber( L, dht_getTemperature() ); -// return 1; -// } - -// // Lua: result = dht.temperaturedecimal() -// static int dht_lapi_temperaturedecimal( lua_State *L ) -// { -// double value = dht_getTemperature(); -// int result = (int)((value - (int)value) * 1000); -// lua_pushnumber( L, result ); -// return 1; -// } - // Module function map LROT_BEGIN(dht, NULL, 0) LROT_FUNCENTRY( read, dht_lapi_read ) LROT_FUNCENTRY( read11, dht_lapi_read11 ) - LROT_FUNCENTRY( readxx, dht_lapi_readxx ) + LROT_FUNCENTRY( read12, dht_lapi_read12 ) + LROT_FUNCENTRY( readxx, dht_lapi_read ) LROT_NUMENTRY( OK, DHTLIB_OK ) LROT_NUMENTRY( ERROR_CHECKSUM, DHTLIB_ERROR_CHECKSUM ) LROT_NUMENTRY( ERROR_TIMEOUT, DHTLIB_ERROR_TIMEOUT ) diff --git a/app/modules/mqtt.c b/app/modules/mqtt.c index a8d072686d..321195739a 100644 --- a/app/modules/mqtt.c +++ b/app/modules/mqtt.c @@ -22,21 +22,12 @@ #define MQTT_MAX_CLIENT_LEN 64 #define MQTT_MAX_USER_LEN 64 #define MQTT_MAX_PASS_LEN 64 -#define MQTT_SEND_TIMEOUT 5 - - /* - * This timeout needs to be long enough for a typical TCP connect() - * *and* the TLS handshake, if any. Most network stacks seem to wait - * tens of seconds for connect(), and TLS can take a good deal of time - * and several round trips. Because this matters only rarely, it may - * as well be set pretty high. - */ -#define MQTT_CONNECT_TIMEOUT 60 +#define MQTT_SEND_TIMEOUT 5 /* seconds */ typedef enum { MQTT_INIT, - MQTT_CONNECT_SENT, MQTT_CONNECT_SENDING, + MQTT_CONNECT_SENT, MQTT_DATA } tConnState; @@ -59,7 +50,6 @@ typedef enum { typedef struct mqtt_state_t { - uint16_t port; msg_queue_t* pending_msg_q; uint16_t next_message_id; @@ -86,15 +76,28 @@ typedef struct lmqtt_userdata int cb_suback_ref; int cb_unsuback_ref; int cb_puback_ref; + + /* Configuration options */ + struct { + int client_id_ref; + int username_ref; + int password_ref; + int will_topic_ref; + int will_message_ref; + int keepalive; + uint16_t max_message_length; + struct { + unsigned will_qos : 2; + bool will_retain : 1; + bool clean_session : 1; + bool secure : 1; + } flags; + } conf; + mqtt_state_t mqtt_state; - mqtt_connect_info_t connect_info; - uint16_t keep_alive_tick; - uint32_t event_timeout; -#ifdef CLIENT_SSL_ENABLE - uint8_t secure; -#endif bool connected; // indicate socket connected, not mqtt prot connected. bool keepalive_sent; + bool sending; // data sent to network stack, awaiting local acknowledge ETSTimer mqttTimer; tConnState connState; }lmqtt_userdata; @@ -116,25 +119,28 @@ static uint16_t mqtt_next_message_id(lmqtt_userdata * mud) return mud->mqtt_state.next_message_id; } +static void mqtt_socket_cb_lua_noarg(lua_State *L, lmqtt_userdata *mud, int cb) +{ + if (cb == LUA_NOREF) + return; + + lua_rawgeti(L, LUA_REGISTRYINDEX, cb); + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); + lua_call(L, 1, 0); +} + + static void mqtt_socket_disconnected(void *arg) // tcp only { NODE_DBG("enter mqtt_socket_disconnected.\n"); - bool call_back = false; lmqtt_userdata *mud = arg; if(mud == NULL) return; os_timer_disarm(&mud->mqttTimer); - lua_State *L = lua_getstate(); - - if(mud->connected){ // call back only called when socket is from connection to disconnection. - mud->connected = false; - if((mud->cb_disconnect_ref != LUA_NOREF) && (mud->self_ref != LUA_NOREF)) { - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_disconnect_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata(client) to callback func in lua - call_back = true; - } + while (mud->mqtt_state.pending_msg_q) { + msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); } if(mud->mqtt_state.recv_buffer) { @@ -148,21 +154,30 @@ static void mqtt_socket_disconnected(void *arg) // tcp only free(mud->pesp_conn.proto.tcp); mud->pesp_conn.proto.tcp = NULL; - mud->connected = false; - luaL_unref(L, LUA_REGISTRYINDEX, mud->self_ref); - mud->self_ref = LUA_NOREF; // unref this, and the mqtt.socket userdata will delete it self + int selfref = mud->self_ref; + mud->self_ref = LUA_NOREF; + + lua_State *L = lua_getstate(); - if(call_back){ - lua_call(L, 1, 0); + if(mud->connected){ // call back only called when socket is from connection to disconnection. + mud->connected = false; + if((mud->cb_disconnect_ref != LUA_NOREF) && (selfref != LUA_NOREF)) { + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_disconnect_ref); + lua_rawgeti(L, LUA_REGISTRYINDEX, selfref); + lua_call(L, 1, 0); + } } + // unref this, and the mqtt.socket userdata will delete it self + luaL_unref(L, LUA_REGISTRYINDEX, selfref); + NODE_DBG("leave mqtt_socket_disconnected.\n"); } static void mqtt_socket_do_disconnect(struct lmqtt_userdata *mud) { #ifdef CLIENT_SSL_ENABLE - if (mud->secure) { + if (mud->conf.flags.secure) { espconn_secure_disconnect(&mud->pesp_conn); } else #endif @@ -173,17 +188,12 @@ static void mqtt_socket_do_disconnect(struct lmqtt_userdata *mud) static void mqtt_socket_reconnected(void *arg, sint8_t err) { - NODE_DBG("enter mqtt_socket_reconnected.\n"); + NODE_DBG("enter mqtt_socket_reconnected (err=%d)\n", err); lmqtt_userdata *mud = arg; if(mud == NULL) return; os_timer_disarm(&mud->mqttTimer); - - mud->event_timeout = 0; // no need to count anymore - - mqtt_socket_do_disconnect(mud); - mqtt_connack_fail(mud, MQTT_CONN_FAIL_SERVER_NOT_FOUND); mqtt_socket_disconnected(arg); @@ -212,7 +222,7 @@ static void deliver_publish(lmqtt_userdata * mud, uint8_t* message, uint16_t len lua_State *L = lua_getstate(); if(event_data.topic && (event_data.topic_length > 0)){ lua_rawgeti(L, LUA_REGISTRYINDEX, cb_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata to callback func in lua + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); lua_pushlstring(L, event_data.topic, event_data.topic_length); } else { NODE_DBG("get wrong packet.\n"); @@ -230,6 +240,8 @@ static void deliver_publish(lmqtt_userdata * mud, uint8_t* message, uint16_t len static void mqtt_connack_fail(lmqtt_userdata * mud, int reason_code) { + NODE_DBG("enter mqtt_connack_fail\n"); + if(mud->cb_connect_fail_ref == LUA_NOREF || mud->self_ref == LUA_NOREF) { return; @@ -238,35 +250,41 @@ static void mqtt_connack_fail(lmqtt_userdata * mud, int reason_code) lua_State *L = lua_getstate(); lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_connect_fail_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata(client) to callback func in lua + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); lua_pushinteger(L, reason_code); lua_call(L, 2, 0); + + NODE_DBG("leave mqtt_connack_fail\n"); } static sint8 mqtt_send_if_possible(struct lmqtt_userdata *mud) { + /* Waiting for the local network stack to get back to us? Can't send. */ + if (mud->sending) + return ESPCONN_OK; + sint8 espconn_status = ESPCONN_OK; - // This indicates if we have sent something and are waiting for something to - // happen - if (mud->event_timeout == 0) { - msg_queue_t *pending_msg = msg_peek(&(mud->mqtt_state.pending_msg_q)); - if (pending_msg) { - mud->event_timeout = MQTT_SEND_TIMEOUT; - NODE_DBG("Sent: %d\n", pending_msg->msg.length); + msg_queue_t *pending_msg = msg_peek(&(mud->mqtt_state.pending_msg_q)); + if (pending_msg && !pending_msg->sent) { + pending_msg->sent = 1; + NODE_DBG("Sent: %d\n", pending_msg->msg.length); #ifdef CLIENT_SSL_ENABLE - if( mud->secure ) - { - espconn_status = espconn_secure_send(&mud->pesp_conn, pending_msg->msg.data, pending_msg->msg.length ); - } - else + if( mud->conf.flags.secure ) + { + espconn_status = espconn_secure_send(&mud->pesp_conn, pending_msg->msg.data, pending_msg->msg.length ); + } + else #endif - { - espconn_status = espconn_send(&mud->pesp_conn, pending_msg->msg.data, pending_msg->msg.length ); - } - mud->keep_alive_tick = 0; + { + espconn_status = espconn_send(&mud->pesp_conn, pending_msg->msg.data, pending_msg->msg.length ); } + mud->sending = true; + + os_timer_disarm(&mud->mqttTimer); + os_timer_arm(&mud->mqttTimer, MQTT_SEND_TIMEOUT * 1000, 0); } + NODE_DBG("send_if_poss, queue size: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q))); return espconn_status; } @@ -388,11 +406,11 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) mqtt_msg_init(&msgb, temp_buffer, MQTT_BUF_SIZE); mqtt_message_t *temp_msg = NULL; - lua_State *L = lua_getstate(); switch(mud->connState){ case MQTT_CONNECT_SENDING: case MQTT_CONNECT_SENT: - mud->event_timeout = 0; + os_timer_disarm(&mud->mqttTimer); + os_timer_arm(&mud->mqttTimer, mud->conf.keepalive * 1000, 0); if(mqtt_get_type(in_buffer) != MQTT_MSG_TYPE_CONNACK){ NODE_DBG("MQTT: Invalid packet\r\n"); @@ -414,13 +432,8 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) mud->connState = MQTT_DATA; NODE_DBG("MQTT: Connected\r\n"); mud->keepalive_sent = 0; - if(mud->cb_connect_ref == LUA_NOREF) - break; - if(mud->self_ref == LUA_NOREF) - break; - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_connect_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata(client) to callback func in lua - lua_call(L, 1, 0); + + mqtt_socket_cb_lua_noarg(lua_getstate(), mud, mud->cb_connect_ref); break; } break; @@ -435,7 +448,7 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) message_length, in_buffer_length); - if (message_length > mud->connect_info.max_message_length) { + if (message_length > mud->conf.max_message_length) { // The pending message length is larger than we was configured to allow if(msg_qos > 0 && msg_id == 0) { NODE_DBG("MQTT: msg too long, but not enough data to get msg_id: total=%u, deliver=%u\r\n", message_length, in_buffer_length); @@ -526,13 +539,8 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) if(pending_msg && pending_msg->msg_type == MQTT_MSG_TYPE_SUBSCRIBE && pending_msg->msg_id == msg_id){ NODE_DBG("MQTT: Subscribe successful\r\n"); msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - if (mud->cb_suback_ref == LUA_NOREF) - break; - if (mud->self_ref == LUA_NOREF) - break; - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_suback_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); - lua_call(L, 1, 0); + + mqtt_socket_cb_lua_noarg(lua_getstate(), mud, mud->cb_suback_ref); } break; case MQTT_MSG_TYPE_UNSUBACK: @@ -540,13 +548,7 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) NODE_DBG("MQTT: UnSubscribe successful\r\n"); msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - if (mud->cb_unsuback_ref == LUA_NOREF) - break; - if (mud->self_ref == LUA_NOREF) - break; - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_unsuback_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); - lua_call(L, 1, 0); + mqtt_socket_cb_lua_noarg(lua_getstate(), mud, mud->cb_unsuback_ref); } break; case MQTT_MSG_TYPE_PUBLISH: @@ -569,13 +571,8 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) if(pending_msg && pending_msg->msg_type == MQTT_MSG_TYPE_PUBLISH && pending_msg->msg_id == msg_id){ NODE_DBG("MQTT: Publish with QoS = 1 successful\r\n"); msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - if(mud->cb_puback_ref == LUA_NOREF) - break; - if(mud->self_ref == LUA_NOREF) - break; - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_puback_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata to callback func in lua - lua_call(L, 1, 0); + + mqtt_socket_cb_lua_noarg(lua_getstate(), mud, mud->cb_puback_ref); } break; @@ -603,13 +600,8 @@ static void mqtt_socket_received(void *arg, char *pdata, unsigned short len) if(pending_msg && pending_msg->msg_type == MQTT_MSG_TYPE_PUBREL && pending_msg->msg_id == msg_id){ NODE_DBG("MQTT: Publish with QoS = 2 successful\r\n"); msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - if(mud->cb_puback_ref == LUA_NOREF) - break; - if(mud->self_ref == LUA_NOREF) - break; - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_puback_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata to callback func in lua - lua_call(L, 1, 0); + + mqtt_socket_cb_lua_noarg(lua_getstate(), mud, mud->cb_puback_ref); } break; case MQTT_MSG_TYPE_PINGREQ: @@ -669,40 +661,51 @@ static void mqtt_socket_sent(void *arg) return; if(!mud->connected) return; - // call mqtt_sent() - mud->event_timeout = 0; - mud->keep_alive_tick = 0; + + mud->sending = false; + + os_timer_disarm(&mud->mqttTimer); if(mud->connState == MQTT_CONNECT_SENDING){ mud->connState = MQTT_CONNECT_SENT; - mud->event_timeout = MQTT_SEND_TIMEOUT; + os_timer_arm(&mud->mqttTimer, MQTT_SEND_TIMEOUT * 1000, 0); // MQTT_CONNECT not queued. return; } + + /* Ready for timeout */ + os_timer_arm(&mud->mqttTimer, mud->conf.keepalive * 1000, 0); + NODE_DBG("sent1, queue size: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q))); - uint8_t try_send = 1; - // qos = 0, publish and forgot. + msg_queue_t *node = msg_peek(&(mud->mqtt_state.pending_msg_q)); - if(node && node->msg_type == MQTT_MSG_TYPE_PUBLISH && node->publish_qos == 0) { - msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - if(mud->cb_puback_ref != LUA_NOREF && mud->self_ref != LUA_NOREF) { - lua_State *L = lua_getstate(); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->cb_puback_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, mud->self_ref); // pass the userdata to callback func in lua - lua_call(L, 1, 0); + if (node) { + switch (node->msg_type) { + case MQTT_MSG_TYPE_PUBLISH: + // qos = 0, publish and forget. Run the callback now because we + // won't get a puback from the server and it's not clear when else + // we should tell the user the message drained from the egress queue + if (node->publish_qos == 0) { + msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); + mqtt_socket_cb_lua_noarg(lua_getstate(), mud, mud->cb_puback_ref); + mqtt_send_if_possible(mud); + } + break; + case MQTT_MSG_TYPE_PUBACK: + /* FALLTHROUGH */ + case MQTT_MSG_TYPE_PUBCOMP: + /* FALLTHROUGH */ + case MQTT_MSG_TYPE_PINGREQ: + msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); + mqtt_send_if_possible(mud); + break; + case MQTT_MSG_TYPE_DISCONNECT: + msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); + mqtt_socket_do_disconnect(mud); + break; } - } else if(node && node->msg_type == MQTT_MSG_TYPE_PUBACK) { - msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - } else if(node && node->msg_type == MQTT_MSG_TYPE_PUBCOMP) { - msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - } else if(node && node->msg_type == MQTT_MSG_TYPE_PINGREQ) { - msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - } else { - try_send = 0; - } - if (try_send) { - mqtt_send_if_possible(mud); } + NODE_DBG("sent2, queue size: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q))); NODE_DBG("leave mqtt_socket_sent.\n"); } @@ -723,13 +726,34 @@ static void mqtt_socket_connected(void *arg) mqtt_message_buffer_t msgb; mqtt_msg_init(&msgb, temp_buffer, MQTT_BUF_SIZE); - mqtt_message_t* temp_msg = mqtt_msg_connect(&msgb, &mud->connect_info); + /* Build the mqtt_connect_info on the stack and assemble the message */ + lua_State *L = lua_getstate(); + int ltop = lua_gettop(L); + struct mqtt_connect_info mci; + mci.clean_session = mud->conf.flags.clean_session; + mci.will_retain = mud->conf.flags.will_retain; + mci.will_qos = mud->conf.flags.will_qos; + mci.keepalive = mud->conf.keepalive; + + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->conf.client_id_ref); + mci.client_id = lua_tostring(L, -1); + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->conf.username_ref); + mci.username = lua_tostring(L, -1); + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->conf.password_ref); + mci.password = lua_tostring(L, -1); + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->conf.will_topic_ref); + mci.will_topic = lua_tostring(L, -1); + lua_rawgeti(L, LUA_REGISTRYINDEX, mud->conf.will_message_ref); + mci.will_message = lua_tostring(L, -1); + + mqtt_message_t* temp_msg = mqtt_msg_connect(&msgb, &mci); NODE_DBG("Send MQTT connection infomation, data len: %d, d[0]=%d \r\n", temp_msg->length, temp_msg->data[0]); - mud->event_timeout = MQTT_SEND_TIMEOUT; + lua_settop(L, ltop); /* Done with strings, restore the stack */ + // not queue this message. should send right now. or should enqueue this before head. #ifdef CLIENT_SSL_ENABLE - if(mud->secure) + if(mud->conf.flags.secure) { espconn_secure_send(pesp_conn, temp_msg->data, temp_msg->length); } @@ -738,10 +762,14 @@ static void mqtt_socket_connected(void *arg) { espconn_send(pesp_conn, temp_msg->data, temp_msg->length); } - mud->keep_alive_tick = 0; + mud->sending = true; + + os_timer_disarm(&mud->mqttTimer); + os_timer_arm(&mud->mqttTimer, MQTT_SEND_TIMEOUT * 1000, 0); mud->connState = MQTT_CONNECT_SENDING; - NODE_DBG("leave mqtt_socket_connectet, heap = %u.\n", system_get_free_heap_size()); + + NODE_DBG("leave mqtt_socket_connected, heap = %u.\n", system_get_free_heap_size()); return; } @@ -755,70 +783,41 @@ void mqtt_socket_timer(void *arg) if(mud->pesp_conn.proto.tcp == NULL){ NODE_DBG("MQTT not connected\n"); - os_timer_disarm(&mud->mqttTimer); return; } NODE_DBG("timer, queue size: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q))); - if(mud->event_timeout > 0){ - NODE_DBG("event_timeout: %d.\n", mud->event_timeout); - mud->event_timeout --; - if(mud->event_timeout > 0){ - return; - } else { - NODE_DBG("event timeout. \n"); - if(mud->connState == MQTT_DATA) - msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); - // should remove the head of the queue and re-send with DUP = 1 - // Not implemented yet. - } - } - if(mud->connState == MQTT_INIT){ // socket connect time out. - NODE_DBG("Can not connect to broker.\n"); - os_timer_disarm(&mud->mqttTimer); - mqtt_socket_do_disconnect(mud); - mqtt_connack_fail(mud, MQTT_CONN_FAIL_SERVER_NOT_FOUND); - } else if(mud->connState == MQTT_CONNECT_SENDING){ // MQTT_CONNECT send time out. + if(mud->connState == MQTT_CONNECT_SENDING){ // MQTT_CONNECT send time out. NODE_DBG("sSend MQTT_CONNECT failed.\n"); mud->connState = MQTT_INIT; mqtt_socket_do_disconnect(mud); mqtt_connack_fail(mud, MQTT_CONN_FAIL_TIMEOUT_SENDING); - - mud->keep_alive_tick = 0; // not need count anymore } else if(mud->connState == MQTT_CONNECT_SENT) { // wait for CONACK time out. NODE_DBG("MQTT_CONNECT timeout.\n"); mud->connState = MQTT_INIT; mqtt_socket_do_disconnect(mud); mqtt_connack_fail(mud, MQTT_CONN_FAIL_TIMEOUT_RECEIVING); } else if(mud->connState == MQTT_DATA){ - msg_queue_t *pending_msg = msg_peek(&(mud->mqtt_state.pending_msg_q)); - if(pending_msg){ - mqtt_send_if_possible(mud); - } else { + if(!msg_peek(&(mud->mqtt_state.pending_msg_q))) { // no queued event. - mud->keep_alive_tick ++; - if(mud->keep_alive_tick > mud->connect_info.keepalive){ - if (mud->keepalive_sent) { - // Oh dear -- keepalive timer expired and still no ack of previous message - mqtt_socket_reconnected(&mud->pesp_conn, 0); - } else { - uint8_t temp_buffer[MQTT_BUF_SIZE]; - mqtt_message_buffer_t msgb; - mqtt_msg_init(&msgb, temp_buffer, MQTT_BUF_SIZE); - - NODE_DBG("\r\nMQTT: Send keepalive packet\r\n"); - mqtt_message_t* temp_msg = mqtt_msg_pingreq(&msgb); - msg_queue_t *node = msg_enqueue( &(mud->mqtt_state.pending_msg_q), temp_msg, - 0, MQTT_MSG_TYPE_PINGREQ, (int)mqtt_get_qos(temp_msg->data) ); - mud->keepalive_sent = 1; - mud->keep_alive_tick = 0; // Need to reset to zero in case flow control stopped. - mqtt_send_if_possible(mud); - } + if (mud->keepalive_sent) { + // Oh dear -- keepalive timer expired and still no ack of previous message + mqtt_socket_do_disconnect(mud); + } else { + uint8_t temp_buffer[MQTT_BUF_SIZE]; + mqtt_message_buffer_t msgb; + mqtt_msg_init(&msgb, temp_buffer, MQTT_BUF_SIZE); + + NODE_DBG("\r\nMQTT: Send keepalive packet\r\n"); + mqtt_message_t* temp_msg = mqtt_msg_pingreq(&msgb); + msg_queue_t *node = msg_enqueue( &(mud->mqtt_state.pending_msg_q), temp_msg, + 0, MQTT_MSG_TYPE_PINGREQ, (int)mqtt_get_qos(temp_msg->data) ); + mud->keepalive_sent = 1; + mqtt_send_if_possible(mud); } } } - NODE_DBG("keep_alive_tick: %d\n", mud->keep_alive_tick); NODE_DBG("leave mqtt_socket_timer.\n"); } @@ -828,18 +827,7 @@ static int mqtt_socket_client( lua_State* L ) NODE_DBG("enter mqtt_socket_client.\n"); lmqtt_userdata *mud; - char tempid[20] = {0}; - sprintf(tempid, "%s%x", "NodeMCU_", system_get_chip_id() ); - NODE_DBG(tempid); - NODE_DBG("\n"); - - const char *clientId = tempid, *username = NULL, *password = NULL; - size_t idl = strlen(tempid); - size_t unl = 0, pwl = 0; - int keepalive = 0; int stack = 1; - int clean_session = 1; - int max_message_length = 0; int top = lua_gettop(L); // create a object @@ -857,6 +845,12 @@ static int mqtt_socket_client( lua_State* L ) mud->cb_unsuback_ref = LUA_NOREF; mud->cb_puback_ref = LUA_NOREF; + mud->conf.client_id_ref = LUA_NOREF; + mud->conf.username_ref = LUA_NOREF; + mud->conf.password_ref = LUA_NOREF; + mud->conf.will_topic_ref = LUA_NOREF; + mud->conf.will_message_ref = LUA_NOREF; + mud->connState = MQTT_INIT; // set its metatable @@ -865,93 +859,52 @@ static int mqtt_socket_client( lua_State* L ) if( lua_isstring(L,stack) ) // deal with the clientid string { - clientId = luaL_checklstring( L, stack, &idl ); + lua_pushvalue(L, stack); stack++; + } else { + char tempid[20] = {0}; + sprintf(tempid, "%s%x", "NodeMCU_", system_get_chip_id() ); + lua_pushstring(L, tempid); } + mud->conf.client_id_ref = luaL_ref(L, LUA_REGISTRYINDEX); if(lua_isnumber( L, stack )) { - keepalive = luaL_checkinteger( L, stack); + mud->conf.keepalive = luaL_checkinteger( L, stack); stack++; } - - if(keepalive == 0){ - keepalive = MQTT_DEFAULT_KEEPALIVE; + if(mud->conf.keepalive == 0) { + mud->conf.keepalive = MQTT_DEFAULT_KEEPALIVE; } if(lua_isstring( L, stack )){ - username = luaL_checklstring( L, stack, &unl ); + lua_pushvalue(L, stack); + mud->conf.username_ref = luaL_ref(L, LUA_REGISTRYINDEX); stack++; } - if(username == NULL) - unl = 0; - NODE_DBG("length username: %d\r\n", unl); if(lua_isstring( L, stack )){ - password = luaL_checklstring( L, stack, &pwl ); + lua_pushvalue(L, stack); + mud->conf.password_ref = luaL_ref(L, LUA_REGISTRYINDEX); stack++; } - if(password == NULL) - pwl = 0; - NODE_DBG("length password: %d\r\n", pwl); if(lua_isnumber( L, stack )) { - clean_session = luaL_checkinteger( L, stack); + mud->conf.flags.clean_session = !!luaL_checkinteger(L, stack); stack++; } - if(clean_session > 1){ - clean_session = 1; - } - if(lua_isnumber( L, stack )) { - max_message_length = luaL_checkinteger( L, stack); + mud->conf.max_message_length = luaL_checkinteger( L, stack); stack++; } - - if(max_message_length == 0) { - max_message_length = DEFAULT_MAX_MESSAGE_LENGTH; + if(mud->conf.max_message_length == 0) { + mud->conf.max_message_length = DEFAULT_MAX_MESSAGE_LENGTH; } - // TODO: check the zalloc result. - mud->connect_info.client_id = (uint8_t *)calloc(1,idl+1); - mud->connect_info.username = (uint8_t *)calloc(1,unl + 1); - mud->connect_info.password = (uint8_t *)calloc(1,pwl + 1); - if(!mud->connect_info.client_id || !mud->connect_info.username || !mud->connect_info.password){ - if(mud->connect_info.client_id) { - free(mud->connect_info.client_id); - mud->connect_info.client_id = NULL; - } - if(mud->connect_info.username) { - free(mud->connect_info.username); - mud->connect_info.username = NULL; - } - if(mud->connect_info.password) { - free(mud->connect_info.password); - mud->connect_info.password = NULL; - } - return luaL_error(L, "not enough memory"); - } - - memcpy(mud->connect_info.client_id, clientId, idl); - mud->connect_info.client_id[idl] = 0; - memcpy(mud->connect_info.username, username, unl); - mud->connect_info.username[unl] = 0; - memcpy(mud->connect_info.password, password, pwl); - mud->connect_info.password[pwl] = 0; - - NODE_DBG("MQTT: Init info: %s, %s, %s\r\n", mud->connect_info.client_id, mud->connect_info.username, mud->connect_info.password); - - mud->connect_info.clean_session = clean_session; - mud->connect_info.will_qos = 0; - mud->connect_info.will_retain = 0; - mud->connect_info.keepalive = keepalive; - mud->connect_info.max_message_length = max_message_length; - mud->mqtt_state.pending_msg_q = NULL; - mud->mqtt_state.port = 1883; mud->mqtt_state.recv_buffer = NULL; mud->mqtt_state.recv_buffer_size = 0; mud->mqtt_state.recv_buffer_state = MQTT_RECV_NORMAL; @@ -968,11 +921,6 @@ static int mqtt_delete( lua_State* L ) NODE_DBG("enter mqtt_delete.\n"); lmqtt_userdata *mud = (lmqtt_userdata *)luaL_checkudata(L, 1, "mqtt.socket"); - luaL_argcheck(L, mud, 1, "mqtt.socket expected"); - if(mud==NULL){ - NODE_DBG("userdata is nil.\n"); - return 0; - } os_timer_disarm(&mud->mqttTimer); mud->connected = false; @@ -986,18 +934,6 @@ static int mqtt_delete( lua_State* L ) msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); } - // ---- alloc-ed in mqtt_socket_lwt() - if(mud->connect_info.will_topic){ - free(mud->connect_info.will_topic); - mud->connect_info.will_topic = NULL; - } - - if(mud->connect_info.will_message){ - free(mud->connect_info.will_message); - mud->connect_info.will_message = NULL; - } - // ---- - //--------- alloc-ed in mqtt_socket_received() if(mud->mqtt_state.recv_buffer) { free(mud->mqtt_state.recv_buffer); @@ -1005,21 +941,6 @@ static int mqtt_delete( lua_State* L ) } // ---- - //--------- alloc-ed in mqtt_socket_client() - if(mud->connect_info.client_id){ - free(mud->connect_info.client_id); - mud->connect_info.client_id = NULL; - } - if(mud->connect_info.username){ - free(mud->connect_info.username); - mud->connect_info.username = NULL; - } - if(mud->connect_info.password){ - free(mud->connect_info.password); - mud->connect_info.password = NULL; - } - // ------- - // free (unref) callback ref luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_connect_ref); mud->cb_connect_ref = LUA_NOREF; @@ -1038,6 +959,17 @@ static int mqtt_delete( lua_State* L ) luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_puback_ref); mud->cb_puback_ref = LUA_NOREF; + luaL_unref(L, LUA_REGISTRYINDEX, mud->conf.client_id_ref); + mud->conf.client_id_ref = LUA_NOREF; + luaL_unref(L, LUA_REGISTRYINDEX, mud->conf.username_ref); + mud->conf.username_ref = LUA_NOREF; + luaL_unref(L, LUA_REGISTRYINDEX, mud->conf.password_ref); + mud->conf.password_ref = LUA_NOREF; + luaL_unref(L, LUA_REGISTRYINDEX, mud->conf.will_topic_ref); + mud->conf.will_topic_ref = LUA_NOREF; + luaL_unref(L, LUA_REGISTRYINDEX, mud->conf.will_message_ref); + mud->conf.will_message_ref = LUA_NOREF; + int selfref = mud->self_ref; mud->self_ref = LUA_NOREF; luaL_unref(L, LUA_REGISTRYINDEX, mud->self_ref); @@ -1049,14 +981,14 @@ static int mqtt_delete( lua_State* L ) static sint8 mqtt_socket_do_connect(struct lmqtt_userdata *mud) { - NODE_DBG("enter socket_connect.\n"); + NODE_DBG("enter mqtt_socket_do_connect.\n"); sint8 espconn_status; - mud->event_timeout = MQTT_CONNECT_TIMEOUT; mud->connState = MQTT_INIT; #ifdef CLIENT_SSL_ENABLE - if(mud->secure) + if(mud->conf.flags.secure) { + NODE_DBG("mqtt_socket_do_connect using espconn_secure\n"); espconn_status = espconn_secure_connect(&mud->pesp_conn); } else @@ -1065,48 +997,31 @@ static sint8 mqtt_socket_do_connect(struct lmqtt_userdata *mud) espconn_status = espconn_connect(&mud->pesp_conn); } - os_timer_arm(&mud->mqttTimer, 1000, 1); - - NODE_DBG("leave socket_connect\n"); + NODE_DBG("leave mqtt_socket_do_connect espconn_status=%d\n", espconn_status); return espconn_status; } -static sint8 socket_dns_found(const char *name, ip_addr_t *ipaddr, void *arg) +static void socket_dns_found(const char *name, ip_addr_t *ipaddr, void *arg) { lmqtt_userdata *mud = arg; - NODE_DBG("enter socket_dns_found.\n"); - sint8 espconn_status = ESPCONN_OK; - - if(ipaddr == NULL) + if((ipaddr == NULL) || (ipaddr->addr == 0)) { + NODE_DBG("socket_dns_found failure\n"); mqtt_connack_fail(mud, MQTT_CONN_FAIL_DNS); // although not connected, but fire disconnect callback to release every thing. mqtt_socket_disconnected(arg); - return -1; - } - - // ipaddr->addr is a uint32_t ip - if(ipaddr->addr != 0) - { - memcpy(&mud->pesp_conn.proto.tcp->remote_ip, &(ipaddr->addr), 4); - NODE_DBG("TCP ip is set: "); - NODE_DBG(IPSTR, IP2STR(&(ipaddr->addr))); - NODE_DBG("\n"); - espconn_status = mqtt_socket_do_connect(mud); + return; } - NODE_DBG("leave socket_dns_found.\n"); - - return espconn_status; -} + memcpy(&mud->pesp_conn.proto.tcp->remote_ip, &(ipaddr->addr), 4); + NODE_DBG("socket_dns_found success: "); + NODE_DBG(IPSTR, IP2STR(&(ipaddr->addr))); + NODE_DBG("\n"); -/* wrapper for using socket_dns_found() as callback function */ -static void socket_dns_foundcb(const char *name, ip_addr_t *ipaddr, void *arg) -{ - socket_dns_found(name, ipaddr, arg); + mqtt_socket_do_connect(mud); } #include "pm/swtimer.h" @@ -1120,76 +1035,47 @@ static int mqtt_socket_connect( lua_State* L ) ip_addr_t ipaddr; const char *domain; int stack = 1; - unsigned secure = 0; int top = lua_gettop(L); mud = (lmqtt_userdata *)luaL_checkudata(L, stack, "mqtt.socket"); - luaL_argcheck(L, mud, stack, "mqtt.socket expected"); stack++; - if(mud == NULL) - return 0; - - if(mud->connected){ - return luaL_error(L, "already connected"); - } struct espconn *pesp_conn = &mud->pesp_conn; - if (!pesp_conn->proto.tcp) - pesp_conn->proto.tcp = (esp_tcp *)calloc(1,sizeof(esp_tcp)); - - if(!pesp_conn->proto.tcp) { - return luaL_error(L, "not enough memory"); + if(mud->connected){ + return luaL_error(L, "already connected"); } - // reverse is for the callback function - pesp_conn->type = ESPCONN_TCP; - pesp_conn->state = ESPCONN_NONE; mud->connected = false; + mud->sending = false; if( (stack<=top) && lua_isstring(L,stack) ) // deal with the domain string { domain = luaL_checklstring( L, stack, &il ); - + if (!domain) + return luaL_error(L, "invalid domain"); stack++; - if (domain == NULL) - { - domain = "127.0.0.1"; - } - ipaddr.addr = ipaddr_addr(domain); - memcpy(pesp_conn->proto.tcp->remote_ip, &ipaddr.addr, 4); - NODE_DBG("TCP ip is set: "); - NODE_DBG(IPSTR, IP2STR(&ipaddr.addr)); - NODE_DBG("\n"); } if ( (stack<=top) && lua_isnumber(L, stack) ) { port = lua_tointeger(L, stack); stack++; - NODE_DBG("TCP port is set: %d.\n", port); } - pesp_conn->proto.tcp->remote_port = port; - if (pesp_conn->proto.tcp->local_port == 0) - pesp_conn->proto.tcp->local_port = espconn_port(); - mud->mqtt_state.port = port; if ( (stack<=top) && (lua_isnumber(L, stack) || lua_isboolean(L, stack)) ) { if (lua_isnumber(L, stack)) { platform_print_deprecation_note("mqtt.connect secure parameter as integer","in the future"); - secure = !!lua_tointeger(L, stack); + mud->conf.flags.secure = !!lua_tointeger(L, stack); } else { - secure = lua_toboolean(L, stack); + mud->conf.flags.secure = lua_toboolean(L, stack); } stack++; - } else { - secure = 0; // default to 0 } -#ifdef CLIENT_SSL_ENABLE - mud->secure = secure; // save -#else - if ( secure ) + +#ifndef CLIENT_SSL_ENABLE + if ( mud->conf.flags.secure ) { return luaL_error(L, "ssl not available"); } @@ -1211,10 +1097,24 @@ static int mqtt_socket_connect( lua_State* L ) mud->cb_connect_fail_ref = luaL_ref(L, LUA_REGISTRYINDEX); } - lua_pushvalue(L, 1); // copy userdata to the top of stack + if (!pesp_conn->proto.tcp) + pesp_conn->proto.tcp = (esp_tcp *)calloc(1,sizeof(esp_tcp)); + + if(!pesp_conn->proto.tcp) { + return luaL_error(L, "not enough memory"); + } + luaL_unref(L, LUA_REGISTRYINDEX, mud->self_ref); + lua_pushvalue(L, 1); // copy userdata and persist it in the registry mud->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + pesp_conn->type = ESPCONN_TCP; + pesp_conn->state = ESPCONN_NONE; + + pesp_conn->proto.tcp->remote_port = port; + if (pesp_conn->proto.tcp->local_port == 0) + pesp_conn->proto.tcp->local_port = espconn_port(); + espconn_regist_connectcb(pesp_conn, mqtt_socket_connected); espconn_regist_reconcb(pesp_conn, mqtt_socket_reconnected); @@ -1225,24 +1125,17 @@ static int mqtt_socket_connect( lua_State* L ) //My guess: If in doubt, resume the timer // timer started in socket_connect() - if((ipaddr.addr == IPADDR_NONE) && (memcmp(domain,"255.255.255.255",16) != 0)) - { - ip_addr_t host_ip; - switch (dns_gethostbyname(domain, &host_ip, socket_dns_foundcb, mud)) - { - case ERR_OK: - socket_dns_found(domain, &host_ip, mud); // ip is returned in host_ip. - break; - case ERR_INPROGRESS: - break; - default: - // Something has gone wrong; bail out? - mqtt_connack_fail(mud, MQTT_CONN_FAIL_DNS); - } - } - else + ip_addr_t host_ip; + switch (dns_gethostbyname(domain, &host_ip, socket_dns_found, mud)) { - mqtt_socket_do_connect(mud); + case ERR_OK: + socket_dns_found(domain, &host_ip, mud); // ip is returned in host_ip. + break; + case ERR_INPROGRESS: + break; + default: + // Something has gone wrong; bail out? + mqtt_connack_fail(mud, MQTT_CONN_FAIL_DNS); } NODE_DBG("leave mqtt_socket_connect.\n"); @@ -1255,17 +1148,11 @@ static int mqtt_socket_connect( lua_State* L ) static int mqtt_socket_close( lua_State* L ) { NODE_DBG("enter mqtt_socket_close.\n"); - int i = 0; lmqtt_userdata *mud = NULL; mud = (lmqtt_userdata *)luaL_checkudata(L, 1, "mqtt.socket"); - luaL_argcheck(L, mud, 1, "mqtt.socket expected"); - if (mud == NULL) { - lua_pushboolean(L, 0); - return 1; - } - sint8 espconn_status = ESPCONN_CONN; + sint8 espconn_status = 0; if (mud->connected) { uint8_t temp_buffer[MQTT_BUF_SIZE]; mqtt_message_buffer_t msgb; @@ -1275,34 +1162,23 @@ static int mqtt_socket_close( lua_State* L ) mqtt_message_t* temp_msg = mqtt_msg_disconnect(&msgb); NODE_DBG("Send MQTT disconnect infomation, data len: %d, d[0]=%d \r\n", temp_msg->length, temp_msg->data[0]); - /* XXX This fails to actually send the disconnect message before hanging up */ -#ifdef CLIENT_SSL_ENABLE - if(mud->secure) { - espconn_status = espconn_secure_send(&mud->pesp_conn, temp_msg->data, temp_msg->length); - if(mud->pesp_conn.proto.tcp->remote_port || mud->pesp_conn.proto.tcp->local_port) - espconn_status |= espconn_secure_disconnect(&mud->pesp_conn); - } else -#endif - { - espconn_status = espconn_send(&mud->pesp_conn, temp_msg->data, temp_msg->length); - if(mud->pesp_conn.proto.tcp->remote_port || mud->pesp_conn.proto.tcp->local_port) - espconn_status |= espconn_disconnect(&mud->pesp_conn); - } - } - mud->connected = false; + if (!msg_enqueue(&(mud->mqtt_state.pending_msg_q), temp_msg, 0, + MQTT_MSG_TYPE_DISCONNECT, 0)) + goto err; - while (mud->mqtt_state.pending_msg_q) { - msg_destroy(msg_dequeue(&(mud->mqtt_state.pending_msg_q))); + mqtt_send_if_possible(mud); } NODE_DBG("leave mqtt_socket_close.\n"); + return 0; - if (espconn_status == ESPCONN_OK) { - lua_pushboolean(L, 1); - } else { - lua_pushboolean(L, 0); - } - return 1; +err: + /* OOM while trying to build the disconnect message. Fail the connection */ + mqtt_socket_do_disconnect(mud); + + NODE_DBG("leave mqtt_socket_close on error path\n"); + + return 0; } // Lua: mqtt:on( "method", function() ) @@ -1313,47 +1189,51 @@ static int mqtt_socket_on( lua_State* L ) size_t sl; mud = (lmqtt_userdata *)luaL_checkudata(L, 1, "mqtt.socket"); - luaL_argcheck(L, mud, 1, "mqtt.socket expected"); - if(mud==NULL){ - NODE_DBG("userdata is nil.\n"); - return 0; - } - - const char *method = luaL_checklstring( L, 2, &sl ); - if (method == NULL) - return luaL_error( L, "wrong arg type" ); luaL_checktype(L, 3, LUA_TFUNCTION); lua_pushvalue(L, 3); // copy argument (func) to the top of stack - if( sl == 7 && strcmp(method, "connect") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_connect_ref); - mud->cb_connect_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 8 && strcmp(method, "connfail") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_connect_fail_ref); - mud->cb_connect_fail_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 7 && strcmp(method, "offline") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_disconnect_ref); - mud->cb_disconnect_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 7 && strcmp(method, "message") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_message_ref); - mud->cb_message_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 8 && strcmp(method, "overflow") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_overflow_ref); - mud->cb_overflow_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 6 && strcmp(method, "puback") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_puback_ref); - mud->cb_puback_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 6 && strcmp(method, "suback") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_suback_ref); - mud->cb_suback_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else if( sl == 8 && strcmp(method, "unsuback") == 0){ - luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_unsuback_ref); - mud->cb_unsuback_ref = luaL_ref(L, LUA_REGISTRYINDEX); - }else{ - lua_pop(L, 1); - return luaL_error( L, "method not supported" ); + static const char * const cbnames[] = { + "connect", "connfail", "offline", + "message", "overflow", + "puback", "suback", "unsuback", + NULL + }; + switch (luaL_checkoption(L, 2, NULL, cbnames)) { + case 0: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_connect_ref); + mud->cb_connect_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 1: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_connect_fail_ref); + mud->cb_connect_fail_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 2: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_disconnect_ref); + mud->cb_disconnect_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 3: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_message_ref); + mud->cb_message_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 4: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_overflow_ref); + mud->cb_overflow_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 5: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_puback_ref); + mud->cb_puback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 6: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_suback_ref); + mud->cb_suback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; + case 7: + luaL_unref(L, LUA_REGISTRYINDEX, mud->cb_unsuback_ref); + mud->cb_unsuback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + break; } + NODE_DBG("leave mqtt_socket_on.\n"); return 0; } @@ -1369,15 +1249,8 @@ static int mqtt_socket_unsubscribe( lua_State* L ) { lmqtt_userdata *mud; mud = (lmqtt_userdata *) luaL_checkudata( L, stack, "mqtt.socket" ); - luaL_argcheck( L, mud, stack, "mqtt.socket expected" ); stack++; - if(mud==NULL){ - NODE_DBG("userdata is nil.\n"); - lua_pushboolean(L, 0); - return 1; - } - if(!mud->connected){ luaL_error( L, "not connected" ); lua_pushboolean(L, 0); @@ -1451,7 +1324,7 @@ static int mqtt_socket_unsubscribe( lua_State* L ) { msg_id, MQTT_MSG_TYPE_UNSUBSCRIBE, (int)mqtt_get_qos(temp_msg->data) ); NODE_DBG("topic: %s - id: %d - qos: %d, length: %d\n", topic, node->msg_id, node->publish_qos, node->msg.length); - NODE_DBG("msg_size: %d, event_timeout: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q)), mud->event_timeout); + NODE_DBG("msg_size: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q))); sint8 espconn_status = ESPCONN_IF; @@ -1478,15 +1351,8 @@ static int mqtt_socket_subscribe( lua_State* L ) { lmqtt_userdata *mud; mud = (lmqtt_userdata *) luaL_checkudata( L, stack, "mqtt.socket" ); - luaL_argcheck( L, mud, stack, "mqtt.socket expected" ); stack++; - if(mud==NULL){ - NODE_DBG("userdata is nil.\n"); - lua_pushboolean(L, 0); - return 1; - } - if(!mud->connected){ luaL_error( L, "not connected" ); lua_pushboolean(L, 0); @@ -1563,7 +1429,7 @@ static int mqtt_socket_subscribe( lua_State* L ) { msg_id, MQTT_MSG_TYPE_SUBSCRIBE, (int)mqtt_get_qos(temp_msg->data) ); NODE_DBG("topic: %s - id: %d - qos: %d, length: %d\n", topic, node->msg_id, node->publish_qos, node->msg.length); - NODE_DBG("msg_size: %d, event_timeout: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q)), mud->event_timeout); + NODE_DBG("msg_size: %d\n", msg_size(&(mud->mqtt_state.pending_msg_q))); sint8 espconn_status = ESPCONN_IF; @@ -1589,13 +1455,7 @@ static int mqtt_socket_publish( lua_State* L ) uint16_t msg_id = 0; mud = (lmqtt_userdata *)luaL_checkudata(L, stack, "mqtt.socket"); - luaL_argcheck(L, mud, stack, "mqtt.socket expected"); stack++; - if(mud==NULL){ - NODE_DBG("userdata is nil.\n"); - lua_pushboolean(L, 0); - return 1; - } if(!mud->connected){ return luaL_error( L, "not connected" ); @@ -1650,80 +1510,33 @@ static int mqtt_socket_publish( lua_State* L ) return 1; } -// Lua: mqtt:lwt( topic, message, qos, retain, function(client) ) +// Lua: mqtt:lwt( topic, message, [qos, [retain]]) static int mqtt_socket_lwt( lua_State* L ) { NODE_DBG("enter mqtt_socket_lwt.\n"); - uint8_t stack = 1; - size_t topicSize, msgSize; - NODE_DBG("mqtt_socket_lwt.\n"); - lmqtt_userdata *mud = NULL; - const char *lwtTopic, *lwtMsg; - uint8_t lwtQoS, lwtRetain; - mud = (lmqtt_userdata *)luaL_checkudata( L, stack, "mqtt.socket" ); - luaL_argcheck( L, mud, stack, "mqtt.socket expected" ); + lmqtt_userdata *mud = luaL_checkudata( L, 1, "mqtt.socket" ); + luaL_argcheck( L, lua_isstring(L, 2), 2, "need lwt topic"); + luaL_argcheck( L, lua_isstring(L, 3), 3, "need lwt message"); - if(mud == NULL) - return 0; - - stack++; - lwtTopic = luaL_checklstring( L, stack, &topicSize ); - if (lwtTopic == NULL) - { - return luaL_error( L, "need lwt topic"); - } + lua_pushvalue(L, 2); + luaL_reref(L, LUA_REGISTRYINDEX, &mud->conf.will_topic_ref); - stack++; - lwtMsg = luaL_checklstring( L, stack, &msgSize ); - if (lwtMsg == NULL) - { - return luaL_error( L, "need lwt message"); - } - stack++; - if(mud->connect_info.will_topic){ // free the previous one if there is any - free(mud->connect_info.will_topic); - mud->connect_info.will_topic = NULL; - } - if(mud->connect_info.will_message){ - free(mud->connect_info.will_message); - mud->connect_info.will_message = NULL; - } - - mud->connect_info.will_topic = (uint8_t*) calloc(1, topicSize + 1 ); - mud->connect_info.will_message = (uint8_t*) calloc(1, msgSize + 1 ); - if(!mud->connect_info.will_topic || !mud->connect_info.will_message){ - if(mud->connect_info.will_topic){ - free(mud->connect_info.will_topic); - mud->connect_info.will_topic = NULL; - } - if(mud->connect_info.will_message){ - free(mud->connect_info.will_message); - mud->connect_info.will_message = NULL; - } - return luaL_error( L, "not enough memory"); - } - memcpy(mud->connect_info.will_topic, lwtTopic, topicSize); - mud->connect_info.will_topic[topicSize] = 0; - memcpy(mud->connect_info.will_message, lwtMsg, msgSize); - mud->connect_info.will_message[msgSize] = 0; + lua_pushvalue(L, 3); + luaL_reref(L, LUA_REGISTRYINDEX, &mud->conf.will_message_ref); + int stack = 4; if ( lua_isnumber(L, stack) ) { - mud->connect_info.will_qos = lua_tointeger(L, stack); + mud->conf.flags.will_qos = lua_tointeger(L, stack) & 0x3; stack++; } if ( lua_isnumber(L, stack) ) { - mud->connect_info.will_retain = lua_tointeger(L, stack); + mud->conf.flags.will_retain = !!lua_tointeger(L, stack); stack++; } - NODE_DBG("mqtt_socket_lwt: topic: %s, message: %s, qos: %d, retain: %d\n", - mud->connect_info.will_topic, - mud->connect_info.will_message, - mud->connect_info.will_qos, - mud->connect_info.will_retain); NODE_DBG("leave mqtt_socket_lwt.\n"); return 0; } diff --git a/app/modules/pipe.c b/app/modules/pipe.c index 777883526b..c46770af42 100644 --- a/app/modules/pipe.c +++ b/app/modules/pipe.c @@ -435,12 +435,19 @@ static int pipe_reader(lua_State *L) { return 1; } +// return number of records +static int pipe_nrec (lua_State *L) { + lua_pushinteger(L, lua_objlen(L, 1) - 1); + return 1; +} + LROT_BEGIN(pipe_funcs, NULL, 0) LROT_FUNCENTRY( __len, pipe__len ) LROT_FUNCENTRY( __tostring, pipe__tostring ) LROT_FUNCENTRY( read, pipe_read ) LROT_FUNCENTRY( reader, pipe_reader ) LROT_FUNCENTRY( unread, pipe_unread ) + LROT_FUNCENTRY( nrec, pipe_nrec ) LROT_END(pipe_funcs, NULL, 0) /* Using a index func is needed because the write method is at pipe[1] */ diff --git a/app/modules/pixbuf.c b/app/modules/pixbuf.c new file mode 100644 index 0000000000..5ab40a6eea --- /dev/null +++ b/app/modules/pixbuf.c @@ -0,0 +1,723 @@ +#include "module.h" +#include "lauxlib.h" + +#include + +#include "pixbuf.h" +#define PIXBUF_METATABLE "pixbuf.buf" + +#ifndef MIN +#define MIN(a,b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef MAX +#define MAX(a,b) ((a) > (b) ? (a) : (b)) +#endif + +pixbuf *pixbuf_from_lua_arg(lua_State *L, int arg) { + return luaL_checkudata(L, arg, PIXBUF_METATABLE); +} + +pixbuf *pixbuf_opt_from_lua_arg(lua_State *L, int arg) { + return luaL_testudata(L, arg, PIXBUF_METATABLE); +} + +static ssize_t posrelat(ssize_t pos, size_t len) { + /* relative string position: negative means back from end */ + if (pos < 0) + pos += (ssize_t)len + 1; + return MIN(MAX(pos, 1), len); +} + +const size_t pixbuf_channels(pixbuf *p) { + return p->nchan; +} + +const size_t pixbuf_size(pixbuf *p) { + return p->npix * p->nchan; +} + +/* + * Construct a pixbuf newuserdata using C arguments. + * + * Allocates, so may throw! Leaves new buffer at the top of the Lua stack + * and returns a C pointer. + */ +static pixbuf *pixbuf_new(lua_State *L, size_t leds, size_t chans) { + // Allocate memory + + // A crude hack of an overflow check, but unlikely to be reached in practice + if ((leds > 8192) || (chans > 32)) { + luaL_error(L, "pixbuf size limits exeeded"); + return NULL; // UNREACHED + } + + size_t size = sizeof(pixbuf) + leds * chans; + + pixbuf *buffer = (pixbuf*)lua_newuserdata(L, size); + + // Associate its metatable + luaL_getmetatable(L, PIXBUF_METATABLE); + lua_setmetatable(L, -2); + + // Save led strip size + *(size_t *)&buffer->npix = leds; + *(size_t *)&buffer->nchan = chans; + + memset(buffer->values, 0, leds * chans); + + return buffer; +} + +// Handle a buffer where we can store led values +int pixbuf_new_lua(lua_State *L) { + const int leds = luaL_checkint(L, 1); + const int chans = luaL_checkint(L, 2); + + luaL_argcheck(L, leds > 0, 1, "should be a positive integer"); + luaL_argcheck(L, chans > 0, 2, "should be a positive integer"); + + pixbuf_new(L, leds, chans); + return 1; +} + +static int pixbuf_concat_lua(lua_State *L) { + pixbuf *lhs = pixbuf_from_lua_arg(L, 1); + pixbuf *rhs = pixbuf_from_lua_arg(L, 2); + + luaL_argcheck(L, lhs->nchan == rhs->nchan, 1, + "can only concatenate buffers with same channel count"); + + size_t osize = lhs->npix + rhs->npix; + if (lhs->npix > osize) { + return luaL_error(L, "size sum overflow"); + } + + pixbuf *buffer = pixbuf_new(L, osize, lhs->nchan); + + memcpy(buffer->values, lhs->values, pixbuf_size(lhs)); + memcpy(buffer->values + pixbuf_size(lhs), rhs->values, pixbuf_size(rhs)); + + return 1; +} + +static int pixbuf_channels_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + lua_pushinteger(L, buffer->nchan); + return 1; +} + +static int pixbuf_dump_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + lua_pushlstring(L, (char*)buffer->values, pixbuf_size(buffer)); + return 1; +} + +static int pixbuf_eq_lua(lua_State *L) { + bool res; + + pixbuf *lhs = pixbuf_from_lua_arg(L, 1); + pixbuf *rhs = pixbuf_from_lua_arg(L, 2); + + if (lhs->npix != rhs->npix) { + res = false; + } else if (lhs->nchan != rhs->nchan) { + res = false; + } else { + res = true; + for(size_t i = 0; i < pixbuf_size(lhs); i++) { + if(lhs->values[i] != rhs->values[i]) { + res = false; + break; + } + } + } + + lua_pushboolean(L, res); + return 1; +} + +static int pixbuf_fade_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + const int fade = luaL_checkinteger(L, 2); + unsigned direction = luaL_optinteger( L, 3, PIXBUF_FADE_OUT ); + + luaL_argcheck(L, fade > 0, 2, "fade value should be a strictly positive int"); + + uint8_t *p = &buffer->values[0]; + for (size_t i = 0; i < pixbuf_size(buffer); i++) + { + if (direction == PIXBUF_FADE_OUT) + { + *p++ /= fade; + } + else + { + // as fade in can result in value overflow, an int is used to perform the check afterwards + int val = *p * fade; + *p++ = MIN(255, val); + } + } + + return 0; +} + +/* Fade an Ixxx-type strip by just manipulating the I bytes */ +static int pixbuf_fadeI_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + const int fade = luaL_checkinteger(L, 2); + unsigned direction = luaL_optinteger( L, 3, PIXBUF_FADE_OUT ); + + luaL_argcheck(L, fade > 0, 2, "fade value should be a strictly positive int"); + + uint8_t *p = &buffer->values[0]; + for (size_t i = 0; i < buffer->npix; i++, p+=buffer->nchan) { + if (direction == PIXBUF_FADE_OUT) { + *p /= fade; + } else { + int val = *p * fade; + *p++ = MIN(255, val); + } + } + + return 0; +} + +static int pixbuf_fill_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + + if (buffer->npix == 0) { + goto out; + } + + if (lua_gettop(L) != (1 + buffer->nchan)) { + return luaL_argerror(L, 1, "need as many values as colors per pixel"); + } + + /* Fill the first pixel from the Lua stack */ + for (size_t i = 0; i < buffer->nchan; i++) { + buffer->values[i] = luaL_checkinteger(L, 2+i); + } + + /* Fill the rest of the pixels from the first */ + for (size_t i = 1; i < buffer->npix; i++) { + memcpy(&buffer->values[i * buffer->nchan], buffer->values, buffer->nchan); + } + +out: + lua_settop(L, 1); + return 1; +} + +static int pixbuf_get_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + const int led = luaL_checkinteger(L, 2) - 1; + size_t channels = buffer->nchan; + + luaL_argcheck(L, led >= 0 && led < buffer->npix, 2, "index out of range"); + + uint8_t tmp[channels]; + memcpy(tmp, &buffer->values[channels*led], channels); + + for (size_t i = 0; i < channels; i++) + { + lua_pushinteger(L, tmp[i]); + } + + return channels; +} + +/* :map(f, buf1, ilo, ihi, [buf2, ilo2]) */ +static int pixbuf_map_lua(lua_State *L) { + pixbuf *outbuf = pixbuf_from_lua_arg(L, 1); + /* f at index 2 */ + + pixbuf *buffer1 = pixbuf_opt_from_lua_arg(L, 3); + if (!buffer1) + buffer1 = outbuf; + + const int ilo = posrelat(luaL_optinteger(L, 4, 1), buffer1->npix) - 1; + const int ihi = posrelat(luaL_optinteger(L, 5, buffer1->npix), buffer1->npix) - 1; + + luaL_argcheck(L, ihi > ilo, 3, "Buffer limits out of order"); + + size_t npix = ihi - ilo + 1; + + luaL_argcheck(L, npix == outbuf->npix, 1, "Output buffer wrong size"); + + pixbuf *buffer2 = pixbuf_opt_from_lua_arg(L, 6); + const int ilo2 = buffer2 ? posrelat(luaL_optinteger(L, 7, 1), buffer2->npix) - 1 : 0; + + if (buffer2) { + luaL_argcheck(L, ilo2 + npix <= buffer2->npix, 6, "Second buffer too short"); + } + + for (size_t p = 0; p < npix; p++) { + lua_pushvalue(L, 2); + for (size_t c = 0; c < buffer1->nchan; c++) { + lua_pushinteger(L, buffer1->values[(ilo + p) * buffer1->nchan + c]); + } + if (buffer2) { + for (size_t c = 0; c < buffer2->nchan; c++) { + lua_pushinteger(L, buffer2->values[(ilo2 + p) * buffer2->nchan + c]); + } + } + lua_call(L, buffer1->nchan + (buffer2 ? buffer2->nchan : 0), outbuf->nchan); + for (size_t c = 0; c < outbuf->nchan; c++) { + outbuf->values[(p + 1) * outbuf->nchan - c - 1] = luaL_checkinteger(L, -1); + lua_pop(L, 1); + } + } + + lua_settop(L, 1); + return 1; +} + +struct mix_source { + int factor; + const uint8_t *values; +}; + +static uint32_t pixbuf_mix_clamp(int32_t v) { + if (v < 0) { return 0; } + if (v > 255) { return 255; } + return v; +} + +/* This one can sum straightforwardly, channel by channel */ +static void pixbuf_mix_raw(pixbuf *out, size_t n_src, struct mix_source* src) { + size_t cells = pixbuf_size(out); + + for (size_t c = 0; c < cells; c++) { + int32_t val = 0; + for (size_t s = 0; s < n_src; s++) { + val += (int32_t)src[s].values[c] * src[s].factor; + } + + val += 128; // rounding instead of floor + val /= 256; // do not use implemetation dependant right shift + + out->values[c] = (uint8_t)pixbuf_mix_clamp(val); + } +} + +/* Mix intensity-mediated three-color pixbufs. + * + * XXX This is untested in real hardware; do they actually behave like this? + */ +static void pixbuf_mix_i3(pixbuf *out, size_t ibits, size_t n_src, + struct mix_source* src) { + for(size_t p = 0; p < out->npix; p++) { + int32_t sums[3] = { 0, 0, 0 }; + + for (size_t s = 0; s < n_src; s++) { + for (size_t c = 0; c < 3; c++) { + sums[c] += (int32_t)src[s].values[4*p+c+1] // color channel + * src[s].values[4*p] // global intensity + * src[s].factor; // user factor + + } + } + + uint32_t pmaxc = 0; + for (size_t c = 0; c < 3; c++) { + pmaxc = sums[c] > pmaxc ? sums[c] : pmaxc; + } + + size_t maxgi; + if (pmaxc == 0) { + /* Zero value */ + memset(&out->values[4*p], 0, 4); + return; + } else if (pmaxc <= (1 << 16)) { + /* Minimum global factor */ + maxgi = 1; + } else if (pmaxc >= ((1 << ibits) - 1) << 16) { + /* Maximum global factor */ + maxgi = (1 << ibits) - 1; + } else { + maxgi = (pmaxc >> 16) + 1; + } + + // printf("mixi3: %x %x %x -> %x, %zx\n", sums[0], sums[1], sums[2], pmaxc, maxgi); + + out->values[4*p] = maxgi; + for (size_t c = 0; c < 3; c++) { + out->values[4*p+c+1] = pixbuf_mix_clamp((sums[c] + 256 * maxgi - 127) / (256 * maxgi)); + } + } +} + +// buffer:mix(factor1, buffer1, ..) +// factor is 256 for 100% +// uses saturating arithmetic (one buffer at a time) +static int pixbuf_mix_core(lua_State *L, size_t ibits) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + pixbuf *src_buffer; + + int pos = 2; + size_t n_sources = (lua_gettop(L) - 1) / 2; + struct mix_source sources[n_sources]; + + if (n_sources == 0) { + lua_settop(L, 1); + return 1; + } + + for (size_t src = 0; src < n_sources; src++, pos += 2) { + int factor = luaL_checkinteger(L, pos); + src_buffer = pixbuf_from_lua_arg(L, pos + 1); + + luaL_argcheck(L, src_buffer->npix == buffer->npix && + src_buffer->nchan == buffer->nchan, + pos + 1, "buffer not same size or shape"); + + sources[src].factor = factor; + sources[src].values = src_buffer->values; + } + + if (ibits != 0) { + luaL_argcheck(L, src_buffer->nchan == 4, 2, "Requires 4 channel pixbuf"); + pixbuf_mix_i3(buffer, ibits, n_sources, sources); + } else { + pixbuf_mix_raw(buffer, n_sources, sources); + } + + lua_settop(L, 1); + return 1; +} + +static int pixbuf_mix_lua(lua_State *L) { + return pixbuf_mix_core(L, 0); +} + +static int pixbuf_mix4I5_lua(lua_State *L) { + return pixbuf_mix_core(L, 5); +} + + +// Returns the total of all channels +static int pixbuf_power_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + + int total = 0; + size_t p = 0; + for (size_t i = 0; i < buffer->npix; i++) { + for (size_t j = 0; j < buffer->nchan; j++, p++) { + total += buffer->values[p]; + } + } + + lua_pushinteger(L, total); + return 1; +} + +// Returns the total of all channels, intensity-style +static int pixbuf_powerI_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + + int total = 0; + size_t p = 0; + for (size_t i = 0; i < buffer->npix; i++) { + int inten = buffer->values[p++]; + for (size_t j = 0; j < buffer->nchan - 1; j++, p++) { + total += inten * buffer->values[p]; + } + } + + lua_pushinteger(L, total); + return 1; +} + +static int pixbuf_replace_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + ptrdiff_t start = posrelat(luaL_optinteger(L, 3, 1), buffer->npix); + size_t channels = buffer->nchan; + + uint8_t *src; + size_t srcLen; + + if (lua_type(L, 2) == LUA_TSTRING) { + size_t length; + src = (uint8_t *) lua_tolstring(L, 2, &length); + srcLen = length / channels; + } else { + pixbuf *rhs = pixbuf_from_lua_arg(L, 2); + luaL_argcheck(L, rhs->nchan == buffer->nchan, 2, "buffers have different channels"); + src = rhs->values; + srcLen = rhs->npix; + } + + luaL_argcheck(L, srcLen + start - 1 <= buffer->npix, 2, "does not fit into destination"); + + memcpy(buffer->values + (start - 1) * channels, src, srcLen * channels); + + return 0; +} + +static int pixbuf_set_lua(lua_State *L) { + + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + const int led = luaL_checkinteger(L, 2) - 1; + const size_t channels = buffer->nchan; + + luaL_argcheck(L, led >= 0 && led < buffer->npix, 2, "index out of range"); + + int type = lua_type(L, 3); + if(type == LUA_TTABLE) + { + for (size_t i = 0; i < channels; i++) + { + lua_rawgeti(L, 3, i+1); + buffer->values[channels*led+i] = lua_tointeger(L, -1); + lua_pop(L, 1); + } + } + else if(type == LUA_TSTRING) + { + size_t len; + const char *buf = lua_tolstring(L, 3, &len); + + // Overflow check + if( channels*led + len > channels*buffer->npix ) { + return luaL_error(L, "string size will exceed strip length"); + } + if ( len % channels != 0 ) { + return luaL_error(L, "string does not contain whole LEDs"); + } + + memcpy(&buffer->values[channels*led], buf, len); + } + else + { + luaL_argcheck(L, lua_gettop(L) <= 2 + channels, 2 + channels, + "extra values given"); + + for (size_t i = 0; i < channels; i++) + { + buffer->values[channels*led+i] = luaL_checkinteger(L, 3+i); + } + } + + lua_settop(L, 1); + return 1; +} + +static void pixbuf_shift_circular(pixbuf *buffer, struct pixbuf_shift_params *sp) { + /* Move a buffer of pixels per iteration; loop repeatedly if needed */ + uint8_t tmpbuf[32]; + uint8_t *v = buffer->values; + size_t shiftRemaining = sp->shift; + size_t cursor = sp->offset; + + do { + size_t shiftNow = MIN(shiftRemaining, sizeof tmpbuf); + + if (sp->shiftLeft) { + memcpy(tmpbuf, &v[cursor], shiftNow); + memmove(&v[cursor], &v[cursor+shiftNow], sp->window - shiftNow); + memcpy(&v[cursor+sp->window-shiftNow], tmpbuf, shiftNow); + } else { + memcpy(tmpbuf, &v[cursor+sp->window-shiftNow], shiftNow); + memmove(&v[cursor+shiftNow], &v[cursor], sp->window - shiftNow); + memcpy(&v[cursor], tmpbuf, shiftNow); + } + + cursor += shiftNow; + shiftRemaining -= shiftNow; + } while(shiftRemaining > 0); +} + +static void pixbuf_shift_logical(pixbuf *buffer, struct pixbuf_shift_params *sp) { + /* Logical shifts don't require a temporary buffer, so we just move bytes */ + uint8_t *v = buffer->values; + + if (sp->shiftLeft) { + memmove(&v[sp->offset], &v[sp->offset+sp->shift], sp->window - sp->shift); + bzero(&v[sp->offset+sp->window-sp->shift], sp->shift); + } else { + memmove(&v[sp->offset+sp->shift], &v[sp->offset], sp->window - sp->shift); + bzero(&v[sp->offset], sp->shift); + } +} + +void pixbuf_shift(pixbuf *b, struct pixbuf_shift_params *sp) { +#if 0 + printf("Pixbuf %p shifting %s %s by %zd from %zd with window %zd\n", + b, + sp->shiftLeft ? "left" : "right", + sp->type == PIXBUF_SHIFT_LOGICAL ? "logically" : "circularly", + sp->shift, sp->offset, sp->window); +#endif + + switch(sp->type) { + case PIXBUF_SHIFT_LOGICAL: return pixbuf_shift_logical(b, sp); + case PIXBUF_SHIFT_CIRCULAR: return pixbuf_shift_circular(b, sp); + } +} + +int pixbuf_shift_lua(lua_State *L) { + struct pixbuf_shift_params sp; + + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + const int shift_shift = luaL_checkinteger(L, 2) * buffer->nchan; + const unsigned shift_type = luaL_optinteger(L, 3, PIXBUF_SHIFT_LOGICAL); + const int pos_start = posrelat(luaL_optinteger(L, 4, 1), buffer->npix); + const int pos_end = posrelat(luaL_optinteger(L, 5, -1), buffer->npix); + + if (shift_shift < 0) { + sp.shiftLeft = true; + sp.shift = -shift_shift; + } else { + sp.shiftLeft = false; + sp.shift = shift_shift; + } + + switch(shift_type) { + case PIXBUF_SHIFT_LOGICAL: + case PIXBUF_SHIFT_CIRCULAR: + sp.type = shift_type; + break; + default: + return luaL_argerror(L, 3, "invalid shift type"); + } + + if (pos_start < 1) { + return luaL_argerror(L, 4, "start position must be >= 1"); + } + + if (pos_end < pos_start) { + return luaL_argerror(L, 5, "end position must be >= start"); + } + + sp.offset = (pos_start - 1) * buffer->nchan; + sp.window = (pos_end - pos_start + 1) * buffer->nchan; + + if (sp.shift > pixbuf_size(buffer)) { + return luaL_argerror(L, 2, "shifting more elements than buffer size"); + } + + if (sp.shift > sp.window) { + return luaL_argerror(L, 2, "shifting more than sliced window"); + } + + pixbuf_shift(buffer, &sp); + + return 0; +} + +/* XXX for backwards-compat with ws2812_effects; deprecated and should be removed */ +void pixbuf_prepare_shift(pixbuf *buffer, struct pixbuf_shift_params *sp, + int shift, enum pixbuf_shift type, int start, int end) +{ + start = posrelat(start, buffer->npix); + end = posrelat(end, buffer->npix); + + lua_assert((end > start) && (start > 0) && (end < buffer->npix)); + + sp->type = type; + sp->offset = (start - 1) * buffer->nchan; + sp->window = (end - start + 1) * buffer->nchan; + + if (shift < 0) { + sp->shiftLeft = true; + sp->shift = -shift * buffer->nchan; + } else { + sp->shiftLeft = false; + sp->shift = shift * buffer->nchan; + } +} + +static int pixbuf_size_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + lua_pushinteger(L, buffer->npix); + return 1; +} + +static int pixbuf_sub_lua(lua_State *L) { + pixbuf *lhs = pixbuf_from_lua_arg(L, 1); + size_t l = lhs->npix; + ssize_t start = posrelat(luaL_checkinteger(L, 2), l); + ssize_t end = posrelat(luaL_optinteger(L, 3, -1), l); + if (start <= end) { + pixbuf *result = pixbuf_new(L, end - start + 1, lhs->nchan); + memcpy(result->values, lhs->values + lhs->nchan * (start - 1), + lhs->nchan * (end - start + 1)); + return 1; + } else { + pixbuf_new(L, 0, lhs->nchan); + return 1; + } +} + +static int pixbuf_tostring_lua(lua_State *L) { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + + luaL_Buffer result; + luaL_buffinit(L, &result); + + luaL_addchar(&result, '['); + int p = 0; + for (size_t i = 0; i < buffer->npix; i++) { + if (i > 0) { + luaL_addchar(&result, ','); + } + luaL_addchar(&result, '('); + for (size_t j = 0; j < buffer->nchan; j++, p++) { + if (j > 0) { + luaL_addchar(&result, ','); + } + char numbuf[5]; + sprintf(numbuf, "%d", buffer->values[p]); + luaL_addstring(&result, numbuf); + } + luaL_addchar(&result, ')'); + } + + luaL_addchar(&result, ']'); + luaL_pushresult(&result); + + return 1; +} + +LROT_BEGIN(pixbuf_map, NULL, LROT_MASK_INDEX | LROT_MASK_EQ) + LROT_TABENTRY ( __index, pixbuf_map ) + LROT_FUNCENTRY( __eq, pixbuf_eq_lua ) + + LROT_FUNCENTRY( __concat, pixbuf_concat_lua ) + LROT_FUNCENTRY( __tostring, pixbuf_tostring_lua ) + + LROT_FUNCENTRY( channels, pixbuf_channels_lua ) + LROT_FUNCENTRY( dump, pixbuf_dump_lua ) + LROT_FUNCENTRY( fade, pixbuf_fade_lua ) + LROT_FUNCENTRY( fadeI, pixbuf_fadeI_lua ) + LROT_FUNCENTRY( fill, pixbuf_fill_lua ) + LROT_FUNCENTRY( get, pixbuf_get_lua ) + LROT_FUNCENTRY( replace, pixbuf_replace_lua ) + LROT_FUNCENTRY( map, pixbuf_map_lua ) + LROT_FUNCENTRY( mix, pixbuf_mix_lua ) + LROT_FUNCENTRY( mix4I5, pixbuf_mix4I5_lua ) + LROT_FUNCENTRY( power, pixbuf_power_lua ) + LROT_FUNCENTRY( powerI, pixbuf_powerI_lua ) + LROT_FUNCENTRY( set, pixbuf_set_lua ) + LROT_FUNCENTRY( shift, pixbuf_shift_lua ) + LROT_FUNCENTRY( size, pixbuf_size_lua ) + LROT_FUNCENTRY( sub, pixbuf_sub_lua ) +LROT_END(pixbuf_map, NULL, LROT_MASK_INDEX | LROT_MASK_EQ) + +LROT_BEGIN(pixbuf, NULL, 0) + LROT_NUMENTRY( FADE_IN, PIXBUF_FADE_IN ) + LROT_NUMENTRY( FADE_OUT, PIXBUF_FADE_OUT ) + + LROT_NUMENTRY( SHIFT_CIRCULAR, PIXBUF_SHIFT_CIRCULAR ) + LROT_NUMENTRY( SHIFT_LOGICAL, PIXBUF_SHIFT_LOGICAL ) + + LROT_FUNCENTRY( newBuffer, pixbuf_new_lua ) +LROT_END(pixbuf, NULL, 0) + +int luaopen_pixbuf(lua_State *L) { + luaL_rometatable(L, PIXBUF_METATABLE, LROT_TABLEREF(pixbuf_map)); + lua_pushrotable(L, LROT_TABLEREF(pixbuf)); + return 1; +} + +NODEMCU_MODULE(PIXBUF, "pixbuf", pixbuf, luaopen_pixbuf); diff --git a/app/modules/pixbuf.h b/app/modules/pixbuf.h new file mode 100644 index 0000000000..6b7ff27d28 --- /dev/null +++ b/app/modules/pixbuf.h @@ -0,0 +1,50 @@ +#ifndef APP_MODULES_PIXBUF_H_ +#define APP_MODULES_PIXBUF_H_ + +typedef struct pixbuf { + const size_t npix; + const size_t nchan; + + /* Flexible Array Member; true size is npix * pixbuf_channels_for(type) */ + uint8_t values[]; +} pixbuf; + +enum pixbuf_fade { + PIXBUF_FADE_IN, + PIXBUF_FADE_OUT +}; + +enum pixbuf_shift { + PIXBUF_SHIFT_LOGICAL, + PIXBUF_SHIFT_CIRCULAR +}; + +pixbuf *pixbuf_from_lua_arg(lua_State *, int); +const size_t pixbuf_size(pixbuf *); + +// Exported for backwards compat with ws2812 module +int pixbuf_new_lua(lua_State *); + +/* + * WS2812_EFFECTS does pixbuf manipulation directly in C, which isn't the + * intended use case, but for backwards compat, we export just what it needs. + * Move this struct to pixbuf.c and mark these exports static instead once + * WS2812_EFFECTS is no more. + */ +struct pixbuf_shift_params { + enum pixbuf_shift type; + // 0 <= offset <= buffer length + size_t offset; + // 0 <= window + offset <= buffer length + size_t window; + // 0 <= shift <= window_size + size_t shift; + bool shiftLeft; +}; +void pixbuf_shift(pixbuf *, struct pixbuf_shift_params *); +void pixbuf_prepare_shift(pixbuf *, struct pixbuf_shift_params *, + int val, enum pixbuf_shift, int start, int end); +const size_t pixbuf_channels(pixbuf *); +/* end WS2812_EFFECTS exports */ + +#endif diff --git a/app/modules/softuart.c b/app/modules/softuart.c index 39559fec20..0980636c4c 100644 --- a/app/modules/softuart.c +++ b/app/modules/softuart.c @@ -194,6 +194,7 @@ static int softuart_init(softuart_t *s) } return platform_gpio_register_intr_hook(mask, softuart_intr_handler); } + return 1; } @@ -206,30 +207,27 @@ static int softuart_setup(lua_State *L) NODE_DBG("[SoftUART]: setup called\n"); baudrate = (uint32_t)luaL_checkinteger(L, 1); // Get Baudrate from luaL_argcheck(L, (baudrate > 0 && baudrate < 230400), 1, "Invalid baud rate"); - lua_remove(L, 1); // Remove baudrate argument from stack - if (lua_gettop(L) == 2) { // 2 arguments: 1st can be nil - if (lua_isnil(L, 1)) { - tx_gpio_id = 0xFF; - } else { - tx_gpio_id = (uint8_t)luaL_checkinteger(L, 1); - luaL_argcheck(L, (platform_gpio_exists(tx_gpio_id) && tx_gpio_id != 0) - , 2, "Invalid SoftUART tx GPIO"); - } - rx_gpio_id = (uint8_t)luaL_checkinteger(L, 2); + + if (lua_isnoneornil(L, 2)) { + tx_gpio_id = 0xFF; + } else { + tx_gpio_id = (uint8_t)luaL_checkinteger(L, 2); + luaL_argcheck(L, (platform_gpio_exists(tx_gpio_id) && tx_gpio_id != 0) + , 2, "Invalid SoftUART tx GPIO"); + } + + if (lua_isnoneornil(L, 3)) { + rx_gpio_id = 0xFF; + } else { + rx_gpio_id = (uint8_t)luaL_checkinteger(L, 3); luaL_argcheck(L, (platform_gpio_exists(rx_gpio_id) && rx_gpio_id != 0) , 3, "Invalid SoftUART rx GPIO"); luaL_argcheck(L, softuart_gpio_instances[rx_gpio_id] == NULL , 3, "SoftUART rx already configured on the pin"); + } - } else if (lua_gettop(L) == 1) { // 1 argument: transmit part only - rx_gpio_id = 0xFF; - tx_gpio_id = (uint8_t)luaL_checkinteger(L, 1); - luaL_argcheck(L, (platform_gpio_exists(tx_gpio_id) && tx_gpio_id != 0) - , 2, "Invalid SoftUART tx GPIO"); - } else { - // SoftUART object without receive and transmit part would be useless - return luaL_error(L, "Not enough arguments"); - } + // SoftUART object without receive and transmit part would be useless + if ((rx_gpio_id == 0xFF) && (tx_gpio_id == 0xFF)) {return luaL_error(L, "Not enough arguments");} softuart = (softuart_t*)lua_newuserdata(L, sizeof(softuart_t)); softuart->pin_rx = rx_gpio_id; diff --git a/app/modules/somfy.c b/app/modules/somfy.c index 7b31a679b0..b3c81b9e7e 100644 --- a/app/modules/somfy.c +++ b/app/modules/somfy.c @@ -12,18 +12,37 @@ //#define NODE_DEBUG #include -#include "os_type.h" -#include "osapi.h" -#include "sections.h" - #include "module.h" #include "lauxlib.h" -#include "lmem.h" #include "platform.h" #include "task/task.h" #include "hw_timer.h" #include "user_interface.h" +#ifdef LUA_USE_MODULES_SOMFY +#if !defined(GPIO_INTERRUPT_ENABLE) || !defined(GPIO_INTERRUPT_HOOK_ENABLE) +#error Must have GPIO_INTERRUPT and GPIO_INTERRUPT_HOOK if using SOMFY module +#endif +#endif + +#ifdef NODE_DEBUG + #define PULLUP PLATFORM_GPIO_PULLUP + #define OUTPUT PLATFORM_GPIO_OUTPUT + #define HIGH PLATFORM_GPIO_HIGH + #define LOW PLATFORM_GPIO_LOW + + #define MODE_TP1 platform_gpio_mode( 3, OUTPUT, PULLUP ); // GPIO 00 + #define SET_TP1 platform_gpio_write(3, HIGH); + #define CLR_TP1 platform_gpio_write(3, LOW); + #define WAIT os_delay_us(1); +#else + #define MODE_TP1 + #define SET_TP1 + #define CLR_TP1 + #define WAIT +#endif + + #define SYMBOL 640 // symbol width in microseconds #define SOMFY_UP 0x2 #define SOMFY_STOP 0x1 @@ -33,24 +52,27 @@ #define DIRECT_WRITE_LOW(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 0)) #define DIRECT_WRITE_HIGH(pin) (GPIO_OUTPUT_SET(GPIO_ID_PIN(pin_num[pin]), 1)) +// ----------------------------------------------------------------------------------------------------// +// ------------------------------- transmitter part ---------------------------------------------------// +// ----------------------------------------------------------------------------------------------------// static const os_param_t TIMER_OWNER = 0x736f6d66; // "somf" -static task_handle_t done_taskid; +static task_handle_t SendDone_taskid; -static uint8_t pin; +static uint8_t TxPin; static uint8_t frame[7]; static uint8_t sync; static uint8_t repeat; -//static uint32_t delay[10] = {9415, 89565, 4*SYMBOL, 4*SYMBOL, 4*SYMBOL, 4550, SYMBOL, SYMBOL, SYMBOL, 30415}; // in us +//static uint32_t delay[10] = {9415, 89565, 4*SYMBOL, 4*SYMBOL, 4*SYMBOL, 4550, SYMBOL, SYMBOL, SYMBOL, 30415}; // inc us // the `delay` array of constants must be in RAM as it is accessed from the timer interrupt -static const RAM_CONST_SECTION_ATTR uint32_t delay[10] = {US_TO_RTC_TIMER_TICKS(9415), US_TO_RTC_TIMER_TICKS(89565), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4550), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(30415)}; // in ticks (no need to recalculate) +static const uint32_t delay[10] = {US_TO_RTC_TIMER_TICKS(9415), US_TO_RTC_TIMER_TICKS(89565), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4*SYMBOL), US_TO_RTC_TIMER_TICKS(4550), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(SYMBOL), US_TO_RTC_TIMER_TICKS(30415)}; // in ticks (no need to recalculate) static uint8_t repeatindex; static uint8_t signalindex; static uint8_t subindex; static uint8_t bitcondition; -int lua_done_ref; // callback when transmission is done +static int lua_done_ref = LUA_NOREF; // callback when transmission is done void buildFrame(uint8_t *frame, uint64_t remote, uint8_t button, uint16_t code) { // NODE_DBG("remote: %x\n", remote); @@ -86,15 +108,6 @@ void buildFrame(uint8_t *frame, uint64_t remote, uint8_t button, uint16_t code) // NODE_DBG("Obfuscated:\t\t%02x %02x %02x %02x %02x %02x %02x\n", frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6]); } -static void somfy_transmissionDone (task_param_t arg) -{ - lua_State *L = lua_getstate(); - lua_rawgeti (L, LUA_REGISTRYINDEX, lua_done_ref); - luaL_unref (L, LUA_REGISTRYINDEX, lua_done_ref); - lua_done_ref = LUA_NOREF; - luaL_pcallx (L, 0, 0); -} - static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { (void) p; // NODE_DBG("%d\t%d\n", signalindex, subindex); @@ -103,7 +116,7 @@ static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { subindex = 0; if(sync == 2) { // Only with the first frame. //Wake-up pulse & Silence - DIRECT_WRITE_HIGH(pin); + DIRECT_WRITE_HIGH(TxPin); signalindex++; // delayMicroseconds(9415); break; @@ -112,7 +125,7 @@ static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { } case 1: //Wake-up pulse & Silence - DIRECT_WRITE_LOW(pin); + DIRECT_WRITE_LOW(TxPin); signalindex++; // delayMicroseconds(89565); break; @@ -122,24 +135,24 @@ static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { // a "useless" step to allow repeating the hardware sync w/o the silence after wake-up pulse case 3: // Hardware sync: two sync for the first frame, seven for the following ones. - DIRECT_WRITE_HIGH(pin); + DIRECT_WRITE_HIGH(TxPin); signalindex++; // delayMicroseconds(4*SYMBOL); break; case 4: - DIRECT_WRITE_LOW(pin); + DIRECT_WRITE_LOW(TxPin); subindex++; if (subindex < sync) {signalindex--;} else {signalindex++;} // delayMicroseconds(4*SYMBOL); break; case 5: // Software sync - DIRECT_WRITE_HIGH(pin); + DIRECT_WRITE_HIGH(TxPin); signalindex++; // delayMicroseconds(4550); break; case 6: - DIRECT_WRITE_LOW(pin); + DIRECT_WRITE_LOW(TxPin); signalindex++; subindex=0; // delayMicroseconds(SYMBOL); @@ -148,10 +161,10 @@ static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { //Data: bits are sent one by one, starting with the MSB. bitcondition = ((frame[subindex/8] >> (7 - (subindex%8))) & 1) == 1; if(bitcondition) { - DIRECT_WRITE_LOW(pin); + DIRECT_WRITE_LOW(TxPin); } else { - DIRECT_WRITE_HIGH(pin); + DIRECT_WRITE_HIGH(TxPin); } signalindex++; // delayMicroseconds(SYMBOL); @@ -159,10 +172,10 @@ static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { case 8: //Data: bits are sent one by one, starting with the MSB. if(bitcondition) { - DIRECT_WRITE_HIGH(pin); + DIRECT_WRITE_HIGH(TxPin); } else { - DIRECT_WRITE_LOW(pin); + DIRECT_WRITE_LOW(TxPin); } if (subindex<56) { @@ -175,19 +188,19 @@ static void ICACHE_RAM_ATTR sendCommand(os_param_t p) { // delayMicroseconds(SYMBOL); break; case 9: - DIRECT_WRITE_LOW(pin); + DIRECT_WRITE_LOW(TxPin); signalindex++; // delayMicroseconds(30415); // Inter-frame silence break; case 10: repeatindex++; if (repeatindex ignore + return ret_gpio_status; // abort IRQ + } + lastMicros = actMicros; + + switch(SomfyRx.status) { + case waiting_synchro: + if (bitMicros > tempo_synchro_hw_min && bitMicros < tempo_synchro_hw_max) { + SET_TP1 WAIT CLR_TP1 WAIT SET_TP1 + ++SomfyRx.cpt_synchro_hw; + CLR_TP1 + } + else if (bitMicros > tempo_synchro_sw_min && bitMicros < tempo_synchro_sw_max && SomfyRx.cpt_synchro_hw >= 4) { + SET_TP1 //WAIT CLR_TP1 WAIT SET_TP1 WAIT CLR_TP1 WAIT SET_TP1 WAIT CLR_TP1 WAIT SET_TP1 + memset( &SomfyRx, 0, sizeof( SomfyRx) ); + SomfyRx.status = receiving_data; + } else { + SomfyRx.cpt_synchro_hw = 0; + } + break; + + case receiving_data: + if (bitMicros > tempo_symbol_min && bitMicros < tempo_symbol_max && !SomfyRx.waiting_half_symbol) { + SET_TP1 + SomfyRx.previous_bit = 1 - SomfyRx.previous_bit; + SomfyRx.payload[SomfyRx.cpt_bits/8] += SomfyRx.previous_bit << (7 - SomfyRx.cpt_bits%8); + ++SomfyRx.cpt_bits; + } else if (bitMicros > tempo_half_symbol_min && bitMicros < tempo_half_symbol_max) { + SET_TP1 WAIT CLR_TP1 WAIT SET_TP1 WAIT CLR_TP1 WAIT SET_TP1 + if (SomfyRx.waiting_half_symbol) { + SomfyRx.waiting_half_symbol = false; + SomfyRx.payload[SomfyRx.cpt_bits/8] += SomfyRx.previous_bit << (7 - SomfyRx.cpt_bits%8); + ++SomfyRx.cpt_bits; + } else { + SomfyRx.waiting_half_symbol = true; + } + } else { + SomfyRx.cpt_synchro_hw = 0; + SomfyRx.status = waiting_synchro; + } + CLR_TP1 + break; + + default: + break; + } + + if (SomfyRx.status == receiving_data && SomfyRx.cpt_bits == 80) { //56) { experiment + task_post_high(DataReady_taskid, (task_param_t)0); + SomfyRx.status = waiting_synchro; + } + + return ret_gpio_status; +} + +static void somfy_decode (os_param_t param, uint8_t prio) +{ + #ifdef NODE_DEBUG + NODE_DBG("Payload:\t"); + for(uint8_t i = 0; i < 10; i++) { + NODE_DBG("%02x ", SomfyRx.payload[i]); + } + NODE_DBG("\n"); + #endif + + // Deobfuscation + uint8_t frame[10]; + frame[0] = SomfyRx.payload[0]; + for(int i = 1; i < 7; ++i) frame[i] = SomfyRx.payload[i] ^ SomfyRx.payload[i-1]; + + frame[7] = SomfyRx.payload[7] ^ SomfyRx.payload[0]; + for(int i = 8; i < 10; ++i) frame[i] = SomfyRx.payload[i] ^ SomfyRx.payload[i-1]; + + #ifdef NODE_DEBUG + NODE_DBG("Frame:\t"); + for(uint8_t i = 0; i < 10; i++) { + NODE_DBG("%02x ", frame[i]); + } + NODE_DBG("\n"); + #endif + + // Checksum check + uint8_t cksum = 0; + for(int i = 0; i < 7; ++i) cksum = cksum ^ frame[i] ^ (frame[i] >> 4); + cksum = cksum & 0x0F; + if (cksum != 0) { + NODE_DBG("Checksum incorrect!\n"); + return; + } + + unsigned long rolling_code = (frame[2] << 8) || frame[3]; + unsigned long address = ((unsigned long)frame[4] << 16) || (frame[5] << 8) || frame[6]; + + if (lua_dataready_ref == LUA_NOREF) + return; + lua_State *L = lua_getstate(); + lua_rawgeti(L, LUA_REGISTRYINDEX, lua_dataready_ref); + lua_pushinteger(L, address); + lua_pushinteger(L, frame[1] >> 4); + lua_pushinteger(L, rolling_code); + lua_pushlstring(L, frame, 10); + luaL_pcallx(L, 4, 0); +} + + +// ----------------------------------------------------------------------------------------------------// +// ------------------------------- Lua part -----------------------------------------------------------// +// ----------------------------------------------------------------------------------------------------// +static inline void register_lua_cb(lua_State* L, int* cb_ref){ + int ref=luaL_ref(L, LUA_REGISTRYINDEX); + if( *cb_ref != LUA_NOREF){ + luaL_unref(L, LUA_REGISTRYINDEX, *cb_ref); + } + *cb_ref = ref; +} + +static inline void unregister_lua_cb(lua_State* L, int* cb_ref){ + if(*cb_ref != LUA_NOREF){ + luaL_unref(L, LUA_REGISTRYINDEX, *cb_ref); + *cb_ref = LUA_NOREF; + } +} + +int somfy_lua_listen(lua_State* L) { // pin, callback + NODE_DBG("[somfy_lua_listen]\n"); + +#if LUA_VERSION_NUM == 501 + if (lua_isnumber(L, 1) && lua_type(L, 2) == LUA_TFUNCTION) { +#else + if (lua_isinteger(L, 1) && lua_type(L, 2) == LUA_TFUNCTION) { +#endif + RxPin = luaL_checkinteger(L, 1); + luaL_argcheck(L, platform_gpio_exists(RxPin) && RxPin>0, 1, "Invalid interrupt pin"); + lua_pushvalue(L, 2); + register_lua_cb(L, &lua_dataready_ref); + + memset( &SomfyRx, 0, sizeof( SomfyRx) ); + IntBitmask = 1 << pin_num[RxPin]; + + MODE_TP1 + NODE_DBG("[somfy_lua_listen] Enabling interrupt on PIN %d\n", RxPin); + platform_gpio_mode(RxPin, PLATFORM_GPIO_INT, PLATFORM_GPIO_PULLUP); + NODE_DBG("[somfy_lua_listen] platform_gpio_register_intr_hook - pin: %d, mask: %d\n", RxPin, IntBitmask); + platform_gpio_register_intr_hook(IntBitmask, InterruptHandler); + + gpio_pin_intr_state_set(GPIO_ID_PIN(pin_num[RxPin]), GPIO_PIN_INTR_ANYEDGE); + +#if LUA_VERSION_NUM == 501 + } else if ((lua_isnoneornil(L, 1) || lua_isnumber(L, 1)) && lua_isnoneornil(L, 2)) { +#else + } else if ((lua_isnoneornil(L, 1) || lua_isinteger(L, 1)) && lua_isnoneornil(L, 2)) { +#endif + NODE_DBG("[somfy_lua_listen] Desabling interrupt on PIN %d\n", RxPin); + platform_gpio_mode(RxPin, PLATFORM_GPIO_INPUT, PLATFORM_GPIO_PULLUP); + + unregister_lua_cb(L, &lua_dataready_ref); + RxPin = 0; + } else { + luaL_error(L, "Invalid parameters"); + } + return 0; +} + +static void somfy_transmissionDone (task_param_t arg) +{ + lua_State *L = lua_getstate(); + lua_rawgeti (L, LUA_REGISTRYINDEX, lua_done_ref); + unregister_lua_cb (L, &lua_done_ref); + luaL_pcallx (L, 0, 0); +} + +int somfy_lua_sendcommand(lua_State* L) { // pin, remote, command, rolling_code, num_repeat, callback + TxPin = luaL_checkinteger(L, 1); uint64_t remote = luaL_checkinteger(L, 2); uint8_t cmd = luaL_checkinteger(L, 3); uint16_t code = luaL_checkinteger(L, 4); repeat=luaL_optint( L, 5, 2 ); - luaL_argcheck(L, platform_gpio_exists(pin), 1, "Invalid pin"); + luaL_argcheck(L, platform_gpio_exists(TxPin), 1, "Invalid pin"); - luaL_unref(L, LUA_REGISTRYINDEX, lua_done_ref); - if (!lua_isnoneornil(L, 6)) { - lua_pushvalue(L, 6); - lua_done_ref = luaL_ref(L, LUA_REGISTRYINDEX); + if (lua_type(L, 6) == LUA_TFUNCTION) { + lua_pushvalue (L, 6); + register_lua_cb (L, &lua_done_ref); } else { - lua_done_ref = LUA_NOREF; + unregister_lua_cb (L, &lua_done_ref); } - MOD_CHECK_ID(gpio, pin); - platform_gpio_mode(pin, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP); + MOD_CHECK_ID(gpio, TxPin); + platform_gpio_mode(TxPin, PLATFORM_GPIO_OUTPUT, PLATFORM_GPIO_PULLUP); buildFrame(frame, remote, cmd, code); @@ -233,18 +476,21 @@ static int somfy_lua_sendcommand(lua_State* L) { // pin, remote, command, rollin return 0; } +int luaopen_somfy( lua_State *L ) { + SendDone_taskid = task_get_id((task_callback_t) somfy_transmissionDone); + DataReady_taskid = task_get_id((task_callback_t) somfy_decode); + return 0; +} + +// Module function map LROT_BEGIN(somfy, NULL, 0) + LROT_FUNCENTRY( sendcommand, somfy_lua_sendcommand ) + LROT_FUNCENTRY( listen, somfy_lua_listen ) + LROT_NUMENTRY( UP, SOMFY_UP ) LROT_NUMENTRY( DOWN, SOMFY_DOWN ) LROT_NUMENTRY( PROG, SOMFY_PROG ) LROT_NUMENTRY( STOP, SOMFY_STOP ) - LROT_FUNCENTRY( sendcommand, somfy_lua_sendcommand ) LROT_END(somfy, NULL, 0) - -int luaopen_somfy( lua_State *L ) { - done_taskid = task_get_id((task_callback_t) somfy_transmissionDone); - return 0; -} - NODEMCU_MODULE(SOMFY, "somfy", somfy, luaopen_somfy); diff --git a/app/modules/tm1829.c b/app/modules/tm1829.c index 0b1713b24a..30ff6a13e3 100644 --- a/app/modules/tm1829.c +++ b/app/modules/tm1829.c @@ -5,6 +5,8 @@ #include #include "user_interface.h" +#include "pixbuf.h" + static inline uint32_t _getCycleCount(void) { uint32_t cycles; __asm__ __volatile__("rsr %0,ccount":"=a" (cycles)); @@ -13,8 +15,9 @@ static inline uint32_t _getCycleCount(void) { // This algorithm reads the cpu clock cycles to calculate the correct // pulse widths. It works in both 80 and 160 MHz mode. -static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, uint8_t *pixels, uint32_t length) { - uint8_t *p, *end; +static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, const uint8_t *pixels, size_t length) { + const uint8_t *p, *end; + uint8_t phasergb = 0; p = pixels; end = p + length; @@ -28,6 +31,13 @@ static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, uint8_t *pixels, ui register int i; register uint8_t pixel = *p++; + if ((phasergb == 0) && (pixel == 0xFF)) { + // clamp initial byte value to avoid constant-current shenanigans. Yuck! + pixel = 0xFE; + } + if (++phasergb == 3) { + phasergb = 0; + } ets_intr_lock(); @@ -55,35 +65,27 @@ static void ICACHE_RAM_ATTR tm1829_write_to_pin(uint8_t pin, uint8_t *pixels, ui } // Lua: tm1829.write(pin, "string") -// Byte triples in the string are interpreted as R G B values and sent to the hardware as G R B. -// WARNING: this function scrambles the input buffer : -// a = string.char(255,0,128) -// tm1829.write(3,a) -// =a.byte() -// (0,255,128) +// Byte triples in the string are interpreted as GRB values. static int ICACHE_FLASH_ATTR tm1829_write(lua_State* L) { const uint8_t pin = luaL_checkinteger(L, 1); + const uint8_t *pixels; size_t length; - const char *rgb = luaL_checklstring(L, 2, &length); - - // dont modify lua-internal lstring - make a copy instead - char *buffer = (char *)malloc(length); - // Ignore incomplete Byte triples at the end of buffer - length -= length % 3; - - // Copy payload and make sure first byte is < 0xFF (triggers - // constant current command, instead of PWM duty command) - size_t i; - for (i = 0; i < length; i += 3) { - buffer[i] = rgb[i]; - buffer[i + 1] = rgb[i + 1]; - buffer[i + 2] = rgb[i + 2]; - - // Check for first byte - if (buffer[i] == 0xff) - buffer[i] = 0xfe; + switch(lua_type(L, 3)) { + case LUA_TSTRING: { + pixels = luaL_checklstring(L, 2, &length); + break; + } + case LUA_TUSERDATA: { + pixbuf *buffer = pixbuf_from_lua_arg(L, 2); + luaL_argcheck(L, pixbuf_channels(buffer) == 3, 2, "Bad pixbuf format"); + pixels = buffer->values; + length = 3 * buffer->npix; + break; + } + default: + return luaL_argerror(L, 2, "String or pixbuf expected"); } // Initialize the output pin and wait a bit @@ -91,12 +93,10 @@ static int ICACHE_FLASH_ATTR tm1829_write(lua_State* L) platform_gpio_write(pin, 1); // Send the buffer - tm1829_write_to_pin(pin_num[pin], (uint8_t*) buffer, length); + tm1829_write_to_pin(pin_num[pin], pixels, length); os_delay_us(500); // reset time - free(buffer); - return 0; } diff --git a/app/modules/tmr.c b/app/modules/tmr.c index b47af1c44a..8e52708f86 100644 --- a/app/modules/tmr.c +++ b/app/modules/tmr.c @@ -30,11 +30,11 @@ static const uint32 MAX_TIMEOUT=MAX_TIMEOUT_DEF; static const char* MAX_TIMEOUT_ERR_STR = "Range: 1-"STRINGIFY(MAX_TIMEOUT_DEF); typedef struct{ - os_timer_t os; - sint32_t lua_ref; /* Reference to registered callback function */ - sint32_t self_ref; /* Reference to UD registered slot */ - uint32_t interval; - uint8_t mode; + os_timer_t os; + sint32_t lua_ref; /* Reference to registered callback function */ + sint32_t self_ref; /* Reference to UD registered slot */ + uint32_t interval; + uint8_t mode; } tmr_t; // The previous implementation extended the rtc counter to 64 bits, and then @@ -56,47 +56,47 @@ static sint32_t soft_watchdog = -1; static os_timer_t rtc_timer; static void alarm_timer_common(void* arg){ - tmr_t *tmr = (tmr_t *) arg; - if(tmr->lua_ref > 0) { - lua_State* L = lua_getstate(); - lua_rawgeti(L, LUA_REGISTRYINDEX, tmr->lua_ref); - lua_rawgeti(L, LUA_REGISTRYINDEX, tmr->self_ref); - if (tmr->mode != TIMER_MODE_AUTO) { - if(tmr->mode == TIMER_MODE_SINGLE) { - luaL_unref2(L, LUA_REGISTRYINDEX, tmr->lua_ref); - luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); - tmr->mode = TIMER_MODE_OFF; - } else if (tmr->mode == TIMER_MODE_SEMI) { - tmr->mode |= TIMER_IDLE_FLAG; - luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); - } - } - luaL_pcallx(L, 1, 0); - } + tmr_t *tmr = (tmr_t *) arg; + if(tmr->lua_ref > 0) { + lua_State* L = lua_getstate(); + lua_rawgeti(L, LUA_REGISTRYINDEX, tmr->lua_ref); + lua_rawgeti(L, LUA_REGISTRYINDEX, tmr->self_ref); + if (tmr->mode != TIMER_MODE_AUTO) { + if(tmr->mode == TIMER_MODE_SINGLE) { + luaL_unref2(L, LUA_REGISTRYINDEX, tmr->lua_ref); + luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); + tmr->mode = TIMER_MODE_OFF; + } else if (tmr->mode == TIMER_MODE_SEMI) { + tmr->mode |= TIMER_IDLE_FLAG; + luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); + } + } + luaL_pcallx(L, 1, 0); + } } // Lua: tmr.delay( us ) static int tmr_delay( lua_State* L ){ - sint32_t us = luaL_checkinteger(L, 1); - luaL_argcheck(L, us>0, 1, "wrong arg range"); - while(us > 0){ - os_delay_us(us >= 1000000 ? 1000000 : us); - system_soft_wdt_feed (); - us -= 1000000; - } - return 0; + sint32_t us = luaL_checkinteger(L, 1); + luaL_argcheck(L, us>0, 1, "wrong arg range"); + while(us > 0){ + os_delay_us(us >= 1000000 ? 1000000 : us); + system_soft_wdt_feed (); + us -= 1000000; + } + return 0; } // Lua: tmr.now() , return system timer in us static int tmr_now(lua_State* L){ - lua_pushinteger(L, (uint32_t) (0x7FFFFFFF & system_get_time())); - return 1; + lua_pushinteger(L, (uint32_t) (0x7FFFFFFF & system_get_time())); + return 1; } // Lua: tmr.ccount() , returns CCOUNT register static int tmr_ccount(lua_State* L){ - lua_pushinteger(L, CCOUNT_REG); - return 1; + lua_pushinteger(L, CCOUNT_REG); + return 1; } /* @@ -106,69 +106,69 @@ static int tmr_ccount(lua_State* L){ // Lua: t:register( interval, mode, function ) static int tmr_register(lua_State* L) { - tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); - uint32_t interval = luaL_checkinteger(L, 2); - uint8_t mode = luaL_checkinteger(L, 3); - - luaL_argcheck(L, (interval > 0 && interval <= MAX_TIMEOUT), 2, MAX_TIMEOUT_ERR_STR); - luaL_argcheck(L, (mode == TIMER_MODE_SINGLE || mode == TIMER_MODE_SEMI || mode == TIMER_MODE_AUTO), 3, "Invalid mode"); - luaL_argcheck(L, lua_isfunction(L, 4), 4, "Must be function"); - - //get the lua function reference - lua_pushvalue(L, 4); - if(!(tmr->mode & TIMER_IDLE_FLAG) && tmr->mode != TIMER_MODE_OFF) - os_timer_disarm(&tmr->os); - luaL_reref(L, LUA_REGISTRYINDEX, &tmr->lua_ref); - tmr->mode = mode|TIMER_IDLE_FLAG; - tmr->interval = interval; - os_timer_setfn(&tmr->os, alarm_timer_common, tmr); - return 0; + tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); + uint32_t interval = luaL_checkinteger(L, 2); + uint8_t mode = luaL_checkinteger(L, 3); + + luaL_argcheck(L, (interval > 0 && interval <= MAX_TIMEOUT), 2, MAX_TIMEOUT_ERR_STR); + luaL_argcheck(L, (mode == TIMER_MODE_SINGLE || mode == TIMER_MODE_SEMI || mode == TIMER_MODE_AUTO), 3, "Invalid mode"); + luaL_argcheck(L, lua_isfunction(L, 4), 4, "Must be function"); + + //get the lua function reference + lua_pushvalue(L, 4); + if(!(tmr->mode & TIMER_IDLE_FLAG) && tmr->mode != TIMER_MODE_OFF) + os_timer_disarm(&tmr->os); + luaL_reref(L, LUA_REGISTRYINDEX, &tmr->lua_ref); + tmr->mode = mode|TIMER_IDLE_FLAG; + tmr->interval = interval; + os_timer_setfn(&tmr->os, alarm_timer_common, tmr); + return 0; } // Lua: t:start( [restart] ) static int tmr_start(lua_State* L){ - tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); - lua_settop(L, 2); - luaL_argcheck(L, lua_isboolean(L, 2) || lua_isnil(L, 2), 2, "boolean expected"); - int restart = lua_toboolean(L, 2); - - lua_settop(L, 1); /* we need to have userdata on top of the stack */ - if (tmr->self_ref == LUA_NOREF) - tmr->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); - - //we return false if the timer is not idle and is not to be restarted - int idle = tmr->mode&TIMER_IDLE_FLAG; - if(!(idle || restart)){ - lua_pushboolean(L, false); - }else{ - if (!idle) {os_timer_disarm(&tmr->os);} - tmr->mode &= ~TIMER_IDLE_FLAG; - os_timer_arm(&tmr->os, tmr->interval, tmr->mode==TIMER_MODE_AUTO); - lua_pushboolean(L, true); - } - return 1; + tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); + lua_settop(L, 2); + luaL_argcheck(L, lua_isboolean(L, 2) || lua_isnil(L, 2), 2, "boolean expected"); + int restart = lua_toboolean(L, 2); + + lua_settop(L, 1); /* we need to have userdata on top of the stack */ + if (tmr->self_ref == LUA_NOREF) + tmr->self_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + //we return false if the timer is not idle and is not to be restarted + int idle = tmr->mode&TIMER_IDLE_FLAG; + if(!(idle || restart)){ + lua_pushboolean(L, false); + }else{ + if (!idle) {os_timer_disarm(&tmr->os);} + tmr->mode &= ~TIMER_IDLE_FLAG; + os_timer_arm(&tmr->os, tmr->interval, tmr->mode==TIMER_MODE_AUTO); + lua_pushboolean(L, true); + } + return 1; } // Lua: t:alarm( interval, repeat, function ) static int tmr_alarm(lua_State* L){ - tmr_register(L); - /* remove tmr.alarm's other then the 1st UD parameters from Lua stack. - tmr.start expects UD and optional restart parameter. */ - lua_settop(L, 1); - return tmr_start(L); + tmr_register(L); + /* remove tmr.alarm's other then the 1st UD parameters from Lua stack. + tmr.start expects UD and optional restart parameter. */ + lua_settop(L, 1); + return tmr_start(L); } // Lua: t:stop() static int tmr_stop(lua_State* L){ - tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); - int idle = tmr->mode == TIMER_MODE_OFF || (tmr->mode & TIMER_IDLE_FLAG); - luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); - - if(!idle) - os_timer_disarm(&tmr->os); - tmr->mode |= TIMER_IDLE_FLAG; - lua_pushboolean(L, !idle); /* return false if the timer is idle (or not registered) */ - return 1; + tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); + int idle = tmr->mode == TIMER_MODE_OFF || (tmr->mode & TIMER_IDLE_FLAG); + luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); + + if(!idle) + os_timer_disarm(&tmr->os); + tmr->mode |= TIMER_IDLE_FLAG; + lua_pushboolean(L, !idle); /* return false if the timer is idle (or not registered) */ + return 1; } #ifdef TIMER_SUSPEND_ENABLE @@ -179,174 +179,173 @@ static int tmr_stop(lua_State* L){ #define tmr_suspend_all tmr_suspend_removed #define tmr_resume_all tmr_suspend_removed static int tmr_suspend_removed(lua_State* L){ - return luaL_error(L, TMR_SUSPEND_REMOVED_MSG); + return luaL_error(L, TMR_SUSPEND_REMOVED_MSG); } #endif // Lua: t:unregister() static int tmr_unregister(lua_State* L){ - tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); - luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); - luaL_unref2(L, LUA_REGISTRYINDEX, tmr->lua_ref); - if(!(tmr->mode & TIMER_IDLE_FLAG) && tmr->mode != TIMER_MODE_OFF) - os_timer_disarm(&tmr->os); - tmr->mode = TIMER_MODE_OFF; - return 0; + tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); + luaL_unref2(L, LUA_REGISTRYINDEX, tmr->self_ref); + luaL_unref2(L, LUA_REGISTRYINDEX, tmr->lua_ref); + if(!(tmr->mode & TIMER_IDLE_FLAG) && tmr->mode != TIMER_MODE_OFF) + os_timer_disarm(&tmr->os); + tmr->mode = TIMER_MODE_OFF; + return 0; } // Lua: t:interval( interval ) static int tmr_interval(lua_State* L){ - tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); - uint32_t interval = luaL_checkinteger(L, 2); - luaL_argcheck(L, (interval > 0 && interval <= MAX_TIMEOUT), 2, MAX_TIMEOUT_ERR_STR); - if(tmr->mode != TIMER_MODE_OFF){ - tmr->interval = interval; - if(!(tmr->mode&TIMER_IDLE_FLAG)){ - os_timer_disarm(&tmr->os); - os_timer_arm(&tmr->os, tmr->interval, tmr->mode==TIMER_MODE_AUTO); - } - } - return 0; + tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); + uint32_t interval = luaL_checkinteger(L, 2); + luaL_argcheck(L, (interval > 0 && interval <= MAX_TIMEOUT), 2, MAX_TIMEOUT_ERR_STR); + if(tmr->mode != TIMER_MODE_OFF){ + tmr->interval = interval; + if(!(tmr->mode&TIMER_IDLE_FLAG)){ + os_timer_disarm(&tmr->os); + os_timer_arm(&tmr->os, tmr->interval, tmr->mode==TIMER_MODE_AUTO); + } + } + return 0; } // Lua: t:state() static int tmr_state(lua_State* L){ - tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); - if(tmr->mode == TIMER_MODE_OFF){ - lua_pushnil(L); - return 1; - } - - lua_pushboolean(L, (tmr->mode & TIMER_IDLE_FLAG) == 0); - lua_pushinteger(L, tmr->mode & (~TIMER_IDLE_FLAG)); - return 2; + tmr_t *tmr = (tmr_t *) luaL_checkudata(L, 1, "tmr.timer"); + if(tmr->mode == TIMER_MODE_OFF){ + lua_pushnil(L); + return 1; + } + + lua_pushboolean(L, (tmr->mode & TIMER_IDLE_FLAG) == 0); + lua_pushinteger(L, tmr->mode & (~TIMER_IDLE_FLAG)); + return 2; } // Lua: tmr.wdclr() static int tmr_wdclr( lua_State* L ){ - system_soft_wdt_feed (); - return 0; + system_soft_wdt_feed (); + return 0; } // The on ESP8266 system_rtc_clock_cali_proc() returns a fixed point value // (12 bit fraction part), giving how many rtc clock ticks represent 1us. // The high 64 bits of the uint64_t multiplication are not needed) static uint32_t rtc2usec(uint64_t rtc){ - return (rtc*rtc_time_cali)>>12; + return (rtc*rtc_time_cali)>>12; } // This returns the number of microseconds uptime. Note that it relies on // the rtc clock, which is notoriously temperature dependent inline static uint64_t rtc_timer_update(bool do_calibration){ - if (do_calibration || rtc_time_cali==0) - rtc_time_cali=system_rtc_clock_cali_proc(); - - uint32_t current = system_get_rtc_time(); - uint32_t since_last=current-last_rtc_time; // This will transparently deal with wraparound - uint32_t us_since_last=rtc2usec(since_last); - uint64_t now=last_rtc_time_us+us_since_last; - - // Only update if at least 100ms has passed since we last updated. - // This prevents the rounding errors in rtc2usec from accumulating - if (us_since_last>=100000){ - last_rtc_time=current; - last_rtc_time_us=now; - } - return now; + if (do_calibration || rtc_time_cali==0) + rtc_time_cali=system_rtc_clock_cali_proc(); + + uint32_t current = system_get_rtc_time(); + uint32_t since_last=current-last_rtc_time; // This will transparently deal with wraparound + uint32_t us_since_last=rtc2usec(since_last); + uint64_t now=last_rtc_time_us+us_since_last; + + // Only update if at least 100ms has passed since we last updated. + // This prevents the rounding errors in rtc2usec from accumulating + if (us_since_last>=100000){ + last_rtc_time=current; + last_rtc_time_us=now; + } + return now; } void rtc_callback(void *arg){ - rtc_timer_update(true); - if(soft_watchdog > 0){ - soft_watchdog--; - if(soft_watchdog == 0) - system_restart(); - } + rtc_timer_update(true); + if(soft_watchdog >= 0){ + soft_watchdog--; + if(soft_watchdog < 0) + system_restart(); + } } // Lua: tmr.time() , return rtc time in second static int tmr_time( lua_State* L ){ - uint64_t us=rtc_timer_update(false); - lua_pushinteger(L, us/1000000); - return 1; + uint64_t us=rtc_timer_update(false); + lua_pushinteger(L, us/1000000); + return 1; } // Lua: tmr.softwd( value ) static int tmr_softwd( lua_State* L ){ - int t = luaL_checkinteger(L, 1); - luaL_argcheck(L, t>0 , 2, "invalid time"); - soft_watchdog = t; - return 0; + soft_watchdog = luaL_checkinteger(L, 1); + // NO check is required as negative Values mean that the timer is disabled. + return 0; } // Lua: tmr.create() static int tmr_create( lua_State *L ) { - tmr_t *ud = (tmr_t *)lua_newuserdata(L, sizeof(*ud)); - luaL_getmetatable(L, "tmr.timer"); - lua_setmetatable(L, -2); - *ud = (tmr_t) {{0}, LUA_NOREF, LUA_NOREF, 0, TIMER_MODE_OFF}; - return 1; + tmr_t *ud = (tmr_t *)lua_newuserdata(L, sizeof(*ud)); + luaL_getmetatable(L, "tmr.timer"); + lua_setmetatable(L, -2); + *ud = (tmr_t) {{0}, LUA_NOREF, LUA_NOREF, 0, TIMER_MODE_OFF}; + return 1; } // Module function map LROT_BEGIN(tmr_dyn, NULL, LROT_MASK_GC_INDEX) - LROT_FUNCENTRY( __gc, tmr_unregister ) - LROT_TABENTRY( __index, tmr_dyn ) - LROT_FUNCENTRY( register, tmr_register ) - LROT_FUNCENTRY( alarm, tmr_alarm ) - LROT_FUNCENTRY( start, tmr_start ) - LROT_FUNCENTRY( stop, tmr_stop ) - LROT_FUNCENTRY( unregister, tmr_unregister ) - LROT_FUNCENTRY( state, tmr_state ) - LROT_FUNCENTRY( interval, tmr_interval ) + LROT_FUNCENTRY( __gc, tmr_unregister ) + LROT_TABENTRY( __index, tmr_dyn ) + LROT_FUNCENTRY( register, tmr_register ) + LROT_FUNCENTRY( alarm, tmr_alarm ) + LROT_FUNCENTRY( start, tmr_start ) + LROT_FUNCENTRY( stop, tmr_stop ) + LROT_FUNCENTRY( unregister, tmr_unregister ) + LROT_FUNCENTRY( state, tmr_state ) + LROT_FUNCENTRY( interval, tmr_interval ) #ifdef TIMER_SUSPEND_ENABLE - LROT_FUNCENTRY( suspend, tmr_suspend ) - LROT_FUNCENTRY( resume, tmr_resume ) + LROT_FUNCENTRY( suspend, tmr_suspend ) + LROT_FUNCENTRY( resume, tmr_resume ) #endif LROT_END(tmr_dyn, NULL, LROT_MASK_GC_INDEX) LROT_BEGIN(tmr, NULL, 0) - LROT_FUNCENTRY( delay, tmr_delay ) - LROT_FUNCENTRY( now, tmr_now ) - LROT_FUNCENTRY( wdclr, tmr_wdclr ) - LROT_FUNCENTRY( softwd, tmr_softwd ) - LROT_FUNCENTRY( time, tmr_time ) - LROT_FUNCENTRY( ccount, tmr_ccount ) + LROT_FUNCENTRY( delay, tmr_delay ) + LROT_FUNCENTRY( now, tmr_now ) + LROT_FUNCENTRY( wdclr, tmr_wdclr ) + LROT_FUNCENTRY( softwd, tmr_softwd ) + LROT_FUNCENTRY( time, tmr_time ) + LROT_FUNCENTRY( ccount, tmr_ccount ) #ifdef TIMER_SUSPEND_ENABLE - LROT_FUNCENTRY( suspend_all, tmr_suspend_all ) - LROT_FUNCENTRY( resume_all, tmr_resume_all ) + LROT_FUNCENTRY( suspend_all, tmr_suspend_all ) + LROT_FUNCENTRY( resume_all, tmr_resume_all ) #endif - LROT_FUNCENTRY( create, tmr_create ) - LROT_NUMENTRY( ALARM_SINGLE, TIMER_MODE_SINGLE ) - LROT_NUMENTRY( ALARM_SEMI, TIMER_MODE_SEMI ) - LROT_NUMENTRY( ALARM_AUTO, TIMER_MODE_AUTO ) + LROT_FUNCENTRY( create, tmr_create ) + LROT_NUMENTRY( ALARM_SINGLE, TIMER_MODE_SINGLE ) + LROT_NUMENTRY( ALARM_SEMI, TIMER_MODE_SEMI ) + LROT_NUMENTRY( ALARM_AUTO, TIMER_MODE_AUTO ) LROT_END(tmr, NULL, 0) #include "pm/swtimer.h" int luaopen_tmr( lua_State *L ){ - luaL_rometatable(L, "tmr.timer", LROT_TABLEREF(tmr_dyn)); + luaL_rometatable(L, "tmr.timer", LROT_TABLEREF(tmr_dyn)); - last_rtc_time=system_get_rtc_time(); // Right now is time 0 - last_rtc_time_us=0; + last_rtc_time=system_get_rtc_time(); // Right now is time 0 + last_rtc_time_us=0; - os_timer_disarm(&rtc_timer); - os_timer_setfn(&rtc_timer, rtc_callback, NULL); - os_timer_arm(&rtc_timer, 1000, 1); + os_timer_disarm(&rtc_timer); + os_timer_setfn(&rtc_timer, rtc_callback, NULL); + os_timer_arm(&rtc_timer, 1000, 1); - // The function rtc_callback calls the a function that calibrates the SoftRTC - // for drift in the esp8266's clock. My guess: after the duration of light_sleep - // there is bound to be some drift in the clock, so a calibration is due. - SWTIMER_REG_CB(rtc_callback, SWTIMER_RESUME); + // The function rtc_callback calls the a function that calibrates the SoftRTC + // for drift in the esp8266's clock. My guess: after the duration of light_sleep + // there is bound to be some drift in the clock, so a calibration is due. + SWTIMER_REG_CB(rtc_callback, SWTIMER_RESUME); - // The function alarm_timer_common handles timers created by the developer via - // tmr.create(). No reason not to resume the timers, so resume em'. - SWTIMER_REG_CB(alarm_timer_common, SWTIMER_RESUME); + // The function alarm_timer_common handles timers created by the developer via + // tmr.create(). No reason not to resume the timers, so resume em'. + SWTIMER_REG_CB(alarm_timer_common, SWTIMER_RESUME); - return 0; + return 0; } NODEMCU_MODULE(TMR, "tmr", tmr, luaopen_tmr); diff --git a/app/modules/ws2801.c b/app/modules/ws2801.c index 45bc90a7a6..7354554f81 100644 --- a/app/modules/ws2801.c +++ b/app/modules/ws2801.c @@ -5,6 +5,8 @@ #include #include "osapi.h" +#include "pixbuf.h" + /** * Code is based on https://github.com/CHERTS/esp8266-devkit/blob/master/Espressif/examples/EspLightNode/user/ws2801.c * and provides a similar api as the ws2812 module. @@ -110,13 +112,28 @@ static int ICACHE_FLASH_ATTR ws2801_init_lua(lua_State* L) { */ static int ICACHE_FLASH_ATTR ws2801_writergb(lua_State* L) { size_t length; - const char *buffer = luaL_checklstring(L, 1, &length); + const uint8_t *values; + + switch(lua_type(L,1)) { + case LUA_TSTRING: + values = (const uint8_t*) luaL_checklstring(L, 1, &length); + break; + case LUA_TUSERDATA: { + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + luaL_argcheck(L, buffer->nchan == 3, 1, "Pixbuf not 3-channel"); + values = buffer->values; + length = pixbuf_size(buffer); + break; + } + default: + return luaL_argerror(L, 1, "pixbuf or string expected"); + } os_delay_us(10); ets_intr_lock(); - ws2801_strip(buffer, length); + ws2801_strip(values, length); ets_intr_unlock(); diff --git a/app/modules/ws2812.c b/app/modules/ws2812.c index ca54b2cd09..536b972e78 100644 --- a/app/modules/ws2812.c +++ b/app/modules/ws2812.c @@ -8,14 +8,13 @@ #include "user_interface.h" #include "driver/uart.h" #include "osapi.h" +#include "cpu_esp8266_irq.h" -#include "ws2812.h" +#include "pixbuf.h" -#define CANARY_VALUE 0x32383132 #define MODE_SINGLE 0 #define MODE_DUAL 1 - // Init UART1 to be able to stream WS2812 data to GPIO2 pin // If DUAL mode is selected, init UART0 to stream to TXD0 as well // You HAVE to redirect LUA's output somewhere else @@ -53,12 +52,18 @@ static int ws2812_init(lua_State* L) { return 0; } -// Stream data using UART1 routed to GPIO2 -// ws2812.init() should be called first -// -// NODE_DEBUG should not be activated because it also uses UART1 -void ICACHE_RAM_ATTR ws2812_write_data(const uint8_t *pixels, uint32_t length, const uint8_t *pixels2, uint32_t length2) { +static bool +ws2812_can_write(int uart) +{ + // If something to send for first buffer and enough room + // in FIFO buffer (we wants to write 4 bytes, so less than + // 124 in the buffer) + return (((READ_PERI_REG(UART_STATUS(uart)) >> UART_TXFIFO_CNT_S) & UART_TXFIFO_CNT) <= 124); +} +static void +ws2812_write_byte(int uart, uint8_t value) +{ // Data are sent LSB first, with a start bit at 0, an end bit at 1 and all inverted // 0b00110111 => 110111 => [0]111011[1] => 10001000 => 00 // 0b00000111 => 000111 => [0]111000[1] => 10001110 => 01 @@ -68,31 +73,37 @@ void ICACHE_RAM_ATTR ws2812_write_data(const uint8_t *pixels, uint32_t length, c // But declared in ".data" section to avoid read penalty from FLASH static const __attribute__((section(".data._uartData"))) uint8_t _uartData[4] = { 0b00110111, 0b00000111, 0b00110100, 0b00000100 }; + WRITE_PERI_REG(UART_FIFO(uart), _uartData[(value >> 6) & 3]); + WRITE_PERI_REG(UART_FIFO(uart), _uartData[(value >> 4) & 3]); + WRITE_PERI_REG(UART_FIFO(uart), _uartData[(value >> 2) & 3]); + WRITE_PERI_REG(UART_FIFO(uart), _uartData[(value >> 0) & 3]); +} + +// Stream data using UART1 routed to GPIO2 +// ws2812.init() should be called first +// +// NODE_DEBUG should not be activated because it also uses UART1 +void ICACHE_RAM_ATTR ws2812_write_data(const uint8_t *pixels, uint32_t length, const uint8_t *pixels2, uint32_t length2) { const uint8_t *end = pixels + length; const uint8_t *end2 = pixels2 + length2; - do { - // If something to send for first buffer and enough room - // in FIFO buffer (we wants to write 4 bytes, so less than - // 124 in the buffer) - if (pixels < end && (((READ_PERI_REG(UART_STATUS(1)) >> UART_TXFIFO_CNT_S) & UART_TXFIFO_CNT) <= 124)) { - uint8_t value = *pixels++; + /* Fill the UART fifos with IRQs disabled */ + uint32_t irq_state = esp8266_defer_irqs(); + while ((pixels < end) && ws2812_can_write(1)) { + ws2812_write_byte(1, *pixels++); + } + while ((pixels2 < end2) && ws2812_can_write(0)) { + ws2812_write_byte(0, *pixels2++); + } + esp8266_restore_irqs(irq_state); - // Fill the buffer - WRITE_PERI_REG(UART_FIFO(1), _uartData[(value >> 6) & 3]); - WRITE_PERI_REG(UART_FIFO(1), _uartData[(value >> 4) & 3]); - WRITE_PERI_REG(UART_FIFO(1), _uartData[(value >> 2) & 3]); - WRITE_PERI_REG(UART_FIFO(1), _uartData[(value >> 0) & 3]); + do { + if (pixels < end && ws2812_can_write(1)) { + ws2812_write_byte(1, *pixels++); } // Same for the second buffer - if (pixels2 < end2 && (((READ_PERI_REG(UART_STATUS(0)) >> UART_TXFIFO_CNT_S) & UART_TXFIFO_CNT) <= 124)) { - uint8_t value = *pixels2++; - - // Fill the buffer - WRITE_PERI_REG(UART_FIFO(0), _uartData[(value >> 6) & 3]); - WRITE_PERI_REG(UART_FIFO(0), _uartData[(value >> 4) & 3]); - WRITE_PERI_REG(UART_FIFO(0), _uartData[(value >> 2) & 3]); - WRITE_PERI_REG(UART_FIFO(0), _uartData[(value >> 0) & 3]); + if (pixels2 < end2 && ws2812_can_write(0)) { + ws2812_write_byte(0, *pixels2++); } } while(pixels < end || pixels2 < end2); // Until there is still something to send } @@ -125,14 +136,14 @@ static int ws2812_write(lua_State* L) { } else if (type == LUA_TUSERDATA) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - + pixbuf *buffer = pixbuf_from_lua_arg(L, 1); + luaL_argcheck(L, pixbuf_channels(buffer) == 3, 1, "Bad pixbuf format"); buffer1 = buffer->values; - length1 = buffer->colorsPerLed*buffer->size; + length1 = pixbuf_size(buffer); } else { - luaL_argerror(L, 1, "ws2812.buffer or string expected"); + luaL_argerror(L, 1, "pixbuf or string expected"); } // Second optionnal parameter @@ -148,14 +159,14 @@ static int ws2812_write(lua_State* L) { } else if (type == LUA_TUSERDATA) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer"); - + pixbuf *buffer = pixbuf_from_lua_arg(L, 2); + luaL_argcheck(L, pixbuf_channels(buffer) == 3, 2, "Bad pixbuf format"); buffer2 = buffer->values; - length2 = buffer->colorsPerLed*buffer->size; + length2 = pixbuf_size(buffer); } else { - luaL_argerror(L, 2, "ws2812.buffer or string expected"); + luaL_argerror(L, 2, "pixbuf or string expected"); } // Send the buffers @@ -164,475 +175,20 @@ static int ws2812_write(lua_State* L) { return 0; } -static ptrdiff_t posrelat(ptrdiff_t pos, size_t len) { - /* relative string position: negative means back from end */ - if (pos < 0) pos += (ptrdiff_t)len + 1; - return MIN(MAX(pos, 1), len); -} - -static ws2812_buffer *allocate_buffer(lua_State *L, int leds, int colorsPerLed) { - // Allocate memory - size_t size = sizeof(ws2812_buffer) + colorsPerLed*leds; - ws2812_buffer * buffer = (ws2812_buffer*)lua_newuserdata(L, size); - - // Associate its metatable - luaL_getmetatable(L, "ws2812.buffer"); - lua_setmetatable(L, -2); - - // Save led strip size - buffer->size = leds; - buffer->colorsPerLed = colorsPerLed; - - return buffer; -} - - -// Handle a buffer where we can store led values -static int ws2812_new_buffer(lua_State *L) { - const int leds = luaL_checkint(L, 1); - const int colorsPerLed = luaL_checkint(L, 2); - - luaL_argcheck(L, leds > 0, 1, "should be a positive integer"); - luaL_argcheck(L, colorsPerLed > 0, 2, "should be a positive integer"); - - ws2812_buffer * buffer = allocate_buffer(L, leds, colorsPerLed); - - memset(buffer->values, 0, colorsPerLed * leds); - - return 1; -} - - -int ws2812_buffer_fill(ws2812_buffer * buffer, int * colors) { - - // Grab colors - int i, j; - - // Fill buffer - uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) - { - for (j = 0; j < buffer->colorsPerLed; j++) - { - *p++ = colors[j]; - } - } - - return 0; -} - -static int ws2812_buffer_fill_lua(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - - // Grab colors - int i; - int * colors = luaM_malloc(L, buffer->colorsPerLed * sizeof(int)); - - for (i = 0; i < buffer->colorsPerLed; i++) - { - colors[i] = luaL_checkinteger(L, 2+i); - } - - ws2812_buffer_fill(buffer, colors); - - // Free memory - luaM_free(L, colors); - - return 0; -} - -void ws2812_buffer_fade(ws2812_buffer * buffer, int fade, unsigned direction) { - uint8_t * p = &buffer->values[0]; - int val = 0; - int i; - for (i = 0; i < buffer->size * buffer->colorsPerLed; i++) - { - if (direction == FADE_OUT) - { - *p++ /= fade; - } - else - { - // as fade in can result in value overflow, an int is used to perform the check afterwards - val = *p * fade; - if (val > 255) val = 255; - *p++ = val; - } - } -} - -static int ws2812_buffer_fade_lua(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - const int fade = luaL_checkinteger(L, 2); - unsigned direction = luaL_optinteger( L, 3, FADE_OUT ); - - luaL_argcheck(L, fade > 0, 2, "fade value should be a strict positive number"); - - ws2812_buffer_fade(buffer, fade, direction); - - return 0; -} - -int ws2812_buffer_shift(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end){ - - ws2812_buffer_shift_prepare* prepare = ws2812_buffer_get_shift_prepare(L, buffer, shiftValue, shift_type, pos_start, pos_end); - ws2812_buffer_shift_prepared(prepare); - // Free memory - luaM_freemem(L, prepare, sizeof(ws2812_buffer_shift_prepare) + prepare->shift_len); - - return 0; -} - -ws2812_buffer_shift_prepare* ws2812_buffer_get_shift_prepare(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end){ - - ptrdiff_t start = posrelat(pos_start, buffer->size); - ptrdiff_t end = posrelat(pos_end, buffer->size); - - start--; - int size = end - start; - size_t offset = start * buffer->colorsPerLed; - - luaL_argcheck(L, shiftValue >= 0-size && shiftValue <= size, 2, "shifting more elements than buffer size"); - - int shift = shiftValue >= 0 ? shiftValue : -shiftValue; - - size_t shift_len, remaining_len; - // calculate length of shift section and remaining section - shift_len = shift*buffer->colorsPerLed; - remaining_len = (size-shift)*buffer->colorsPerLed; - - ws2812_buffer_shift_prepare* prepare = luaM_malloc(L, sizeof(ws2812_buffer_shift_prepare) + shift_len); - prepare->offset = offset; - prepare->tmp_pixels = (uint8_t*)(prepare+1); - prepare->shiftValue = shiftValue; - prepare->shift_len = shift_len; - prepare->remaining_len = remaining_len; - prepare->shift_type = shift_type; - prepare->buffer = buffer; - - return prepare; -} - -void ws2812_buffer_shift_prepared(ws2812_buffer_shift_prepare* prepare) { - - // check if we want to shift at all - if (prepare->shift_len == 0 || (prepare->shift_len + prepare->remaining_len) <= 0) - { - return; - } - - if (prepare->shiftValue > 0) - { - // Store the values which are moved out of the array (last n pixels) - memcpy(prepare->tmp_pixels, &prepare->buffer->values[prepare->offset + prepare->remaining_len], prepare->shift_len); - // Move pixels to end - os_memmove(&prepare->buffer->values[prepare->offset + prepare->shift_len], &prepare->buffer->values[prepare->offset], prepare->remaining_len); - // Fill beginning with temp data - if (prepare->shift_type == SHIFT_LOGICAL) - { - memset(&prepare->buffer->values[prepare->offset], 0, prepare->shift_len); - } - else - { - memcpy(&prepare->buffer->values[prepare->offset], prepare->tmp_pixels, prepare->shift_len); - } - } - else - { - // Store the values which are moved out of the array (last n pixels) - memcpy(prepare->tmp_pixels, &prepare->buffer->values[prepare->offset], prepare->shift_len); - // Move pixels to end - os_memmove(&prepare->buffer->values[prepare->offset], &prepare->buffer->values[prepare->offset + prepare->shift_len], prepare->remaining_len); - // Fill beginning with temp data - if (prepare->shift_type == SHIFT_LOGICAL) - { - memset(&prepare->buffer->values[prepare->offset + prepare->remaining_len], 0, prepare->shift_len); - } - else - { - memcpy(&prepare->buffer->values[prepare->offset + prepare->remaining_len], prepare->tmp_pixels, prepare->shift_len); - } - } -} - -static int ws2812_buffer_shift_lua(lua_State* L) { - - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - const int shiftValue = luaL_checkinteger(L, 2); - const unsigned shift_type = luaL_optinteger( L, 3, SHIFT_LOGICAL ); - - const int pos_start = luaL_optinteger(L, 4, 1); - const int pos_end = luaL_optinteger(L, 5, -1); - - - ws2812_buffer_shift(L, buffer, shiftValue, shift_type, pos_start, pos_end); - return 0; -} - -static int ws2812_buffer_dump(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - - lua_pushlstring(L, buffer->values, buffer->size * buffer->colorsPerLed); - - return 1; -} - -static int ws2812_buffer_replace(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - ptrdiff_t start = posrelat(luaL_optinteger(L, 3, 1), buffer->size); - - uint8_t *src; - size_t srcLen; - - if (lua_type(L, 2) == LUA_TSTRING) { - size_t length; - - src = (uint8_t *) lua_tolstring(L, 2, &length); - srcLen = length / buffer->colorsPerLed; - } else { - ws2812_buffer * rhs = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer"); - src = rhs->values; - srcLen = rhs->size; - luaL_argcheck(L, rhs->colorsPerLed == buffer->colorsPerLed, 2, "Buffers have different colors"); - } - - luaL_argcheck(L, srcLen + start - 1 <= buffer->size, 2, "Does not fit into destination"); - - memcpy(buffer->values + (start - 1) * buffer->colorsPerLed, src, srcLen * buffer->colorsPerLed); - - return 0; -} - -// buffer:mix(factor1, buffer1, ..) -// factor is 256 for 100% -// uses saturating arithmetic (one buffer at a time) -static int ws2812_buffer_mix(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - - int pos = 2; - size_t cells = buffer->size * buffer->colorsPerLed; - - int n_sources = (lua_gettop(L) - 1) / 2; - - struct { - int factor; - const uint8_t *values; - } source[n_sources]; - - int src; - for (src = 0; src < n_sources; src++, pos += 2) { - int factor = luaL_checkinteger(L, pos); - ws2812_buffer *src_buffer = (ws2812_buffer*) luaL_checkudata(L, pos + 1, "ws2812.buffer"); - - luaL_argcheck(L, src_buffer->size == buffer->size && src_buffer->colorsPerLed == buffer->colorsPerLed, pos + 1, "Buffer not same shape"); - - source[src].factor = factor; - source[src].values = src_buffer->values; - } - - size_t i; - for (i = 0; i < cells; i++) { - int32_t val = 0; - for (src = 0; src < n_sources; src++) { - val += (int32_t)(source[src].values[i] * source[src].factor); - } - - val += 128; // rounding istead of floor - val /= 256; // do not use implemetation dependant right shift - - if (val < 0) { - val = 0; - } else if (val > 255) { - val = 255; - } - buffer->values[i] = (uint8_t)val; - } - - return 0; -} - -// Returns the total of all channels -static int ws2812_buffer_power(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - - size_t cells = buffer->size * buffer->colorsPerLed; - - size_t i; - int total = 0; - for (i = 0; i < cells; i++) { - total += buffer->values[i]; - } - - lua_pushinteger(L, total); - - return 1; -} - -static int ws2812_buffer_get(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - const int led = luaL_checkinteger(L, 2) - 1; - - luaL_argcheck(L, led >= 0 && led < buffer->size, 2, "index out of range"); - - int i; - for (i = 0; i < buffer->colorsPerLed; i++) - { - lua_pushinteger(L, buffer->values[buffer->colorsPerLed*led+i]); - } - - return buffer->colorsPerLed; -} - -static int ws2812_buffer_set(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - const int led = luaL_checkinteger(L, 2) - 1; - - luaL_argcheck(L, led >= 0 && led < buffer->size, 2, "index out of range"); - - int type = lua_type(L, 3); - if(type == LUA_TTABLE) - { - int i; - for (i = 0; i < buffer->colorsPerLed; i++) - { - // Get value and push it on stack - lua_rawgeti(L, 3, i+1); - - // Convert it as int and store them in buffer - buffer->values[buffer->colorsPerLed*led+i] = lua_tointeger(L, -1); - } - - // Clean up the stack - lua_pop(L, buffer->colorsPerLed); - } - else if(type == LUA_TSTRING) - { - size_t len; - const char * buf = lua_tolstring(L, 3, &len); - - // Overflow check - if( buffer->colorsPerLed*led + len > buffer->colorsPerLed*buffer->size ) - { - return luaL_error(L, "string size will exceed strip length"); - } - - memcpy(&buffer->values[buffer->colorsPerLed*led], buf, len); - } - else - { - int i; - for (i = 0; i < buffer->colorsPerLed; i++) - { - buffer->values[buffer->colorsPerLed*led+i] = luaL_checkinteger(L, 3+i); - } - } - - return 0; -} - -static int ws2812_buffer_size(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - - lua_pushinteger(L, buffer->size); - - return 1; -} - -static int ws2812_buffer_sub(lua_State* L) { - ws2812_buffer * lhs = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - size_t l = lhs->size; - ptrdiff_t start = posrelat(luaL_checkinteger(L, 2), l); - ptrdiff_t end = posrelat(luaL_optinteger(L, 3, -1), l); - if (start <= end) { - ws2812_buffer *result = allocate_buffer(L, end - start + 1, lhs->colorsPerLed); - memcpy(result->values, lhs->values + lhs->colorsPerLed * (start - 1), lhs->colorsPerLed * (end - start + 1)); - } else { - ws2812_buffer *result = allocate_buffer(L, 0, lhs->colorsPerLed); - } - return 1; -} - -static int ws2812_buffer_concat(lua_State* L) { - ws2812_buffer * lhs = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - ws2812_buffer * rhs = (ws2812_buffer*)luaL_checkudata(L, 2, "ws2812.buffer"); - - luaL_argcheck(L, lhs->colorsPerLed == rhs->colorsPerLed, 1, "Can only concatenate buffers with same colors"); - - int colorsPerLed = lhs->colorsPerLed; - int leds = lhs->size + rhs->size; - - ws2812_buffer * buffer = allocate_buffer(L, leds, colorsPerLed); - - memcpy(buffer->values, lhs->values, lhs->colorsPerLed * lhs->size); - memcpy(buffer->values + lhs->colorsPerLed * lhs->size, rhs->values, rhs->colorsPerLed * rhs->size); - - return 1; -} - -static int ws2812_buffer_tostring(lua_State* L) { - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - - luaL_Buffer result; - luaL_buffinit(L, &result); - - luaL_addchar(&result, '['); - int i; - int p = 0; - for (i = 0; i < buffer->size; i++) { - int j; - if (i > 0) { - luaL_addchar(&result, ','); - } - luaL_addchar(&result, '('); - for (j = 0; j < buffer->colorsPerLed; j++, p++) { - if (j > 0) { - luaL_addchar(&result, ','); - } - char numbuf[5]; - sprintf(numbuf, "%d", buffer->values[p]); - luaL_addstring(&result, numbuf); - } - luaL_addchar(&result, ')'); - } - - luaL_addchar(&result, ']'); - luaL_pushresult(&result); - - return 1; -} - -LROT_BEGIN(ws2812_buffer_map, NULL, LROT_MASK_INDEX) - LROT_FUNCENTRY( __concat, ws2812_buffer_concat ) - LROT_TABENTRY( __index, ws2812_buffer_map ) - LROT_FUNCENTRY( __tostring, ws2812_buffer_tostring ) - LROT_FUNCENTRY( dump, ws2812_buffer_dump ) - LROT_FUNCENTRY( fade, ws2812_buffer_fade_lua) - LROT_FUNCENTRY( fill, ws2812_buffer_fill_lua ) - LROT_FUNCENTRY( get, ws2812_buffer_get ) - LROT_FUNCENTRY( replace, ws2812_buffer_replace ) - LROT_FUNCENTRY( mix, ws2812_buffer_mix ) - LROT_FUNCENTRY( power, ws2812_buffer_power ) - LROT_FUNCENTRY( set, ws2812_buffer_set ) - LROT_FUNCENTRY( shift, ws2812_buffer_shift_lua ) - LROT_FUNCENTRY( size, ws2812_buffer_size ) - LROT_FUNCENTRY( sub, ws2812_buffer_sub ) -LROT_END(ws2812_buffer_map, NULL, LROT_MASK_INDEX) - LROT_BEGIN(ws2812, NULL, 0) LROT_FUNCENTRY( init, ws2812_init ) - LROT_FUNCENTRY( newBuffer, ws2812_new_buffer ) + LROT_FUNCENTRY( newBuffer, pixbuf_new_lua ) // backwards compatibility LROT_FUNCENTRY( write, ws2812_write ) - LROT_NUMENTRY( FADE_IN, FADE_IN ) - LROT_NUMENTRY( FADE_OUT, FADE_OUT ) + LROT_NUMENTRY( FADE_IN, PIXBUF_FADE_IN ) // BC + LROT_NUMENTRY( FADE_OUT, PIXBUF_FADE_OUT ) // BC LROT_NUMENTRY( MODE_SINGLE, MODE_SINGLE ) LROT_NUMENTRY( MODE_DUAL, MODE_DUAL ) - LROT_NUMENTRY( SHIFT_LOGICAL, SHIFT_LOGICAL ) - LROT_NUMENTRY( SHIFT_CIRCULAR, SHIFT_CIRCULAR ) + LROT_NUMENTRY( SHIFT_LOGICAL, PIXBUF_SHIFT_LOGICAL ) // BC + LROT_NUMENTRY( SHIFT_CIRCULAR, PIXBUF_SHIFT_CIRCULAR ) // BC LROT_END(ws2812, NULL, 0) -int luaopen_ws2812(lua_State *L) { +static int luaopen_ws2812(lua_State *L) { // TODO: Make sure that the GPIO system is initialized - luaL_rometatable(L, "ws2812.buffer", LROT_TABLEREF(ws2812_buffer_map)); return 0; } diff --git a/app/modules/ws2812.h b/app/modules/ws2812.h deleted file mode 100644 index 95fd3f0292..0000000000 --- a/app/modules/ws2812.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef APP_MODULES_WS2812_H_ -#define APP_MODULES_WS2812_H_ - -#include "module.h" -#include "lauxlib.h" -#include "lmem.h" -#include "platform.h" -#include -#include -#include -#include "user_interface.h" -#include "driver/uart.h" -#include "osapi.h" - -#define FADE_IN 1 -#define FADE_OUT 0 -#define SHIFT_LOGICAL 0 -#define SHIFT_CIRCULAR 1 - -#ifndef MIN -#define MIN(a,b) ((a) < (b) ? (a) : (b)) -#endif -#ifndef MAX -#define MAX(a,b) ((a) > (b) ? (a) : (b)) -#endif - -typedef struct { - int size; - uint8_t colorsPerLed; - uint8_t values[0]; -} ws2812_buffer; - -typedef struct { - size_t offset; - uint8_t* tmp_pixels; - int shiftValue; - size_t shift_len; - size_t remaining_len; - unsigned shift_type; - ws2812_buffer* buffer; -} ws2812_buffer_shift_prepare; - - -void ICACHE_RAM_ATTR ws2812_write_data(const uint8_t *pixels, uint32_t length, const uint8_t *pixels2, uint32_t length2); -// To shift the lua_State is needed for error message and memory allocation. -// We also need the shift operation inside a timer callback, where we cannot access the lua_State, -// so This is split up in prepare and the actual call, which can be called multiple times with the same prepare object. -// After being done just luaM_free on the prepare object. -void ws2812_buffer_shift_prepared(ws2812_buffer_shift_prepare* prepare); -ws2812_buffer_shift_prepare* ws2812_buffer_get_shift_prepare(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end); - -int ws2812_buffer_fill(ws2812_buffer * buffer, int * colors); -void ws2812_buffer_fade(ws2812_buffer * buffer, int fade, unsigned direction); - -#endif /* APP_MODULES_WS2812_H_ */ diff --git a/app/modules/ws2812_effects.c b/app/modules/ws2812_effects.c index 894a66ae00..1b3f1c9a92 100644 --- a/app/modules/ws2812_effects.c +++ b/app/modules/ws2812_effects.c @@ -10,7 +10,7 @@ #include "osapi.h" #include "pm/swtimer.h" -#include "ws2812.h" +#include "pixbuf.h" #include "color_utils.h" #define CANARY_VALUE 0x32372132 @@ -43,9 +43,8 @@ #define IDX_B 2 #define IDX_W 3 - typedef struct { - ws2812_buffer *buffer; + pixbuf *buffer; int buffer_ref; uint32_t mode_delay; uint32_t counter_mode_call; @@ -58,10 +57,9 @@ typedef struct { uint8_t effect_type; uint8_t color[4]; int effect_int_param1; - ws2812_buffer_shift_prepare* prepare; -} ws2812_effects; - + struct pixbuf_shift_params shift; +} ws2812_effects; enum ws2812_effects_type { WS2812_EFFECT_STATIC, @@ -91,40 +89,33 @@ static ws2812_effects *state; // UTILITY METHODS //----------------- +// XXX Not exported because this module is its sole non-Lua consumer and we +// should be going away soon! Deprecated, 'n all that. +extern void ICACHE_RAM_ATTR ws2812_write_data( + const uint8_t *pixels, uint32_t length, + const uint8_t *pixels2, uint32_t length2); -static int ws2812_write(ws2812_buffer* buffer) { - size_t length1, length2; - const char *buffer1, *buffer2; - - buffer1 = buffer->values; - length1 = buffer->colorsPerLed*buffer->size; - - buffer2 = 0; - length2 = 0; - - // Send the buffers - ws2812_write_data(buffer1, length1, buffer2, length2); - +static int ws2812_effects_write(pixbuf* buffer) { + ws2812_write_data(buffer->values, pixbuf_size(buffer), 0, 0); return 0; } +// :opens_boxes -1 +static void ws2812_set_pixel(int pixel, uint32_t color) { + pixbuf * buffer = state->buffer; -static int ws2812_set_pixel(int pixel, uint32_t color) { - ws2812_buffer * buffer = state->buffer; uint8_t g = ((color & 0x00FF0000) >> 16); uint8_t r = ((color & 0x0000FF00) >> 8); uint8_t b = (color & 0x000000FF); - uint8_t w = buffer->colorsPerLed == 4 ? ((color & 0xFF000000) >> 24) : 0; + uint8_t w = pixbuf_channels(buffer) == 4 ? ((color & 0xFF000000) >> 24) : 0; - int offset = pixel * buffer->colorsPerLed; + int offset = pixel * pixbuf_channels(buffer); buffer->values[offset+IDX_R] = r; buffer->values[offset+IDX_G] = g; buffer->values[offset+IDX_B] = b; - if (buffer->colorsPerLed == 4) { + if (pixbuf_channels(buffer) == 4) { buffer->values[offset+IDX_W] = w; } - - return 0; } @@ -160,16 +151,14 @@ static int ws2812_effects_init(lua_State *L) { platform_print_deprecation_note("ws2812_effects", "soon; please see https://github.com/nodemcu/nodemcu-firmware/issues/3122"); - ws2812_buffer * buffer = (ws2812_buffer*)luaL_checkudata(L, 1, "ws2812.buffer"); - luaL_argcheck(L, buffer != NULL, 1, "no valid buffer provided"); + pixbuf * buffer = pixbuf_from_lua_arg(L, 1); + // get rid of old state if (state != NULL) { - if (state->prepare) { - luaM_free(L, state->prepare); - } luaL_unref(L, LUA_REGISTRYINDEX, state->buffer_ref); free((void *) state); } + // Allocate memory and set all to zero state = (ws2812_effects *) calloc(1,sizeof(ws2812_effects)); // initialize @@ -245,11 +234,10 @@ static int ws2812_effects_set_brightness(lua_State* L) { return 0; } - - +// :opens_boxes -1 static void ws2812_effects_fill_buffer(uint8_t r, uint8_t g, uint8_t b, uint8_t w) { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; uint8_t bright_g = g * state->brightness / BRIGHTNESS_MAX; uint8_t bright_r = r * state->brightness / BRIGHTNESS_MAX; @@ -259,11 +247,11 @@ static void ws2812_effects_fill_buffer(uint8_t r, uint8_t g, uint8_t b, uint8_t // Fill buffer int i; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { + for(i = 0; i < buffer->npix; i++) { *p++ = bright_g; *p++ = bright_r; *p++ = bright_b; - if (buffer->colorsPerLed == 4) { + if (pixbuf_channels(buffer) == 4) { *p++ = bright_w; } } @@ -308,8 +296,8 @@ static int ws2812_effects_mode_blink() { } else { // off - ws2812_buffer * buffer = state->buffer; - memset(&buffer->values[0], 0, buffer->size * buffer->colorsPerLed); + pixbuf * buffer = state->buffer; + memset(&buffer->values[0], 0, pixbuf_size(buffer)); } return 0; } @@ -318,10 +306,10 @@ static int ws2812_effects_mode_blink() { static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; - int segments = (length1 / buffer->colorsPerLed) - 1; - int segmentSize = buffer->size / segments; + int segments = (length1 / pixbuf_channels(buffer)) - 1; + int segmentSize = buffer->npix / segments; uint8_t g1, r1, b1, g2, r2, b2; int i,j,k; @@ -330,7 +318,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) { r2 = *gradient_spec++; b2 = *gradient_spec++; // skip non-rgb components - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *gradient_spec++; } @@ -353,7 +341,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) { g2 = *gradient_spec++; r2 = *gradient_spec++; b2 = *gradient_spec++; - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *gradient_spec++; } @@ -371,7 +359,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) { int numPixels = segmentSize; // make sure we fill the strip correctly in case of rounding errors if (k == segments - 1) { - numPixels = buffer->size - (segmentSize * (segments - 1)); + numPixels = buffer->npix - (segmentSize * (segments - 1)); } int steps = numPixels - 1; @@ -391,7 +379,7 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) { *p++ = ((grb & 0x0000FF00) >> 8) * state->brightness / BRIGHTNESS_MAX; *p++ = (grb & 0x000000FF) * state->brightness / BRIGHTNESS_MAX; - for (j = 3; j < buffer->colorsPerLed; j++) { + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } } @@ -404,10 +392,10 @@ static int ws2812_effects_gradient(const char *gradient_spec, size_t length1) { static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; - int segments = (length1 / buffer->colorsPerLed) - 1; - int segmentSize = buffer->size / segments; + int segments = (length1 / pixbuf_channels(buffer)) - 1; + int segmentSize = buffer->npix / segments; uint8_t g1, r1, b1, g2, r2, b2; int i,j,k; @@ -416,7 +404,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) { r2 = *buffer1++; b2 = *buffer1++; // skip non-rgb components - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *buffer1++; } @@ -432,7 +420,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) { r2 = *buffer1++; b2 = *buffer1++; - for (j = 3; j < buffer->colorsPerLed; j++) { + for (j = 3; j < pixbuf_channels(buffer); j++) { *buffer1++; } @@ -440,7 +428,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) { int numPixels = segmentSize; // make sure we fill the strip correctly in case of rounding errors if (k == segments - 1) { - numPixels = buffer->size - (segmentSize * (segments - 1)); + numPixels = buffer->npix - (segmentSize * (segments - 1)); } int steps = numPixels - 1; @@ -449,7 +437,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) { *p++ = (g1 + ((g2-g1) * i / steps)) * state->brightness / BRIGHTNESS_MAX; *p++ = (r1 + ((r2-r1) * i / steps)) * state->brightness / BRIGHTNESS_MAX; *p++ = (b1 + ((b2-b1) * i / steps)) * state->brightness / BRIGHTNESS_MAX; - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } @@ -466,7 +454,7 @@ static int ws2812_effects_gradient_rgb(const char *buffer1, size_t length1) { */ static int ws2812_effects_mode_random_color() { state->mode_color_index = get_random_wheel_index(state->mode_color_index); - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; uint32_t color = color_wheel(state->mode_color_index); uint8_t r = ((color & 0x00FF0000) >> 16) * state->brightness / BRIGHTNESS_MAX; @@ -476,11 +464,11 @@ static int ws2812_effects_mode_random_color() { // Fill buffer int i,j; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { + for(i = 0; i < buffer->npix; i++) { *p++ = g; *p++ = r; *p++ = b; - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } @@ -493,7 +481,7 @@ static int ws2812_effects_mode_random_color() { */ static int ws2812_effects_mode_rainbow() { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; uint32_t color = color_wheel(state->counter_mode_step); uint8_t r = (color & 0x00FF0000) >> 16; @@ -503,11 +491,11 @@ static int ws2812_effects_mode_rainbow() { // Fill buffer int i,j; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { + for(i = 0; i < buffer->npix; i++) { *p++ = g * state->brightness / BRIGHTNESS_MAX; *p++ = r * state->brightness / BRIGHTNESS_MAX; *p++ = b * state->brightness / BRIGHTNESS_MAX; - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } @@ -523,12 +511,12 @@ static int ws2812_effects_mode_rainbow() { */ static int ws2812_effects_mode_rainbow_cycle(int repeat_count) { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; int i,j; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { - uint16_t wheel_index = (i * 360 / buffer->size * repeat_count) % 360; + for(i = 0; i < buffer->npix; i++) { + uint16_t wheel_index = (i * 360 / buffer->npix * repeat_count) % 360; uint32_t color = color_wheel(wheel_index); uint8_t r = ((color & 0x00FF0000) >> 16) * state->brightness / BRIGHTNESS_MAX; uint8_t g = ((color & 0x0000FF00) >> 8) * state->brightness / BRIGHTNESS_MAX; @@ -536,7 +524,7 @@ static int ws2812_effects_mode_rainbow_cycle(int repeat_count) { *p++ = g; *p++ = r; *p++ = b; - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } @@ -552,7 +540,7 @@ static int ws2812_effects_mode_rainbow_cycle(int repeat_count) { */ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; uint8_t p_g = state->color[0]; uint8_t p_r = state->color[1]; @@ -561,7 +549,7 @@ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) { // Fill buffer int i,j; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { + for(i = 0; i < buffer->npix; i++) { int flicker = rand() % (max_flicker > 0 ? max_flicker : 1); int r1 = p_r-flicker; int g1 = p_g-flicker; @@ -572,7 +560,7 @@ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) { *p++ = g1 * state->brightness / BRIGHTNESS_MAX; *p++ = r1 * state->brightness / BRIGHTNESS_MAX; *p++ = b1 * state->brightness / BRIGHTNESS_MAX; - for (j = 3; j < buffer->colorsPerLed; j++) { + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } } @@ -584,7 +572,7 @@ static int ws2812_effects_mode_flicker_int(uint8_t max_flicker) { * Halloween effect */ static int ws2812_effects_mode_halloween() { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; int g1 = 50 * state->brightness / BRIGHTNESS_MAX; int r1 = 255 * state->brightness / BRIGHTNESS_MAX; @@ -598,11 +586,11 @@ static int ws2812_effects_mode_halloween() { // Fill buffer int i,j; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { + for(i = 0; i < buffer->npix; i++) { *p++ = (i % 4 < 2) ? g1 : g2; *p++ = (i % 4 < 2) ? r1 : r2; *p++ = (i % 4 < 2) ? b1 : b2; - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } @@ -614,7 +602,7 @@ static int ws2812_effects_mode_halloween() { static int ws2812_effects_mode_circus_combustus() { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; int g1 = 0 * state->brightness / BRIGHTNESS_MAX; int r1 = 255 * state->brightness / BRIGHTNESS_MAX; @@ -627,7 +615,7 @@ static int ws2812_effects_mode_circus_combustus() { // Fill buffer int i,j; uint8_t * p = &buffer->values[0]; - for(i = 0; i < buffer->size; i++) { + for(i = 0; i < buffer->npix; i++) { if (i % 6 < 2) { *p++ = g1; *p++ = r1; @@ -643,7 +631,7 @@ static int ws2812_effects_mode_circus_combustus() { *p++ = 0; *p++ = 0; } - for (j = 3; j < buffer->colorsPerLed; j++) + for (j = 3; j < pixbuf_channels(buffer); j++) { *p++ = 0; } @@ -660,35 +648,37 @@ static int ws2812_effects_mode_circus_combustus() { */ static int ws2812_effects_mode_larson_scanner() { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; int led_index = 0; - ws2812_buffer_fade(buffer, 2, FADE_OUT); + for(int i=0; i < pixbuf_size(buffer); i++) { + buffer->values[i] = buffer->values[i] >> 2; + } uint16_t pos = 0; - if(state->counter_mode_step < buffer->size) { + if(state->counter_mode_step < buffer->npix) { pos = state->counter_mode_step; } else { - pos = (buffer->size * 2) - state->counter_mode_step - 2; + pos = (buffer->npix * 2) - state->counter_mode_step - 2; } - pos = pos * buffer->colorsPerLed; + pos = pos * pixbuf_channels(buffer); buffer->values[pos + 1] = state->color[1]; buffer->values[pos] = state->color[0]; buffer->values[pos + 2] = state->color[2]; - state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->size * 2) - 2); + state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->npix * 2) - 2); } static int ws2812_effects_mode_color_wipe() { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; - int led_index = (state->counter_mode_step % buffer->size) * buffer->colorsPerLed; + int led_index = (state->counter_mode_step % buffer->npix) * pixbuf_channels(buffer); - if (state->counter_mode_step >= buffer->size) + if (state->counter_mode_step >= buffer->npix) { buffer->values[led_index] = 0; buffer->values[led_index + 1] = 0; @@ -703,30 +693,30 @@ static int ws2812_effects_mode_color_wipe() { buffer->values[led_index + 1] = px_r; buffer->values[led_index + 2] = px_b; } - state->counter_mode_step = (state->counter_mode_step + 1) % (buffer->size * 2); + state->counter_mode_step = (state->counter_mode_step + 1) % (buffer->npix * 2); } static int ws2812_effects_mode_random_dot(uint8_t dots) { - ws2812_buffer * buffer = state->buffer; + pixbuf * buffer = state->buffer; // fade out - for(int i=0; i < buffer->size * buffer->colorsPerLed; i++) { + for(int i=0; i < pixbuf_size(buffer); i++) { buffer->values[i] = buffer->values[i] >> 1; } for(int i=0; i < dots; i++) { // pick random pixel - int led_index = rand() % buffer->size; + int led_index = rand() % buffer->npix; uint32_t color = (state->color[0] << 16) | (state->color[1] << 8) | state->color[2]; - if (buffer->colorsPerLed == 4) { + if (pixbuf_channels(buffer) == 4) { color = color | (state->color[3] << 24); } ws2812_set_pixel(led_index, color); } - state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->size * 2) - 2); + state->counter_mode_step = (state->counter_mode_step + 1) % ((buffer->npix * 2) - 2); } @@ -767,6 +757,11 @@ static uint32_t ws2812_effects_mode_delay() return delay; } +static void ws2812_effects_do_shift(void) +{ + pixbuf_shift(state->buffer, &state->shift); + ws2812_effects_write(state->buffer); +} /** * run loop for the effects. @@ -784,7 +779,7 @@ static void ws2812_effects_loop(void* p) else if (state->effect_type == WS2812_EFFECT_RAINBOW_CYCLE) { // the rainbow cycle effect can be achieved by shifting the buffer - ws2812_buffer_shift_prepared(state->prepare); + ws2812_effects_do_shift(); } else if (state->effect_type == WS2812_EFFECT_FLICKER) { @@ -816,11 +811,11 @@ static void ws2812_effects_loop(void* p) } else if (state->effect_type == WS2812_EFFECT_HALLOWEEN) { - ws2812_buffer_shift_prepared(state->prepare); + ws2812_effects_do_shift(); } else if (state->effect_type == WS2812_EFFECT_CIRCUS_COMBUSTUS) { - ws2812_buffer_shift_prepared(state->prepare); + ws2812_effects_do_shift(); } else if (state->effect_type == WS2812_EFFECT_LARSON_SCANNER) { @@ -828,7 +823,7 @@ static void ws2812_effects_loop(void* p) } else if (state->effect_type == WS2812_EFFECT_CYCLE) { - ws2812_buffer_shift_prepared(state->prepare); + ws2812_effects_do_shift(); } else if (state->effect_type == WS2812_EFFECT_COLOR_WIPE) { @@ -845,7 +840,7 @@ static void ws2812_effects_loop(void* p) // call count state->counter_mode_call = (state->counter_mode_call + 1) % UINT32_MAX; // write the buffer - ws2812_write(state->buffer); + ws2812_effects_write(state->buffer); // set the timer if (state->running == 1 && state->mode_delay >= 10) if (state->running == 1 && state->mode_delay >= 10) @@ -855,15 +850,6 @@ static void ws2812_effects_loop(void* p) } } -void prepare_shift(lua_State* L, ws2812_buffer * buffer, int shiftValue, unsigned shift_type, int pos_start, int pos_end){ - // deinit old effect - if (state->prepare) { - luaM_free(L, state->prepare); - } - - state->prepare = ws2812_buffer_get_shift_prepare(L, buffer, shiftValue, shift_type, pos_start, pos_end); -} - /** * Set the active effect mode */ @@ -910,13 +896,13 @@ static int ws2812_effects_set_mode(lua_State* L) { size_t length1; const char *buffer1 = lua_tolstring(L, 2, &length1); - if ((length1 / state->buffer->colorsPerLed < 2) || (length1 % state->buffer->colorsPerLed != 0)) + if ((length1 / pixbuf_channels(state->buffer) < 2) || (length1 % pixbuf_channels(state->buffer) != 0)) { luaL_argerror(L, 2, "must be at least two colors and same size as buffer colors"); } ws2812_effects_gradient(buffer1, length1); - ws2812_write(state->buffer); + ws2812_effects_write(state->buffer); } else { @@ -930,13 +916,13 @@ static int ws2812_effects_set_mode(lua_State* L) { size_t length1; const char *buffer1 = lua_tolstring(L, 2, &length1); - if ((length1 / state->buffer->colorsPerLed < 2) || (length1 % state->buffer->colorsPerLed != 0)) + if ((length1 / pixbuf_channels(state->buffer) < 2) || (length1 % pixbuf_channels(state->buffer) != 0)) { luaL_argerror(L, 2, "must be at least two colors and same size as buffer colors"); } ws2812_effects_gradient_rgb(buffer1, length1); - ws2812_write(state->buffer); + ws2812_effects_write(state->buffer); } else { @@ -952,7 +938,7 @@ static int ws2812_effects_set_mode(lua_State* L) { break; case WS2812_EFFECT_RAINBOW_CYCLE: ws2812_effects_mode_rainbow_cycle(effect_param != EFFECT_PARAM_INVALID ? effect_param : 1); - prepare_shift(L, state->buffer, 1, SHIFT_CIRCULAR, 1, -1); + pixbuf_prepare_shift(state->buffer, &state->shift, 1, PIXBUF_SHIFT_CIRCULAR, 1, -1); break; case WS2812_EFFECT_FLICKER: state->effect_int_param1 = effect_param; @@ -967,11 +953,11 @@ static int ws2812_effects_set_mode(lua_State* L) { break; case WS2812_EFFECT_HALLOWEEN: ws2812_effects_mode_halloween(); - prepare_shift(L, state->buffer, 1, SHIFT_CIRCULAR, 1, -1); + pixbuf_prepare_shift(state->buffer, &state->shift, 1, PIXBUF_SHIFT_CIRCULAR, 1, -1); break; case WS2812_EFFECT_CIRCUS_COMBUSTUS: ws2812_effects_mode_circus_combustus(); - prepare_shift(L, state->buffer, 1, SHIFT_CIRCULAR, 1, -1); + pixbuf_prepare_shift(state->buffer, &state->shift, 1, PIXBUF_SHIFT_CIRCULAR, 1, -1); break; case WS2812_EFFECT_LARSON_SCANNER: ws2812_effects_mode_larson_scanner(); @@ -980,7 +966,7 @@ static int ws2812_effects_set_mode(lua_State* L) { if (effect_param != EFFECT_PARAM_INVALID) { state->effect_int_param1 = effect_param; } - prepare_shift(L, state->buffer, state->effect_int_param1, SHIFT_CIRCULAR, 1, -1); + pixbuf_prepare_shift(state->buffer, &state->shift, state->effect_int_param1, PIXBUF_SHIFT_CIRCULAR, 1, -1); break; case WS2812_EFFECT_COLOR_WIPE: // fill buffer with black. r,g,b,w = 0 diff --git a/app/mqtt/mqtt_msg.h b/app/mqtt/mqtt_msg.h index bdaeb3e601..b192b6b18d 100644 --- a/app/mqtt/mqtt_msg.h +++ b/app/mqtt/mqtt_msg.h @@ -99,16 +99,15 @@ typedef struct mqtt_message_buffer typedef struct mqtt_connect_info { - char* client_id; - char* username; - char* password; - char* will_topic; - char* will_message; + const char* client_id; + const char* username; + const char* password; + const char* will_topic; + const char* will_message; int keepalive; int will_qos; int will_retain; int clean_session; - uint16_t max_message_length; } mqtt_connect_info_t; diff --git a/app/mqtt/msg_queue.c b/app/mqtt/msg_queue.c index 1b00d5e8b8..c2df1ae05b 100644 --- a/app/mqtt/msg_queue.c +++ b/app/mqtt/msg_queue.c @@ -18,6 +18,8 @@ msg_queue_t *msg_enqueue(msg_queue_t **head, mqtt_message_t *msg, uint16_t msg_i return NULL; } + node->sent = 0; + node->msg.data = (uint8_t *)calloc(1,msg->length); if(!node->msg.data){ NODE_DBG("not enough memory\n"); diff --git a/app/mqtt/msg_queue.h b/app/mqtt/msg_queue.h index 05b910ae9a..88b2fee2be 100644 --- a/app/mqtt/msg_queue.h +++ b/app/mqtt/msg_queue.h @@ -13,6 +13,8 @@ typedef struct msg_queue_t { uint16_t msg_id; int msg_type; int publish_qos; + + bool sent; } msg_queue_t; msg_queue_t * msg_enqueue(msg_queue_t **head, mqtt_message_t *msg, uint16_t msg_id, int msg_type, int publish_qos); diff --git a/app/net/nodemcu_mdns.c b/app/net/nodemcu_mdns.c index 5750a9b73a..119d7a38f7 100644 --- a/app/net/nodemcu_mdns.c +++ b/app/net/nodemcu_mdns.c @@ -1001,7 +1001,7 @@ mdns_set_servicename(const char *name) { char tmpBuf[128]; os_sprintf(tmpBuf, "_%s._tcp.local", name); if (service_name_with_suffix) { - os_free(service_name_with_suffix); + os_free((void *) service_name_with_suffix); } service_name_with_suffix = strdup(tmpBuf); } diff --git a/app/platform/cpu_esp8266_irq.h b/app/platform/cpu_esp8266_irq.h new file mode 100644 index 0000000000..44e003e7ad --- /dev/null +++ b/app/platform/cpu_esp8266_irq.h @@ -0,0 +1,15 @@ +#pragma once + +static inline uint32_t +esp8266_defer_irqs(void) +{ + uint32_t state; + __asm__ __volatile__ ("rsil %0, 15" : "=a"(state) : : "memory"); + return state; +} + +static inline void +esp8266_restore_irqs(uint32_t state) +{ + __asm__ __volatile__ ("wsr %0, ps" : : "a"(state) : "memory"); +} diff --git a/docs/build.md b/docs/build.md index e9f04e5f97..42afca9f91 100644 --- a/docs/build.md +++ b/docs/build.md @@ -28,7 +28,7 @@ If you decide to build with either the Docker image or the native environment th ``` git clone --recurse-submodules -b https://github.com/nodemcu/nodemcu-firmware.git ``` -Omitting the optional `-b ` will clone master. +Omitting the optional `-b ` will clone release. ## Build Options @@ -76,17 +76,35 @@ editing `BIT_RATE_DEFAULT` in `app/include/user_config.h`: Note that, by default, the firmware runs an auto-baudrate detection algorithm so that typing a few characters at boot time will cause the firmware to lock onto that baud rate (between 1200 and 230400). -### Integer build -By default a build will be generated supporting floating-point variables. +### Double build (Lua 5.3 only) +By default a build will be generated supporting floating point variables (floats) and integers. +To increase the precision of the floating point variables, a double build can be created. This +is also the default in the Lua 5.1 builds. The downside is that more memory is consumed when +storing variables. +You can change this +either by uncommenting `LUA_NUMBER_64BITS` in `app/include/user_config.h`: + +```c +//#define LUA_NUMBER_64BITS +``` + +OR by overriding this with the `make` command + +``` +make EXTRA_CCFLAGS="-DLUA_NUMBER_64BITS .... +``` + +### Integer build (Lua 5.1 only) +By default a build will be generated supporting floating-point variables (doubles). To reduce memory size an integer build can be created. You can change this either by uncommenting `LUA_NUMBER_INTEGRAL` in `app/include/user_config.h`: ```c -#define LUA_NUMBER_INTEGRAL +//#define LUA_NUMBER_INTEGRAL ``` OR by overriding this with the `make` command as it's [done during the CI -build](https://github.com/nodemcu/nodemcu-firmware/blob/master/.travis.yml#L30): +build](https://github.com/nodemcu/nodemcu-firmware/blob/release/.travis.yml#L30): ``` make EXTRA_CCFLAGS="-DLUA_NUMBER_INTEGRAL .... diff --git a/docs/compiling.md b/docs/compiling.md index 731ee547bb..700a44f747 100644 --- a/docs/compiling.md +++ b/docs/compiling.md @@ -57,11 +57,30 @@ facilitates simple OTA updates to an LFS based Lua application; the absolute for facilitates factory installation of LFS based applications. Also note that the `app/lua/luac_cross` make and Makefile can be executed to build -just the `luac.cross` image. You must first ensure that the following option in -`app/include/user_config.h` is matched to your target configuration: +just the `luac.cross` image. You must first ensure that the following options in +`app/include/user_config.h` are matched to your target configuration: ```c -//#define LUA_NUMBER_INTEGRAL // uncomment if you want an integer build +// When using Lua 5.1, two different builds are now supported. +// The main difference is in the // processing of numeric data types. +// If LUA_NUMBER_INTEGRAL is defined, then +// all numeric calculations are done in integer, with divide being an integer +// operation, and decimal fraction constants are illegal. +// Otherwise all floating point operations use doubles. All integer values +// can be represented exactly in floating point. + +//#define LUA_NUMBER_INTEGRAL + +// When using Lua 5.3, two different builds are now supported. +// If LUA_NUMBER_64BITS is defined, then doubles are used to hold floating +// point numbers and integers are 64 bits long. Integers smaller than 2^53 can be i +// converted to doubles and back without any loss of precision. +// Otherwise all floating point operations use floats, and integers are 32-bit. +// Only integers under 2^24 can be represented exactly in floating point. +// Note that Lua 5.3 also supports Integers natively, but you have to be careful +// not to promote an integer to a floating point variable as you can lose precision. + +//#define LUA_NUMBER_64BITS ``` Note that the use of LFS and the LFS region size is now configured through the partition table. diff --git a/docs/getting-started.md b/docs/getting-started.md index d39be16159..20092165a7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -10,9 +10,9 @@ You will typically do steps 1 and 2 only once, but then repeat step 3 as you dev !!! caution - There's more than one way to skin a cat. For each of the tasks you have a number of choices with regards to tooling. The colored boxes represent an opinionated path to start your journey - the quickest way to success so to speak. Feel free to follow the links above to get more detailed information. + For each of the tasks you have a number of choices with regards to tooling and depending on the OS you are on. The colored boxes represent an opinionated path to start your journey - the quickest way to success so to speak. Feel free to follow the links above to get more detailed information. -### Task / OS selector +### Task and OS selector @@ -80,7 +80,7 @@ You will typically do steps 1 and 2 only once, but then repeat step 3 as you dev - + @@ -90,6 +90,11 @@ You will typically do steps 1 and 2 only once, but then repeat step 3 as you dev + + + + + @@ -146,7 +151,7 @@ For everything else the cloud builder GUI is self-explanatory. Hence, no need fo 1. Select an LFS size, 64KB is likely going to be large enough 1. Select other options and build -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) _Note that this service is not maintained by the NodeMCU team. It's run by a NodeMCU team member as an individual, though._ @@ -163,7 +168,7 @@ _Note that this service is not maintained by the NodeMCU team. It's run by a Nod 1. [Download](https://github.com/marcelstoer/nodemcu-pyflasher) then start PyFlasher 1. Select serial port, browse for firmware binary and set the flash options. -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) _Note that this tool is not an official NodeMCU offering. It's maintained by a NodeMCU team member as an individual, though._ @@ -183,13 +188,13 @@ The [default baud rate](https://github.com/espressif/esptool#baud-rate) is 11520 More details available on esptool.py GitHub repo. -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) ## ESPlorer TBD [https://github.com/4refr0nt/ESPlorer](https://github.com/4refr0nt/ESPlorer) -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) ## NodeMCU-Tool @@ -211,7 +216,7 @@ Quick start: Note that you may need to use the `sudo` prefix to install the tool at step 2, and also possibly add the `–unsafe-perm` flag after the install command. -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) ## Docker @@ -229,7 +234,7 @@ Detailed instructions available in the image's README. As for available config o 1. In `app/include/user_config.h` edit the line `#define LUA_FLASH_STORE 0x0` and adjust the size to that needed. Note that this must be a multiple of 4Kb. 2. Build as you would otherwise build with this image (i.e. see its README) -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) _Note that this Docker image is not an official NodeMCU offering. It's maintained by a NodeMCU team member as an individual, though._ @@ -239,17 +244,18 @@ A local copy of `luac.cross` is only needed if you want to compile the Lua files ### Windows Windows users can compile a local copy of the `luac.cross` executable for use on a development PC. To this you need: -- To download the current NodeMCU sources (this [dev ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/dev.zip) or [master ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/master.zip)) and unpack into a local folder, say `C:\nodemcu-firmware`; choose the master / dev versions to match the firmware version that you want to use. If you want an Integer buld then edit the `app/includes/user_config.h` file to select this. + +- To download the current NodeMCU sources (this [dev ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/dev.zip) or [release ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/release.zip)) and unpack into a local folder, say `C:\nodemcu-firmware`; choose the master / dev versions to match the firmware version that you want to use. If you want an Integer buld then edit the `app/includes/user_config.h` file to select this. - Choose a preferred toolchain to build your `luac.cross` executable. You have a number of options here: - If you are a Windows 10 user with the Windows Subsystem for Linux (WSL) already installed, then this is a Linux environment so you can follow the [Linux build instructions](#Linux) below. - A less resource intensive option which works on all Windows OS variants is to use Cygwin or MinGW, which are varaint ports of the [GNU Compiler Collection](https://gcc.gnu.org/) to Windows and which can both compile to native Windows executables. In the case of Cygwin, [install Cygwin](https://www.cygwin.com/install.html) (selecting the Cygwin core + **gcc-core** + **gnu make** in the install menu). In the case of MinGW you again only need a very basic C build environment so [install the MINGW](http://mingw.org/wiki/InstallationHOWTOforMinGW); you only need the core GCC and mingw32-make. Both both these create a **Cmd** prompt which paths in the relevant GCC toolchain. Switch to the `app/lua/luac_cross` and run make to build the compiler in the NodeMCU firmware root directory. You do this by rning `make` in Cygwin and `mingw32-make -f mingw32-Makefile.mak` in MinGW. - - If you can C development experience on the PC and a version of the MS Visual Studio on your PC then you can also simply build the image using the supplied MS project file. -- Once you have a built `luac.cross` executable, then you can use this to compile Lua code into an LFS image. You might wish to move this out of the nodemcu-firmware hierarchy, since this folder hierarchy is no longer required and can be trashed. + - You can also use MS Visual Studio (free community version is available). Just open the supplied MS solution file (msvc\hosttools.sln) and build it to get the Lua 5.1 luac.cross.exe file. Currently there is no sln file available for the Lua 5.3 version. +- Once you have a built `luac.cross` executable, then you can use this to compile Lua code into an LFS image. You might wish to move it out of the nodemcu-firmware hierarchy, since this folder hierarchy is no longer required and can be removed. ### Linux - Ensure that you have a "build essential" GCC toolchain installed. -- Download the current NodeMCU sources (this [dev ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/dev.zip) or [master ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/master.zip)) and unpack into a local folder; choose the master / dev versions to match the firmware version that you want to use. If you want an Integer buld then edit the `app/includes/user_config.h` file to select this. +- Download the current NodeMCU sources (this [dev ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/dev.zip) or [release ZIP file](https://github.com/nodemcu/nodemcu-firmware/archive/release.zip)) and unpack into a local folder; choose the master / dev versions to match the firmware version that you want to use. If you want an Integer buld then edit the `app/includes/user_config.h` file to select this. - Change directory to the `app/lua/luac_cross` sub-folder - Run `make` to build the executable. - Once you have a built `luac.cross` executable, then you can use this to compile Lua code into an LFS image. You might wish to move this out of the nodemcu-firmware hierarchy, since this folder hierarchy is no longer required and can be trashed. @@ -258,7 +264,7 @@ Windows users can compile a local copy of the `luac.cross` executable for use on As for [Linux](#linux) -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) ## Compile Lua into LFS image @@ -276,6 +282,9 @@ For example to run the Telnet and FTP servers from LFS, put the following files You should always include the first two modules, but the remaining files would normally be replaced by your own project files. Also remember that these are examples and that you are entirely free to modify or to replace them for your own application needs. +!!! Note + You will need to grab a luac.cross compiler that matches your configuration regarding float/integer, Lua 5.1/5.3 and possibly the release. + ### Terry's LFS Lua Cross-Compile Web Service [https://blog.ellisons.org.uk/article/nodemcu/a-lua-cross-compile-web-service/](https://blog.ellisons.org.uk/article/nodemcu/a-lua-cross-compile-web-service/) @@ -301,7 +310,8 @@ The same Docker image you used to build the NodeMCU firmware can be used to [com Note: read up on [selecting Lua files](#select-lua-files-to-be-run-from-lfs) first -For Windows you will do this from within the WSL / Cygwin command window, both of which use the `bash` shell. +For Windows if you built with WSL / Cygwin you will do this from within the respective command window, both of which use the `bash` shell. +If you used Visual Studio just use the windows cmd window. 1. `$ cd ` 1. `$ luac.cross -o lfs.img -f *.lua` @@ -310,13 +320,13 @@ You will need to adjust the `img` and `lua` paths according to their location, a You might also want to add a simple one-line script file to your `~/bin` directory to wrap this command up. -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) ## Upload LFS image The compiled LFS image file (e.g. `lfs.img`) is uploaded as a regular file to the device file system (SPIFFS). You do this just like with Lua files with e.g. [ESPlorer](#esplorer) or [NodeMCU-Tool](#nodemcu-tool). There is also a new example, [HTTP_OTA.lua](https://github.com/nodemcu/nodemcu-firmware/tree/dev/lua_examples/lfs/HTTP_OTA.lua), in `lua_examples` that can retrieve images from a standard web service. -Once the LFS image file is on SPIFFS, you can execute the [node.flashreload()](../modules/node/#nodeflashreload) command and the loader will then load it into flash and immediately restart the ESP module with the new LFS loaded, if the image file is valid. However, the call will return with an error _if_ the file is found to be invalid, so your reflash code should include logic to handle such an error return. +Once the LFS image file is on SPIFFS, you can execute the [node.flashreload()](modules/node.md#nodeflashreload) command and the loader will then load it into flash and immediately restart the ESP module with the new LFS loaded, if the image file is valid. However, the call will return with an error _if_ the file is found to be invalid, so your reflash code should include logic to handle such an error return. ### Edit your `init.lua` file @@ -325,47 +335,56 @@ Once the LFS image file is on SPIFFS, you can execute the [node.flashreload()](. - Individual functions can be executed directly, e.g. `LFS.myfunc(a,b)` - LFS is now in the require path, so `require 'myModule'` works as expected. -Do a protected call of this `_init` code: `pcall(node.flashindex("_init"))` and check the error status. See [Programming Techniques and Approachs](lfs.md#programming-techniques-and-approachs) in the LFS whitepaper for a more detailed description. +Do a protected call of this `_init` code: `pcall(node.LFS._init())` and check the error status. See [Programming Techniques and Approachs](lfs.md#programming-techniques-and-approachs) in the LFS whitepaper for a more detailed description. ### Minimal LFS example Below is a brief overview of building and running the simplest LFS-based system possible. -To use LFS, start with a version of NodeMCU with `LUA_FLASH_STORE` set in `app/include/user_config.h`, and load it on the ESP8266 in the usual way (whatever that is for your set up). +To use LFS, start with a version of the NodeMCU firmware with LFS enabled. See [the matrix](#task-and-os-selector) section "Build LFS +enabled firmware" for how to do that. Load it on the ESP8266 in the usual way (whatever that is for your set up). -Then build an LFS file system. This can be done in several ways, as discussed above; one of the easiest is to use `luac.cross -f -o lfs.img *lua` on the host machine. The file [lua_examples/lfs/_init.lua](https://github.com/nodemcu/nodemcu-firmware/tree/dev/lua_examples/lfs/_init.lua) should definitely be included in the image, since it's the easiest way of registering the LFS modules. The `lfs.img` file can then be downloaded to the ESP8266 just like any other file. +Then build an LFS file system. This can be done in several ways, as discussed above; one of the easiest is to use `luac.cross -o lfs.img -f *lua` on the host machine. Make sure to include a file named `hello_world.lua` with the following one line content: `print("Hello ESP8266 world!")` +The file [lua_examples/lfs/_init.lua](https://github.com/nodemcu/nodemcu-firmware/tree/dev/lua_examples/lfs/_init.lua) should definitely be included in the image, since it's the easiest way to integrate the LFS system. The `lfs.img` file can then be downloaded to the ESP8266 just like any other file. -The next step is to tell the ESP8266 that the LFS file system exists. This is done with eg. [node.flashreload("lfs.img")](../modules/node/#nodeflashreload), which will trigger a reset, followed by [node.flashindex("_init")()](../modules/node/#nodeflashindex) to register the modules; logging into the esp8266 and running the following commands gives an overview of the command sequence, given a main.lua file consisting of the line `print("LFS main() module")` +The next step is to tell the ESP8266 that the LFS exists. This is done with [node.LFS.reload("lfs.img")](modules/node.md#nodelfsreload), which will trigger a reset, followed by [node.LFS._init()](modules/node.md#nodelfsget) to better integrate LFS; logging into the esp8266 and running the following commands gives an overview of the command sequence. ``` > -> node.flashreload("lfs.img") --- flashreload() triggers a reset here. -> print(LFS) -nil -> node.flashindex("_init")() --- LFS is now initialised. -> print(LFS) -table: 3fff06e0 +> node.LFS.reload("lfs.img") +-- node.LFS.reload() triggers one or two resets here. +-- Call the LFS hello_world. +> node.LFS.hello_world() +Hello ESP8266 world! +-- DONE! + +-- now for some more insights and helpers -- List the modules in the LFS. -> print(LFS._list) -table: 3fff0728 -> for k,v in pairs(LFS._list) do print(k,v) end +> print(node.LFS.list) +function: 3fff0728 +> for k,v in pairs(node.LFS.list()) do print(k,v) end 1 dummy_strings 2 _init -3 main --- Call the LFS main() module. -> LFS.main() -LFS main() module +3 hello_world +-- integrate LFS with SPIFFS +> node.LFS._init() +-- We now can run and load files from SPIFFS or LFS using `dofile` and `loadfile`. +> dofile("hello_world.lua") +Hello ESP8266 world! +-- `require()` also works the same way now. +-- if there was a file called "hello_world.lua" in SPIFFS the that would be executed. But if there isn't a lookup in LFS is made. +-- _init.lua also sets a global LFS as a copy of node.LFS. This is somewhat backwards compatibility and might get removed in the future. +> print(LFS) +table: 3fff06e0 > ``` Note that no error correction has been used, since the commands are intended to be entered at a terminal, and errors will become obvious. -Then you should set up the ESP8266 boot process to check for the existence of an LFS image and run whichever module is required. Once the LFS module table has been registered by running [lua_examples/lfs/_init.lua](https://github.com/nodemcu/nodemcu-firmware/tree/dev/lua_examples/lfs/_init.lua) , running an LFS module is simple a matter of eg: `LFS.main()`. +Then you should set up the ESP8266 boot process to check for the existence of an LFS image and run whichever module is required. Once the LFS module table has been registered by running [lua_examples/lfs/_init.lua](https://github.com/nodemcu/nodemcu-firmware/tree/dev/lua_examples/lfs/_init.lua), running an LFS module is simply a matter of eg: `LFS.hello_world()`. -[node.flashreload()](../modules/node/#nodeflashreload) need only be rerun if the LFS image is updated; after it has loaded the LFS image into flash memory the original file (in SPIFFS) is no longer used, and can be deleted. +[node.LFS.reload()](modules/node.md#nodelfsreload) need only be rerun if the LFS image is updated; after it has loaded the LFS image into flash memory the original file (in SPIFFS) is no longer used, and can be deleted. Once LFS is known to work, then modules such as [lua_examples/lfs/dummy_strings.lua](https://github.com/nodemcu/nodemcu-firmware/tree/dev/lua_examples/lfs/dummy_strings.lua) can usefully be added, together of course with effective error checking. -[↑ back to matrix](#task-os-selector) +[↑ back to matrix](#task-and-os-selector) diff --git a/docs/index.md b/docs/index.md index 659f394b7f..e45650e409 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,19 +7,20 @@ The firmware was initially developed as is a companion project to the popular ES → [Getting Started](getting-started.md) ## Programming Model -The NodeMCU programming model is similar to that of [Node.js](https://en.wikipedia.org/wiki/Node.js), only in Lua. It is asynchronous and event-driven. Many functions, therefore, have parameters for callback functions. To give you an idea what a NodeMCU program looks like study the short snippets below. For more extensive examples have a look at the [`/lua_examples`](https://github.com/nodemcu/nodemcu-firmware/tree/master/lua_examples) folder in the repository on GitHub. +The NodeMCU programming model is similar to that of [Node.js](https://en.wikipedia.org/wiki/Node.js), only in Lua. It is asynchronous and event-driven. Many functions, therefore, have parameters for callback functions. To give you an idea what a NodeMCU program looks like study the short snippets below. For more extensive examples have a look at the [`/lua_examples`](https://github.com/nodemcu/nodemcu-firmware/tree/release/lua_examples) folder in the repository on GitHub. ```lua -- a simple HTTP server srv = net.createServer(net.TCP) srv:listen(80, function(conn) - conn:on("receive", function(sck, payload) - print(payload) - sck:send("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n

Hello, NodeMCU.

") - end) - conn:on("sent", function(sck) sck:close() end) + conn:on("receive", function(sck, payload) + print(payload) + sck:send("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n

Hello, NodeMCU.

") + end) + conn:on("sent", function(sck) sck:close() end) end) ``` + ```lua -- connect to WiFi access point (DO NOT save config to flash) wifi.setmode(wifi.STATION) @@ -32,12 +33,9 @@ wifi.sta.config(station_cfg) ```lua -- register event callbacks for WiFi events -wifi.sta.eventMonReg(wifi.STA_CONNECTING, function(previous_state) - if(previous_state==wifi.STA_GOTIP) then - print("Station lost connection with access point. Attempting to reconnect...") - else - print("STATION_CONNECTING") - end +wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, function(T) + print("\n\tSTA - CONNECTED".."\n\tSSID: "..T.SSID.."\n\tBSSID: ".. + T.BSSID.."\n\tChannel: "..T.channel) end) ``` @@ -56,9 +54,9 @@ In September 2018 support for a [Lua Flash Store (LFS)](lfs.md) was introduced. ## Releases -This project uses two main branches, `master` and `dev`. `dev` is actively worked on and it's also where PRs should be created against. `master` thus can be considered "stable" even though there are no automated regression tests. The goal is to merge back to `master` roughly every 2 months. Depending on the current "heat" (issues, PRs) we accept changes to `dev` for 5-6 weeks and then hold back for 2-3 weeks before the next snap is completed. +This project uses two main branches, `release` and `dev`. `dev` is actively worked on and it's also where PRs should be created against. `release` thus can be considered "stable" even though there are no automated regression tests. The goal is to merge back to `release` roughly every 2 months. Depending on the current "heat" (issues, PRs) we accept changes to `dev` for 5-6 weeks and then hold back for 2-3 weeks before the next snap is completed. -A new tag is created every time `dev` is merged back to `master`. They are listed in the [releases section on GitHub](https://github.com/nodemcu/nodemcu-firmware/releases). Tag names follow the `-master_yyyymmdd` pattern. +A new tag is created every time `dev` is merged back to `release` branch. They are listed in the [releases section on GitHub](https://github.com/nodemcu/nodemcu-firmware/releases). Tag names follow the `-release_yyyymmdd` pattern. ## Up-To-Date Documentation At the moment the only up-to-date documentation maintained by the current NodeMCU team is in English. It is part of the source code repository (`/docs` subfolder) and kept in sync with the code. diff --git a/docs/lua-modules/bme280.md b/docs/lua-modules/bme280.md index 22e542b724..1d1fe6f79e 100644 --- a/docs/lua-modules/bme280.md +++ b/docs/lua-modules/bme280.md @@ -1,15 +1,23 @@ # BME280 module | Since | Origin / Contributor | Maintainer | Source | | :----- | :-------------------- | :---------- | :------ | -| 2020-10-04 | [vsky279](https://github.com/vsky279) | [vsky279](https://github.com/vsky279) | [bme280.c](../../app/modules/bme280.c)| +| 2020-10-04 | [vsky279](https://github.com/vsky279) | [vsky279](https://github.com/vsky279) | [bme280.lua](../../lua_modules/bme280/bme280.lua)| This module communicates with [BME280/BMP280 temperature/air presssure/humidity sensors](http://www.bosch-sensortec.com/bst/products/all_products/bme280) (Bosch Sensortec) through [I2C](../modules/i2c.md) interface. -Note: the module works only with the [bme280_math](../modules/bme280_math) module. +!!! note + + The module works only with the [bme280_math](../modules/bme280_math) module. + +!!! caution + + The BMP280 only supports temperature and air pressure measurements. It will give wrong readings for humidity but no warnings or errors. Sometimes sellers of breakout boards for these sensors confuse the two and sell one as the other. + + To easily check if you have a BMP280 or a BME280 look at the shape of the sensor: The BMP280 has rectangular shape, whereas the BME280 has a square one. ## bme280.setup() -Creates bme280sensor object and initializes module. Initialization is mandatory before read values. Note that there has to be a delay between some tens to hundreds of milliseconds between calling `setup()` and reading measurements. +Creates bme280sensor object and initializes the module. Initialization is mandatory before reading values. Note that there has to be a delay between some tens to hundreds of milliseconds between calling `setup()` and reading measurements. Functions supported by bme280sensor object: - [setup()](#sobjsetup) diff --git a/docs/lua-modules/ds18b20.md b/docs/lua-modules/ds18b20.md index 61fa8eb67b..a9d1da54b6 100644 --- a/docs/lua-modules/ds18b20.md +++ b/docs/lua-modules/ds18b20.md @@ -5,8 +5,9 @@ This Lua module provides access to [DS18B20](https://datasheets.maximintegrated.com/en/ds/DS18B20.pdf) 1-Wire digital thermometer. -!!! note - The module requires `ow` C module built into firmware. +For integer version of firmware use [ds18b20-integer.lua](../../lua_modules/ds18b20/ds18b20-integer.lua) module - measured temperatures are multiplied by 10000. + +The module requires `ow` C module built into firmware. ### Require ```lua diff --git a/docs/lua-modules/liquidcrystal.md b/docs/lua-modules/liquidcrystal.md index 12db0525ed..1f8d1b6b14 100644 --- a/docs/lua-modules/liquidcrystal.md +++ b/docs/lua-modules/liquidcrystal.md @@ -76,8 +76,8 @@ It configures I²C backend and returns backend object. In most cases only `sda` and `scl` parameters are required #### Parameters -- `sda`: I²C data pin. If set to `nil`, I²C bus initialization step via [`i2c.setup`](https://nodemcu.readthedocs.io/en/master/modules/i2c/#i2csetup) will be skipped -- `scl`: I²C clock pin. If set to `nil`, I²C bus initialization step via [`i2c.setup`](https://nodemcu.readthedocs.io/en/master/modules/i2c/#i2csetup) will be skipped +- `sda`: I²C data pin. If set to `nil`, I²C bus initialization step via [`i2c.setup`](https://nodemcu.readthedocs.io/en/release/modules/i2c/#i2csetup) will be skipped +- `scl`: I²C clock pin. If set to `nil`, I²C bus initialization step via [`i2c.setup`](https://nodemcu.readthedocs.io/en/release/modules/i2c/#i2csetup) will be skipped - `busid`: I²C bus ID. Defaults to `0` - `busad`: chip I²C address. Defaults to `0x27` (default PCF8574 address) - `speed`: I²C speed. Defaults to `i2c.SLOW` @@ -237,6 +237,12 @@ liquidcrystal:blink(true) ## liquidcrystal.busy Get busy status of the LCD. When using GPIO backend without `rw` argument specification function does nothing. +!!! note + At least some HD44780s and/or interfaces have been observed to count polling + the busy flag as grounds for incrementing their position in memory. This is + mysterious, but software should restore the position after observing that the + busy flag is clear. + #### Syntax `liquidcrystal.busy(self)` @@ -429,6 +435,11 @@ liquidcrystal:leftToRight() ## liquidcrystal.position Get current position of the cursor. Position is 0 indexed. When using GPIO backend without `rw` argument specification function does nothing. +!!! note + At least some HD44780s and/or interfaces have been observed to count reading + the position as grounds for incrementing their position in memory. This is + mysterious, but software likely intends to restore the position anyway. + #### Syntax `liquidcrystal.position(self)` diff --git a/docs/lua53.md b/docs/lua53.md index 415e18f1b4..2d57d3f934 100644 --- a/docs/lua53.md +++ b/docs/lua53.md @@ -244,7 +244,7 @@ The Lua public API has largely been preserved across both Lua versions. Having - NodeMCU features that were in Lua51 and have also been added to Lua53 as part of this migration. - Differences (additions / removals / API changes) that we are not using in our modules and which can therefore be effectively ignored for the purposes of migration. - Some very nice feature enhancements in Lua53, for example the table access functions are now type `int` instead of type `void` and return the type of the accessed TValue. These features have been back-ported to Lua51 so that modules can be coded using Lua53 best practice and still work in a Lua51 runtime environment. -- Source differences which can be encapsulated through common macros will be be removed by updating module code to use this common macro set. In a very small number of cases module functionality will be recoded to employ this common API base. +- Source differences which can be encapsulated through common macros will be removed by updating module code to use this common macro set. In a very small number of cases module functionality will be recoded to employ this common API base. Both the LRM and PiL make quite clear that the public API for C modules is as documented in `lua.h` and all its definitions start with `lua_`. This API strives for economy and orthogonality. The supplementary functions provided by the auxiliary library (`lauxlib.c`) access Lua services and functions through the `lua.h` interface and without other reference to the internals of Lua; this is exposed through `lauxlib.h` and all its definitions start with `luaL_`. All Lua library modules (such as `lstrlib.c` which implements `string`) conform to this policy and only access the Lua runtime via the `lua.h` and `lauxlib.h` interfaces. diff --git a/docs/modules/apa102.md b/docs/modules/apa102.md index 9eb656f99e..f5a040b93e 100644 --- a/docs/modules/apa102.md +++ b/docs/modules/apa102.md @@ -19,7 +19,10 @@ Send ABGR data in 8 bits to a APA102 chain. - `data_pin` any GPIO pin 0, 1, 2, ... - `clock_pin` any GPIO pin 0, 1, 2, ... - `string` payload to be sent to one or more APA102 LEDs. - It should be composed from a ABGR quadruplet per element. + + It may be a [pixbuf](pixbuf) with four channels or a string, + composed from a ABGR quadruplet per element: + - `A1` the first pixel's Intensity channel (0-31) - `B1` the first pixel's Blue channel (0-255)
- `G1` the first pixel's Green channel (0-255) diff --git a/docs/modules/dht.md b/docs/modules/dht.md index 35bea003b8..50c8b2cab0 100644 --- a/docs/modules/dht.md +++ b/docs/modules/dht.md @@ -9,7 +9,8 @@ Constants for various functions. `dht.OK`, `dht.ERROR_CHECKSUM`, `dht.ERROR_TIMEOUT` represent the potential values for the DHT read status ## dht.read() -Read all kinds of DHT sensors, including DHT11, 21, 22, 33, 44 humidity temperature combo sensor. +Reads all kinds of DHT sensors, including DHT11, 21, 22, 33, 44 humidity temperature combo sensor. +Returns correct readout except for DHT12 and negative temperatures by DHT11. Use [`dht.read12()`](#dhtread12) and [`dht.read11()`](#dhtread11) instead. It is to use model specific read function anyway. #### Syntax `dht.read(pin)` @@ -74,8 +75,32 @@ Read DHT11 humidity temperature combo sensor. #### See also [dht.read()](#dhtread) +## dht.read12() +Read DHT12 humidity temperature combo sensor. + +#### Syntax +`dht.read12(pin)` + +#### Parameters +`pin` pin number of DHT12 sensor (can't be 0), type is number + +#### Returns +- `status` as defined in Constants +- `temp` temperature (see note below) +- `humi` humidity (see note below) +- `temp_dec` temperature decimal +- `humi_dec` humidity decimal + +!!! note + + If using float firmware then `temp` and `humi` are floating point numbers. On an integer firmware, the final values have to be concatenated from `temp` and `temp_dec` / `humi` and `hum_dec`. + +#### See also +[dht.read()](#dhtread) + + ## dht.readxx() -Read all kinds of DHT sensors, except DHT11. +Read all kinds of DHT sensors, except DHT11 and DHT12. Differs from `dht.read()` only by waiting only sufficient 1 ms for sensor wake-up while `dht.read()` waits universal 18 ms. ####Syntax `dht.readxx(pin)` diff --git a/docs/modules/file.md b/docs/modules/file.md index 4426e9795d..a6361efd44 100644 --- a/docs/modules/file.md +++ b/docs/modules/file.md @@ -56,7 +56,7 @@ Determines whether the specified file exists. - `filename` file to check #### Returns -true of the file exists (even if 0 bytes in size), and false if it does not exist +true if the file exists (even if 0 bytes in size), and false if it does not exist #### Example diff --git a/docs/modules/mqtt.md b/docs/modules/mqtt.md index 289550c7bb..9ffeb0a3b7 100644 --- a/docs/modules/mqtt.md +++ b/docs/modules/mqtt.md @@ -64,8 +64,6 @@ m = mqtt.Client("clientid", 120, "user", "password") -- to topic "/lwt" if client don't send keepalive packet m:lwt("/lwt", "offline", 0, 0) -m:on("connect", function(client) print ("connected") end) -m:on("connfail", function(client, reason) print ("connection failed", reason) end) m:on("offline", function(client) print ("offline") end) -- on publish message receive event @@ -96,11 +94,11 @@ m:connect("192.168.11.118", 1883, false, function(client) client:publish("/topic", "hello", 0, 0, function(client) print("sent") end) end, function(client, reason) - print("failed reason: " .. reason) + print("Connection failed reason: " .. reason) end) -m:close(); --- you can call m:connect again +m:close() +-- you can call m:connect again after the offline callback fires ``` # MQTT Client @@ -108,7 +106,11 @@ m:close(); ## mqtt.client:close() -Closes connection to the broker. +Schedules a clean teardown of the connection. + +MQTT requires clients to actively signal a desire to disconnect to the server +to avoid sending their LWT. Thus, the Client is not immediately reusable +after this call, but only after the "offline" callback has fired. #### Syntax `mqtt:close()` @@ -117,7 +119,7 @@ Closes connection to the broker. none #### Returns -`true` on success, `false` otherwise +`nil` ## mqtt.client:connect() diff --git a/docs/modules/net.md b/docs/modules/net.md index 3712fdb1f4..ea51e2ecb9 100644 --- a/docs/modules/net.md +++ b/docs/modules/net.md @@ -358,7 +358,7 @@ srv:on("receive", function(sck, c) end end) -- throttling could be implemented using socket:hold() --- example: https://github.com/nodemcu/nodemcu-firmware/blob/master/lua_examples/pcm/play_network.lua#L83 +-- example: https://github.com/nodemcu/nodemcu-firmware/blob/release/lua_examples/pcm/play_network.lua#L83 ``` #### See also diff --git a/docs/modules/pipe.md b/docs/modules/pipe.md index 1be4fa4fe4..0b717d549b 100644 --- a/docs/modules/pipe.md +++ b/docs/modules/pipe.md @@ -129,3 +129,11 @@ Write a string to a pipe object. #### Returns Nothing + +## pobj:nrec() + +Return the number of internal records in the pipe. Each record ranges from 1 +to 256 bytes in length, with full chunks being the most common case. As +extracting from a pipe only to `unread` if too few bytes are available, it may +be useful to have a quickly estimated upper bound on the length of the string +that would be returned. diff --git a/docs/modules/pixbuf.md b/docs/modules/pixbuf.md new file mode 100644 index 0000000000..c55730e3dd --- /dev/null +++ b/docs/modules/pixbuf.md @@ -0,0 +1,342 @@ +# Pixel Buffer (pixbuf) Module + +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2020-?? | [nwf](https://github.com/nwf) | nwf | [pixbuf.c](../../app/modules/pixbuf.c) | + +The pixbuf library offers C-array byte objects and convenient utility functions +for maintaining small frame buffers, usually for use with LED arrays, as +supported by, e.g., ws2812. + +## pixbuf.newBuffer() +Allocate a new memory buffer to store LED values. + +#### Syntax +`pixbuf.newBuffer(numberOfLeds, numberOfChannels)` + +#### Parameters + - `numberOfLeds` length of the LED strip (in pixels) + - `numberOfChannels` the channel count (bytes per pixel) + +#### Returns +`pixbuf.buffer` object + +## pixbuf.buffer:get() +Return the value at the given position, in native strip color order + +#### Syntax +`buffer:get(index)` + +#### Parameters + - `index` position in the buffer (1 for first LED) + +#### Returns +`(color)` + +#### Example +```lua +buffer = pixbuf.newBuffer(32, 4) +print(buffer:get(1)) +0 0 0 0 +``` + +## pixbuf.buffer:set() +Set the value at the given position, in native strip color order + +#### Syntax +`buffer:set(index, color)` + +#### Parameters + - `index` position in the buffer (1 for the first LED) + - `color` payload of the color + +Payload could be: +- `number, number, ...`, passing as many colors as required by the array type +- `table` should contain one value per color required by the array type +- `string` with a natural multiple of the colors required by the array type + +`string` inputs may be used to set multiple consecutive pixels! + +#### Returns +The buffer + +#### Example +```lua +buffer = pixbuf.newBuffer(32, 3) +buffer:set(1, 255, 0, 0) -- set the first LED green for a GRB strip +``` + +```lua +buffer = pixbuf.newBuffer(32, 4) +buffer:set(1, {255, 0, 0, 255}) -- set the first LED white and red for a RGBW strip +``` + +```lua +-- set the first LED green for a RGB strip and exploit the return value +buffer = pixbuf.newBuffer(32, 3):set(1, string.char(0, 255, 0)) +``` + +## pixbuf.buffer:size() +Return the size of the buffer in number of LEDs + +#### Syntax +`buffer:size()` + +#### Parameters +none + +#### Returns +`int` + +## pixbuf.buffer:channels() +Return the buffer's channel count + +#### Syntax +`buffer:channels()` + +#### Parameters +none + +#### Returns +`int` + +## pixbuf.buffer:fill() +Fill the buffer with the given color. +The number of given bytes must match the channel count of the buffer. + +#### Syntax +`buffer:fill(color)` + +#### Parameters + - `color` bytes for each channel + +#### Returns +The buffer + +#### Example +```lua +buffer:fill(0, 0, 0) -- fill the buffer with black for a RGB strip +``` + +## pixbuf.buffer:dump() +Returns the contents of the buffer (the pixel values) as a string. This can then be saved to a file or sent over a network and may be fed back to [`pixbuf.buffer:set()`](#pixbufbufferset). + +#### Syntax +`buffer:dump()` + +#### Returns +A string containing the pixel values. + +#### Example +```lua +local s = buffer:dump() +``` + +## pixbuf.buffer:replace() +Inserts a string (or a pixbuf) into another buffer with an offset. +The buffer must be of the same type or an error will be thrown. + +#### Syntax +`buffer:replace(source[, offset])` + +#### Parameters + - `source` the pixel values to be set into the buffer. This is either a string or a pixbuf. + - `offset` the offset where the source is to be placed in the buffer. Default is 1. Negative values can be used. + +#### Returns +`nil` + +#### Example +```lua +buffer:replace(anotherbuffer:dump()) -- copy one buffer into another via a string +buffer:replace(anotherbuffer) -- copy one buffer into another +newbuffer = buffer.sub(1) -- make a copy of a buffer into a new buffer +``` + +## pixbuf.buffer:mix() +This is a general method that loads data into a buffer that is a linear combination of data from other buffers. It can be used to copy a buffer or, +more usefully, do a cross fade. The pixel values are computed as integers and then range limited to [0, 255]. This means that negative +factors work as expected, and that the order of combining buffers does not matter. + +#### Syntax +`buffer:mix(factor1, buffer1, ...)` + +#### Parameters + - `factor1` This is the factor that the contents of `buffer1` are multiplied by. This factor is scaled by a factor of 256. Thus `factor1` value of 256 is a factor of 1.0. + - `buffer1` This is the source buffer. It must be of the same shape as the destination buffer. + +There can be any number of factor/buffer pairs. + +#### Returns +The output buffer. + +#### Example +```lua +-- loads buffer with a crossfade between buffer1 and buffer2 +buffer:mix(256 - crossmix, buffer1, crossmix, buffer2) + +-- multiplies all values in buffer by 0.75 +-- This can be used in place of buffer:fade +buffer:mix(192, buffer) +``` + +## pixbuf.buffer:mix4I5() +Like [`pixbuf.buffer:mix()`](#pixbufbuffermix) but treats the first channel as +a scaling, 5-bit intensity value. The buffers must all have four channels. +This is mostly useful for APA102 LEDs. + +## pixbuf.buffer:power() +Computes the total energy requirement for the buffer. This is merely the total sum of all the pixel values (which assumes that each color in each +pixel consumes the same amount of power). A real WS2812 (or WS2811) has three constant current drivers of 20mA -- one for each of R, G and B. The +pulse width modulation will cause the *average* current to scale linearly with pixel value. + +#### Syntax +`buffer:power()` + +#### Returns +An integer which is the sum of all the pixel values. + +#### Example +```lua +-- Dim the buffer to no more than the PSU can provide +local psu_current_ma = 1000 +local led_current_ma = 20 +local led_sum = psu_current_ma * 255 / led_current_ma + +local p = buffer:power() +if p > led_sum then + buffer:mix(256 * led_sum / p, buffer) -- power is now limited +end +``` + +## pixbuf.buffer:powerI() +Like [`pixbuf.buffer:power()`](#pixbufbufferpower) but treats the first channel as +a scaling intensity value. + +## pixbuf.buffer:fade() +Fade in or out. Defaults to out. Multiply or divide each byte of each led with/by the given value. Useful for a fading effect. + +#### Syntax +`buffer:fade(value [, direction])` + +#### Parameters + - `value` value by which to divide or multiply each byte + - `direction` pixbuf.FADE\_IN or pixbuf.FADE\_OUT. Defaults to pixbuf.FADE\_OUT + +#### Returns +`nil` + +#### Example +```lua +buffer:fade(2) +buffer:fade(2, pixbuf.FADE_IN) +``` + +## pixbuf.buffer:fadeI() +Like [`pixbuf.buffer:fade()`](#pixbufbufferfade) but treats the first channel as +a scaling intensity value. This is mostly useful for APA102 LEDs. + +## pixbuf.buffer:shift() +Shift the content of (a piece of) the buffer in positive or negative direction. This allows simple animation effects. A slice of the buffer can be specified by using the +standard start and end offset Lua notation. Negative values count backwards from the end of the buffer. + +#### Syntax +`buffer:shift(value [, mode[, i[, j]]])` + +#### Parameters + - `value` number of pixels by which to rotate the buffer. Positive values rotate forwards, negative values backwards. + - `mode` is the shift mode to use. Can be one of `pixbuf.SHIFT_LOGICAL` or `pixbuf.SHIFT_CIRCULAR`. In case of SHIFT\_LOGICAL, the freed pixels are set to 0 (off). In case of SHIFT\_CIRCULAR, the buffer is treated like a ring buffer, inserting the pixels falling out on one end again on the other end. Defaults to SHIFT\_LOGICAL. + - `i` is the first offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is 1. + - `j` is the last offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is -1. + +#### Returns +`nil` + +#### Example +```lua +buffer:shift(3) +``` + +## pixbuf.buffer:sub() +This implements the extraction function like `string.sub`. The indexes are in leds and all the same rules apply. + +#### Syntax +`buffer1:sub(i[, j])` + +#### Parameters + - `i` This is the start of the extracted data. Negative values can be used. + - `j` this is the end of the extracted data. Negative values can be used. The default is -1. + +#### Returns +A buffer containing the extracted piece. + +#### Example +``` +b = buffer:sub(1,10) +``` + +## pixbuf.buffer:__concat() +This implements the `..` operator to concatenate two buffers. They must have the same number of colors per led. + +#### Syntax +`buffer1 .. buffer2` + +#### Parameters + - `buffer1` this is the start of the resulting buffer + - `buffer2` this is the end of the resulting buffer + +#### Returns +The concatenated buffer. + +#### Example +``` +ws2812.write(buffer1 .. buffer2) +``` +## pixbuf.buffer:map() +Map a function across each pixel of one, or zip a function along two, +pixbuf(s), storing into the buffer on which it is called. + +#### Syntax +`buffer0:map(f, [buffer1], [start1], [end1], [buffer2, [start2]])` + +#### Parameters + - `f` This is the mapping function; it is applied for each pixel to all channels of `buffer1` and + all channels of `buffer2`, if given. It must return a value for each channel of the output + buffer, `buffer0`. + - `buffer1` The first source buffer. Defaults to `buffer0`. + - `start1` This is the start of the mapped range of `buffer1`. Negative values can be used and will be interpreted as before the end of `buffer1`. The default is 1. + - `end1` this is the end of the mapped range. Negative values can be used. The default is -1 (i.e., the end of `buffer1`). + - `buffer2` is a second buffer, for zip operations + - `start2` This is the start of the mapped range within `buffer2`. Negative values can be used and will be interpreted as before the end of `buffer2`. The default is 1. + +`buffer0` must have sufficient room to recieve all pixels from `start1` to +`end1` (which is true of the defaults, when `buffer1` is `buffer0` and `start1` +is 1 and `end1` is -1). `buffer2`, if given, must have sufficient pixels after +`start2`. + +#### Returns +`buffer0` + +#### Examples + +Change channel order within a single buffer: +```Lua +buffer:map(function(r,g,b) return g,r,b end) +``` + +Change channel order for a subset of pixels: +```Lua +buffer:map(function(r,g,b) return g,r,b end, nil, 2, 5) +``` + +Extract one channel for a subset of pixels: +```Lua +outbuf = pixbuf.create(11, 1) +outbuf:map(function(r,g,b) return b end, inbuf, 10, 20) +``` + +Concatenate channels per pixel, possibly with different offsets in buffers: +```Lua +outbuf:map(function(...) return ... end, inbuf1, inbuf2) +outbuf:map(function(...) return ... end, inbuf1, 5, 10, inbuf2, 3) +``` diff --git a/docs/modules/somfy.md b/docs/modules/somfy.md index 89812bbbbf..94e28579b7 100644 --- a/docs/modules/somfy.md +++ b/docs/modules/somfy.md @@ -3,9 +3,9 @@ | :----- | :-------------------- | :---------- | :------ | | 2016-09-27 | [vsky279](https://github.com/vsky279) | [vsky279](https://github.com/vsky279) | [somfy.c](../../app/modules/somfy.c)| -This module provides a simple interface to control Somfy blinds via an RF transmitter (433.42 MHz). It is based on [Nickduino Somfy Remote Arduino skecth](https://github.com/Nickduino/Somfy_Remote). +This module provides a simple interface to control Somfy blinds via an RF transmitter (433.42 MHz). It is based on [Nickduino Somfy Remote Arduino skecth](https://github.com/Nickduino/Somfy_Remote). It also allows listening to commands transmitted by the Somfy remote control. -The hardware used is the standard 433 MHz RF transmitter. Unfortunately these chips are usually transmitting at he frequency of 433.92MHz so the crystal resonator should be replaced with the 433.42 MHz resonator though some reporting that it is working even with the original crystal. +The hardware used is the standard 433 MHz RF transmitter. Unfortunately these chips are usually transmitting at he frequency of 433.92MHz so the crystal resonator should be replaced with the resonator on this frequency though some reporting that it is working even with the original crystal. To understand details of the Somfy protocol please refer to [Somfy RTS protocol](https://pushstack.wordpress.com/somfy-rts-protocol/) and also discussion [here](https://forum.arduino.cc/index.php?topic=208346.0). @@ -16,7 +16,7 @@ The module is using hardware timer so it cannot be used at the same time with ot Builds an frame defined by Somfy protocol and sends it to the RF transmitter. #### Syntax -`somfy.sendcommand(pin, remote_address, command, rolling_code, repeat_count, call_back)` +`somfy.sendcommand(pin, remote_address, command, rolling_code, repeat_count, callback)` #### Parameters - `pin` GPIO pin the RF transmitter is connected to. @@ -24,9 +24,9 @@ Builds an frame defined by Somfy protocol and sends it to the RF transmitter. - `command` command to be transmitted. Can be one of `somfy.SOMFY_UP`, `somfy.SOMFY_DOWN`, `somfy.SOMFY_PROG`, `somfy.SOMFY_STOP` - `rolling_code` The rolling code is increased every time a button is pressed. The receiver only accepts command if the rolling code is above the last received code and is not to far ahead of the last received code. This window is in the order of a 100 big. The rolling code needs to be stored in the EEPROM (i.e. filesystem) to survive the ESP8266 reset. - `repeat_count` how many times the command is repeated -- `call_back` a function to be called after the command is transmitted. Allows chaining commands to set the blinds to a defined position. +- `callback` a function to be called after the command is transmitted. Allows chaining commands to set the blinds to a defined position. -My original remote is [TELIS 4 MODULIS RTS](https://www.somfy.co.uk/products/1810765/telis-4-modulis-rts). This remote is working with the additional info - additional 56 bits that follow data (shortening the Inter-frame gap). It seems that the scrambling algorithm has not been revealed yet. +My original remote is [TELIS 4 MODULIS RTS](https://www.somfy.co.uk/products/blinds-and-curtains/buy-products/controls). This remote is working with the additional info - additional 56 bits that follow data (shortening the Inter-frame gap). It seems that the scrambling algorithm has not been revealed yet. When I send the `somfy.DOWN` command, repeating the frame twice (which seems to be the standard for a short button press), i.e. `repeat_count` equal to 2, the blinds go only 1 step down. This corresponds to the movement of the wheel on the original remote. The down button on the original remote sends also `somfy.DOWN` command but the additional info is different and this makes the blinds go full down. Fortunately it seems that repeating the frame 16 times makes the blinds go fully down. @@ -43,3 +43,32 @@ To start with controlling your Somfy blinds you need to: - running `somfy.sendcommand(4, 123, somfy.DOWN, 2, 16)` - fully closes the blinds For more elaborated example please refer to [`somfy.lua`](../../lua_examples/somfy.lua). + + +## somfy.listen() + +Using RF receiver listens to Somfy commands and triggers a callback when command is identified. + +#### Syntax +`somfy.listen(pin, callback)` + +#### Parameters +- `pin` GPIO pin the RF receiver is connected to. +- `callback(address, command, rc, frame)` a function called when a Somfy command is identified. Use `nil` to stop listening. + - `address` of the remote controller sending the command + - `command` sent by the remote controller. A number between 0 and 0xf. Can be `somfy.SOMFY_UP`, `somfy.SOMFY_DOWN`, `somfy.SOMFY_PROG`, `somfy.SOMFY_STOP`. + - `rc` rolling code + - `frame` String of 10 characters with the full captured data frame. + +#### Returns +nil + +#### Example +```Lua +somfy.listen(4, function(address, command, rc, frame) + print(("Address:\t0x%x\nCommand:\t0x%x\nRolling code:\t%d"):format(address, command, rc)) + print(("Frame:\t"..("%02x "):rep(#frame)):format(frame:byte(1, -1))) +end) +``` + +Use `somfy.listen()` or `somfy.listen(4, nil)` to unhook the GPIO and free the callback. diff --git a/docs/modules/tm1829.md b/docs/modules/tm1829.md index b47904912c..976a7eec44 100644 --- a/docs/modules/tm1829.md +++ b/docs/modules/tm1829.md @@ -15,7 +15,9 @@ Send data to a led strip using native chip format. `tm1829.write(string)` #### Parameters -- `string` payload to be sent to one or more TM1829 leds. +- `string` payload to be sent to one or more TM1829 leds. It is either + a 3-channel [pixbuf](pixbuf) (e.g., `pixbuf.TYPE_RGB`) or a string of + raw byte values to be sent. #### Returns `nil` diff --git a/docs/modules/ucg.md b/docs/modules/ucg.md index d9dbf1b58f..6aafaa7e18 100644 --- a/docs/modules/ucg.md +++ b/docs/modules/ucg.md @@ -66,7 +66,7 @@ All other pins can be assigned to any available GPIO: * D/C * RES (optional for some displays) -Also refer to the initialization sequence eg in [GraphicsTest.lua](https://github.com/nodemcu/nodemcu-firmware/blob/master/lua_examples/ucglib/GraphicsTest.lua): +Also refer to the initialization sequence eg in [GraphicsTest.lua](https://github.com/nodemcu/nodemcu-firmware/blob/release/lua_examples/ucglib/GraphicsTest.lua): ```lua spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 8) ``` @@ -83,7 +83,7 @@ disp = ucg.ili9341_18x240x320_hw_spi(cs, dc, res) ``` This object provides all of ucglib's methods to control the display. -Again, refer to [GraphicsTest.lua](https://github.com/nodemcu/nodemcu-firmware/blob/master/lua_examples/ucglib/GraphicsTest.lua) to get an impression how this is achieved with Lua code. Visit the [ucglib homepage](https://github.com/olikraus/ucglib) for technical details. +Again, refer to [GraphicsTest.lua](https://github.com/nodemcu/nodemcu-firmware/blob/release/lua_examples/ucglib/GraphicsTest.lua) to get an impression how this is achieved with Lua code. Visit the [ucglib homepage](https://github.com/olikraus/ucglib) for technical details. ### Display selection HW SPI based displays with support in u8g2 can be enabled. diff --git a/docs/modules/wifi.md b/docs/modules/wifi.md index 798d90f1c4..f8f56c9e6f 100644 --- a/docs/modules/wifi.md +++ b/docs/modules/wifi.md @@ -1603,46 +1603,46 @@ T: Table returned by event. #### Example ```lua - wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, function(T) - print("\n\tSTA - CONNECTED".."\n\tSSID: "..T.SSID.."\n\tBSSID: ".. - T.BSSID.."\n\tChannel: "..T.channel) - end) - - wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, function(T) - print("\n\tSTA - DISCONNECTED".."\n\tSSID: "..T.SSID.."\n\tBSSID: ".. - T.BSSID.."\n\treason: "..T.reason) - end) - - wifi.eventmon.register(wifi.eventmon.STA_AUTHMODE_CHANGE, function(T) - print("\n\tSTA - AUTHMODE CHANGE".."\n\told_auth_mode: ".. - T.old_auth_mode.."\n\tnew_auth_mode: "..T.new_auth_mode) - end) - - wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(T) - print("\n\tSTA - GOT IP".."\n\tStation IP: "..T.IP.."\n\tSubnet mask: ".. - T.netmask.."\n\tGateway IP: "..T.gateway) - end) - - wifi.eventmon.register(wifi.eventmon.STA_DHCP_TIMEOUT, function() - print("\n\tSTA - DHCP TIMEOUT") - end) - - wifi.eventmon.register(wifi.eventmon.AP_STACONNECTED, function(T) - print("\n\tAP - STATION CONNECTED".."\n\tMAC: "..T.MAC.."\n\tAID: "..T.AID) - end) - - wifi.eventmon.register(wifi.eventmon.AP_STADISCONNECTED, function(T) - print("\n\tAP - STATION DISCONNECTED".."\n\tMAC: "..T.MAC.."\n\tAID: "..T.AID) - end) - - wifi.eventmon.register(wifi.eventmon.AP_PROBEREQRECVED, function(T) - print("\n\tAP - PROBE REQUEST RECEIVED".."\n\tMAC: ".. T.MAC.."\n\tRSSI: "..T.RSSI) - end) - - wifi.eventmon.register(wifi.eventmon.WIFI_MODE_CHANGED, function(T) - print("\n\tSTA - WIFI MODE CHANGED".."\n\told_mode: ".. - T.old_mode.."\n\tnew_mode: "..T.new_mode) - end) +wifi.eventmon.register(wifi.eventmon.STA_CONNECTED, function(T) + print("\n\tSTA - CONNECTED".."\n\tSSID: "..T.SSID.."\n\tBSSID: ".. + T.BSSID.."\n\tChannel: "..T.channel) +end) + +wifi.eventmon.register(wifi.eventmon.STA_DISCONNECTED, function(T) + print("\n\tSTA - DISCONNECTED".."\n\tSSID: "..T.SSID.."\n\tBSSID: ".. + T.BSSID.."\n\treason: "..T.reason) +end) + +wifi.eventmon.register(wifi.eventmon.STA_AUTHMODE_CHANGE, function(T) + print("\n\tSTA - AUTHMODE CHANGE".."\n\told_auth_mode: ".. + T.old_auth_mode.."\n\tnew_auth_mode: "..T.new_auth_mode) +end) + +wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(T) + print("\n\tSTA - GOT IP".."\n\tStation IP: "..T.IP.."\n\tSubnet mask: ".. + T.netmask.."\n\tGateway IP: "..T.gateway) +end) + +wifi.eventmon.register(wifi.eventmon.STA_DHCP_TIMEOUT, function() + print("\n\tSTA - DHCP TIMEOUT") +end) + +wifi.eventmon.register(wifi.eventmon.AP_STACONNECTED, function(T) + print("\n\tAP - STATION CONNECTED".."\n\tMAC: "..T.MAC.."\n\tAID: "..T.AID) +end) + +wifi.eventmon.register(wifi.eventmon.AP_STADISCONNECTED, function(T) + print("\n\tAP - STATION DISCONNECTED".."\n\tMAC: "..T.MAC.."\n\tAID: "..T.AID) +end) + +wifi.eventmon.register(wifi.eventmon.AP_PROBEREQRECVED, function(T) + print("\n\tAP - PROBE REQUEST RECEIVED".."\n\tMAC: ".. T.MAC.."\n\tRSSI: "..T.RSSI) +end) + +wifi.eventmon.register(wifi.eventmon.WIFI_MODE_CHANGED, function(T) + print("\n\tSTA - WIFI MODE CHANGED".."\n\told_mode: ".. + T.old_mode.."\n\tnew_mode: "..T.new_mode) +end) ``` #### See also - [`wifi.eventmon.unregister()`](#wifieventmonunregister) @@ -1655,7 +1655,7 @@ Unregister callbacks for WiFi event monitor. wifi.eventmon.unregister(Event) #### Parameters -Event: WiFi event you would like to set a callback for. +Event: WiFi event you would like to remove the callback for. - Valid WiFi events: - wifi.eventmon.STA_CONNECTED diff --git a/docs/modules/ws2812.md b/docs/modules/ws2812.md index 5570438148..de2d58fc9c 100644 --- a/docs/modules/ws2812.md +++ b/docs/modules/ws2812.md @@ -13,7 +13,7 @@ handle two led strips at the same time. **WARNING**: In dual mode, you will loose access to the Lua's console through the serial port (it will be reconfigured to support WS2812-like protocol). If you want to keep access to Lua's console, you will have to -use an other input channel like a TCP server (see [example](https://github.com/nodemcu/nodemcu-firmware/blob/master/lua_examples/telnet/telnet.lua)) +use an other input channel like a TCP server (see [example](https://github.com/nodemcu/nodemcu-firmware/blob/release/lua_modules/telnet/telnet.lua)) ## ws2812.init() Initialize UART1 and GPIO2, should be called once and before write(). @@ -31,8 +31,20 @@ In `ws2812.MODE_DUAL` mode you will be able to handle two strips in parallel but `nil` ## ws2812.write() -Send data to one or two led strip using its native format which is generally Green,Red,Blue for RGB strips -and Green,Red,Blue,White for RGBW strips. +Send data to one or two led strip using its native format, which is generally +Green, Red, Blue for RGB strips and Green, Red, Blue, White for RGBW strips. +(However, ws2812 drivers have been observed wired up in other orders.) + +Because this function uses the hardware UART(s), it is able to return and allow +Lua to resume execution up to 300 microseconds before the data has finished +being sent. If you wish to perform actions synchronous with the end of the +data transmission, [`tmr.delay()`](../tmr#tmr.delay()) for 300 microseconds. + +Separately, because this function returns early, back-to-back invocations may +not leave enough time for the strip to latch, and so may appear to the ws2812 +drivers to be simply writes to a longer LED strip. Please ensure that you have +more than 350 microseconds between the return of `ws2812.write()` to your Lua +and the next invocation thereof. #### Syntax `ws2812.write(data1, [data2])` @@ -44,7 +56,7 @@ and Green,Red,Blue,White for RGBW strips. Payload type could be: - `nil` nothing is done - `string` representing bytes to send -- `ws2812.buffer` see [Buffer module](#buffer-module) +- a [pixbuf](pixbuf) object containing the bytes to send. The pixbuf's type is not checked! #### Returns `nil` @@ -70,280 +82,31 @@ ws2812.init(ws2812.MODE_DUAL) ws2812.write(nil, string.char(0, 255, 0, 0, 255, 0)) -- turn the two first RGB leds to red on the second strip, do nothing on the first ``` -# Buffer module +# Pixbuf support For more advanced animations, it is useful to keep a "framebuffer" of the strip, interact with it and flush it to the strip. -For this purpose, the ws2812 library offers a read/write buffer. This buffer has a `__tostring` method so that it can be printed. This is useful for debugging. +For this purpose, the [pixbuf](pixbuf) library offers a read/write buffer and +convenient functions for pixel value manipulation. + +For backwards-compatibility, `pixbuf.newBuffer()` is aliased as +`ws2812.newBuffer`, but this will be removed in the next nodemcu-firmware +release. #### Example Led chaser with a RGBW strip + ```lua ws2812.init() -local i, buffer = 0, ws2812.newBuffer(300, 4); buffer:fill(0, 0, 0, 0); tmr.create():alarm(50, 1, function() + +i, buffer = 0, pixbuf.newBuffer(300, 4) + +buffer:fill(0, 0, 0, 0) + +tmr.create():alarm(50, 1, function() i = i + 1 buffer:fade(2) buffer:set(i % buffer:size() + 1, 0, 0, 0, 255) ws2812.write(buffer) end) ``` - -## ws2812.newBuffer() -Allocate a new memory buffer to store led values. - -#### Syntax -`ws2812.newBuffer(numberOfLeds, bytesPerLed)` - -#### Parameters - - `numberOfLeds` length of the led strip - - `bytesPerLed` 3 for RGB strips and 4 for RGBW strips - -#### Returns -`ws2812.buffer` - -## ws2812.buffer:get() -Return the value at the given position - -#### Syntax -`buffer:get(index)` - -#### Parameters - - `index` position in the buffer (1 for first led) - -#### Returns -`(color)` - -#### Example -```lua -buffer = ws2812.newBuffer(32, 4) -print(buffer:get(1)) -0 0 0 0 -``` - -## ws2812.buffer:set() -Set the value at the given position - -#### Syntax -`buffer:set(index, color)` - -#### Parameters - - `index` position in the buffer (1 for the first led) - - `color` payload of the color - -Payload could be: -- `number, number, ...` you should pass as many arguments as `bytesPerLed` -- `table` should contains `bytesPerLed` numbers -- `string` should contains `bytesPerLed` bytes - -#### Returns -`nil` - -#### Example -```lua -buffer = ws2812.newBuffer(32, 3) -buffer:set(1, 255, 0, 0) -- set the first led green for a RGB strip -``` - -```lua -buffer = ws2812.newBuffer(32, 4) -buffer:set(1, {0, 0, 0, 255}) -- set the first led white for a RGBW strip -``` - -```lua -buffer = ws2812.newBuffer(32, 3) -buffer:set(1, string.char(255, 0, 0)) -- set the first led green for a RGB strip -``` - -## ws2812.buffer:size() -Return the size of the buffer in number of leds - -#### Syntax -`buffer:size()` - -#### Parameters -none - -#### Returns -`int` - -## ws2812.buffer:fill() -Fill the buffer with the given color. -The number of given bytes must match the number of bytesPerLed of the buffer - -#### Syntax -`buffer:fill(color)` - -#### Parameters - - `color` bytes of the color, you should pass as many arguments as `bytesPerLed` - -#### Returns -`nil` - -#### Example -```lua -buffer:fill(0, 0, 0) -- fill the buffer with black for a RGB strip -``` - -## ws2812.buffer:dump() -Returns the contents of the buffer (the pixel values) as a string. This can then be saved to a file or sent over a network. - -#### Syntax -`buffer:dump()` - -#### Returns -A string containing the pixel values. - -#### Example -```lua -local s = buffer:dump() -``` - -## ws2812.buffer:replace() -Inserts a string (or a buffer) into another buffer with an offset. -The buffer must have the same number of colors per led or an error will be thrown. - -#### Syntax -`buffer:replace(source[, offset])` - -#### Parameters - - `source` the pixel values to be set into the buffer. This is either a string or a buffer. - - `offset` the offset where the source is to be placed in the buffer. Default is 1. Negative values can be used. - -#### Returns -`nil` - -#### Example -```lua -buffer:replace(anotherbuffer:dump()) -- copy one buffer into another via a string -buffer:replace(anotherbuffer) -- copy one buffer into another -newbuffer = buffer.sub(1) -- make a copy of a buffer into a new buffer -``` - -## ws2812.buffer:mix() -This is a general method that loads data into a buffer that is a linear combination of data from other buffers. It can be used to copy a buffer or, -more usefully, do a cross fade. The pixel values are computed as integers and then range limited to [0, 255]. This means that negative -factors work as expected, and that the order of combining buffers does not matter. - -#### Syntax -`buffer:mix(factor1, buffer1, ...)` - -#### Parameters - - `factor1` This is the factor that the contents of `buffer1` are multiplied by. This factor is scaled by a factor of 256. Thus `factor1` value of 256 is a factor of 1.0. - - `buffer1` This is the source buffer. It must be of the same shape as the destination buffer. - -There can be any number of factor/buffer pairs. - -#### Returns -`nil` - -#### Example -```lua --- loads buffer with a crossfade between buffer1 and buffer2 -buffer:mix(256 - crossmix, buffer1, crossmix, buffer2) - --- multiplies all values in buffer by 0.75 --- This can be used in place of buffer:fade -buffer:mix(192, buffer) -``` - -## ws2812.buffer:power() -Computes the total energy requirement for the buffer. This is merely the total sum of all the pixel values (which assumes that each color in each -pixel consumes the same amount of power). A real WS2812 (or WS2811) has three constant current drivers of 20mA -- one for each of R, G and B. The -pulse width modulation will cause the *average* current to scale linearly with pixel value. - -#### Syntax -`buffer:power()` - -#### Returns -An integer which is the sum of all the pixel values. - -#### Example -```lua --- Dim the buffer to no more than the PSU can provide -local psu_current_ma = 1000 -local led_current_ma = 20 -local led_sum = psu_current_ma * 255 / led_current_ma - -local p = buffer:power() -if p > led_sum then - buffer:mix(256 * led_sum / p, buffer) -- power is now limited -end -``` - -## ws2812.buffer:fade() -Fade in or out. Defaults to out. Multiply or divide each byte of each led with/by the given value. Useful for a fading effect. - -#### Syntax -`buffer:fade(value [, direction])` - -#### Parameters - - `value` value by which to divide or multiply each byte - - `direction` ws2812.FADE\_IN or ws2812.FADE\_OUT. Defaults to ws2812.FADE\_OUT - -#### Returns -`nil` - -#### Example -```lua -buffer:fade(2) -buffer:fade(2, ws2812.FADE_IN) -``` -## ws2812.buffer:shift() -Shift the content of (a piece of) the buffer in positive or negative direction. This allows simple animation effects. A slice of the buffer can be specified by using the -standard start and end offset Lua notation. Negative values count backwards from the end of the buffer. - -#### Syntax -`buffer:shift(value [, mode[, i[, j]]])` - -#### Parameters - - `value` number of pixels by which to rotate the buffer. Positive values rotate forwards, negative values backwards. - - `mode` is the shift mode to use. Can be one of `ws2812.SHIFT_LOGICAL` or `ws2812.SHIFT_CIRCULAR`. In case of SHIFT\_LOGICAL, the freed pixels are set to 0 (off). In case of SHIFT\_CIRCULAR, the buffer is treated like a ring buffer, inserting the pixels falling out on one end again on the other end. Defaults to SHIFT\_LOGICAL. - - `i` is the first offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is 1. - - `j` is the last offset in the buffer to be affected. Negative values are permitted and count backwards from the end. Default is -1. - -#### Returns -`nil` - -#### Example -```lua -buffer:shift(3) -``` - -## ws2812.buffer:sub() -This implements the extraction function like `string.sub`. The indexes are in leds and all the same rules apply. - -#### Syntax -`buffer1:sub(i[, j])` - -#### Parameters - - `i` This is the start of the extracted data. Negative values can be used. - - `j` this is the end of the extracted data. Negative values can be used. The default is -1. - -#### Returns -A buffer containing the extracted piece. - -#### Example -``` -b = buffer:sub(1,10) -``` - -## ws2812.buffer:__concat() -This implements the `..` operator to concatenate two buffers. They must have the same number of colors per led. - -#### Syntax -`buffer1 .. buffer2` - -#### Parameters - - `buffer1` this is the start of the resulting buffer - - `buffer2` this is the end of the resulting buffer - -#### Returns -The concatenated buffer. - -#### Example -``` -ws2812.write(buffer1 .. buffer2) -``` - - - diff --git a/lua_examples/pipeutils.lua b/lua_examples/pipeutils.lua new file mode 100644 index 0000000000..9caf9ab0a0 --- /dev/null +++ b/lua_examples/pipeutils.lua @@ -0,0 +1,54 @@ +-- A collection of pipe-based utility functions + +-- A convenience wrapper for chunking data arriving in bursts into more sizable +-- blocks; `o` will be called once per chunk. The `flush` method can be used to +-- drain the internal buffer. `flush` MUST be called at the end of the stream, +-- **even if the stream is a multiple of the chunk size** due to internal +-- buffering. Flushing results in smaller chunk(s) being output, of course. +local function chunker(o, csize, prio) + assert (type(o) == "function" and type(csize) == "number" and 1 <= csize) + local p = pipe.create(function(p) + -- wait until it looks very likely that read is going to succeed + -- and we won't have to unread. This may hold slightly more than + -- a chunk in the underlying pipe object. + if 256 * (p:nrec() - 1) <= csize then return nil end + + local d = p:read(csize) + if #d < csize + then p:unread(d) return false + else o(d) return true + end + end, prio or node.task.LOW_PRIORITY) + + return { + flush = function() for d in p:reader(csize) do o(d) end end, + write = function(d) p:write(d) end + } +end + +-- Stream and decode lines of complete base64 blocks, calling `o(data)` with +-- decoded chunks or calling `e(badinput, errorstr)` on error; the error +-- callback must ensure that this conduit is never written to again. +local function debase64(o, e, prio) + assert (type(o) == "function" and type(e) == "function") + local p = pipe.create(function(p) + local s = p:read("\n+") + if s:sub(-1) == "\n" then -- guard against incomplete line + s = s:match("^%s*(%S*)%s*$") + if #s ~= 0 then -- guard against empty line + local ok, d = pcall(encoder.fromBase64, s) + if ok then o(d) else e(s, d); return false end + end + return true + else + p:unread(s) + return false + end + end, prio or node.task.LOW_PRIORITY) + return { write = function(d) p:write(d) end } +end + +return { + chunker = chunker, + debase64 = debase64, +} diff --git a/lua_modules/bme280/bme280.lua b/lua_modules/bme280/bme280.lua index ed0692b834..5fd4bfabe9 100644 --- a/lua_modules/bme280/bme280.lua +++ b/lua_modules/bme280/bme280.lua @@ -130,10 +130,10 @@ function bme280_startreadout(self, callback, delay, alt) delay = delay or BME280_SAMPLING_DELAY if self._isbme then write_reg(self.id, self.addr, BME280_REGISTER_CONTROL_HUM, self._config[2]) end - write_reg(self.id, self.addr, BME280_REGISTER_CONTROL, math_floor(self._config[3]:byte(1)/4)+ 1) - -- math_floor(self._config[3]:byte(1)/4)+ 1 + write_reg(self.id, self.addr, BME280_REGISTER_CONTROL, 4*math_floor(self._config[3]:byte(1)/4)+ 1) -- LUA51 + -- 4*math_floor(self._config[3]:byte(1)/4)+ 1 -- an awful way to avoid bit operations but calculate (config[3] & 0xFC) | BME280_FORCED_MODE - -- Lua 5.3 integer division // would be more suitable + -- Lua 5.3: write_reg(self.id, self.addr, BME280_REGISTER_CONTROL, (self._config[3]:byte(1) & 0xFC) | 1) tmr_create():alarm(delay, tmr_ALARM_SINGLE, function() diff --git a/lua_modules/ds18b20/ds18b20-integer.lua b/lua_modules/ds18b20/ds18b20-integer.lua new file mode 100644 index 0000000000..7e40a84bac --- /dev/null +++ b/lua_modules/ds18b20/ds18b20-integer.lua @@ -0,0 +1,209 @@ +-------------------------------------------------------------------------------- +-- DS18B20 one wire module for NODEMCU +-- NODEMCU TEAM +-- LICENCE: http://opensource.org/licenses/MIT +-- @voborsky, @devsaurus, TerryE 26 Mar 2017 +-------------------------------------------------------------------------------- +local modname = ... + +-- Used modules and functions +local type, tostring, pcall, ipairs = + type, tostring, pcall, ipairs +-- Local functions +local ow_setup, ow_search, ow_select, ow_read, ow_read_bytes, ow_write, ow_crc8, + ow_reset, ow_reset_search, ow_skip, ow_depower = + ow.setup, ow.search, ow.select, ow.read, ow.read_bytes, ow.write, ow.crc8, + ow.reset, ow.reset_search, ow.skip, ow.depower + +local node_task_post, node_task_LOW_PRIORITY = node.task.post, node.task.LOW_PRIORITY +local string_char, string_dump = string.char, string.dump +local now, tmr_create, tmr_ALARM_SINGLE = tmr.now, tmr.create, tmr.ALARM_SINGLE +local table_sort, table_concat = table.sort, table.concat +local file_open = file.open +local conversion + +local DS18B20FAMILY = 0x28 +local DS1920FAMILY = 0x10 -- and DS18S20 series +local CONVERT_T = 0x44 +local READ_SCRATCHPAD = 0xBE +local READ_POWERSUPPLY= 0xB4 +local MODE = 1 + +local pin, cb, unit = 3 +local status = {} + +local debugPrint = function() return end + +-------------------------------------------------------------------------------- +-- Implementation +-------------------------------------------------------------------------------- +local function enable_debug() + debugPrint = function (...) print(now(),' ', ...) end +end + +local function to_string(addr, esc) + if type(addr) == 'string' and #addr == 8 then + return ( esc == true and + '"\\%u\\%u\\%u\\%u\\%u\\%u\\%u\\%u"' or + '%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X '):format(addr:byte(1,8)) + else + return tostring(addr) + end +end + +local function readout(self) + local next = false + local sens = self.sens + local temp = self.temp + for i, s in ipairs(sens) do + if status[i] == 1 then + ow_reset(pin) + local addr = s:sub(1,8) + ow_select(pin, addr) -- select the sensor + ow_write(pin, READ_SCRATCHPAD, MODE) + local data = ow_read_bytes(pin, 9) + + local t=(data:byte(1)+data:byte(2)*256) + -- t is actually signed so process the sign bit and adjust for fractional bits + -- the DS18B20 family has 4 fractional bits and the DS18S20s, 1 fractional bit + t = ((t <= 32767) and t or t - 65536) * + ((addr:byte(1) == DS18B20FAMILY) and 625 or 5000) + local crc, b9 = ow_crc8(string.sub(data,1,8)), data:byte(9) + + if unit == 'F' then + t = (t * 18)/10 + 320000 + elseif unit == 'K' then + t = t + 2731500 + end + local sgn = t<0 and -1 or 1 + local tA = sgn*t + local tH=tA/10000 + local tL=(tA%10000)/1000 + ((tA%1000)/100 >= 5 and 1 or 0) + + if tH and (t~=850000) then + debugPrint(to_string(addr),(sgn<0 and "-" or "")..tH.."."..tL, crc, b9) + if crc==b9 then temp[addr]=t end + status[i] = 2 + end + end + next = next or status[i] == 0 + end + if next then + node_task_post(node_task_LOW_PRIORITY, function() return conversion(self) end) + else + --sens = {} + if cb then + node_task_post(node_task_LOW_PRIORITY, function() return cb(temp) end) + end + end +end + +conversion = (function (self) + local sens = self.sens + local powered_only = true + for _, s in ipairs(sens) do powered_only = powered_only and s:byte(9) ~= 1 end + if powered_only then + debugPrint("starting conversion: all sensors") + ow_reset(pin) + ow_skip(pin) -- skip ROM selection, talk to all sensors + ow_write(pin, CONVERT_T, MODE) -- and start conversion + for i, _ in ipairs(sens) do status[i] = 1 end + else + local started = false + for i, s in ipairs(sens) do + if status[i] == 0 then + local addr, parasite = s:sub(1,8), s:byte(9) == 1 + if parasite and started then break end -- do not start concurrent conversion of powered and parasite + debugPrint("starting conversion:", to_string(addr), parasite and "parasite" or "") + ow_reset(pin) + ow_select(pin, addr) -- select the sensor + ow_write(pin, CONVERT_T, MODE) -- and start conversion + status[i] = 1 + if parasite then break end -- parasite sensor blocks bus during conversion + started = true + end + end + end + tmr_create():alarm(750, tmr_ALARM_SINGLE, function() return readout(self) end) +end) + +local function _search(self, lcb, lpin, search, save) + self.temp = {} + if search then self.sens = {}; status = {} end + local sens = self.sens + pin = lpin or pin + + local addr + if not search and #sens == 0 then + -- load addreses if available + debugPrint ("geting addreses from flash") + local s,check,a = pcall(dofile, "ds18b20_save.lc") + if s and check == "ds18b20" then + for i = 1, #a do sens[i] = a[i] end + end + debugPrint (#sens, "addreses found") + end + + ow_setup(pin) + if search or #sens == 0 then + ow_reset_search(pin) + -- ow_target_search(pin,0x28) + -- search the first device + addr = ow_search(pin) + else + for i, _ in ipairs(sens) do status[i] = 0 end + end + local function cycle() + if addr then + local crc=ow_crc8(addr:sub(1,7)) + if (crc==addr:byte(8)) and ((addr:byte(1)==DS1920FAMILY) or (addr:byte(1)==DS18B20FAMILY)) then + ow_reset(pin) + ow_select(pin, addr) + ow_write(pin, READ_POWERSUPPLY, MODE) + local parasite = (ow_read(pin)==0 and 1 or 0) + sens[#sens+1]= addr..string_char(parasite) + status[#sens] = 0 + debugPrint("contact: ", to_string(addr), parasite == 1 and "parasite" or "") + end + addr = ow_search(pin) + node_task_post(node_task_LOW_PRIORITY, cycle) + else + ow_depower(pin) + -- place powered sensors first + table_sort(sens, function(a, b) return a:byte(9)= 5 and 1 or 0) - - if tH and (t~=850000) then - debugPrint(to_string(addr),(sgn<0 and "-" or "")..tH.."."..tL, crc, b9) - if crc==b9 then temp[addr]=(sgn<0 and "-" or "")..tH.."."..tL end - status[i] = 2 - end - -- end integer version - else - -- float version - t = t / 10000 - if math_floor(t)~=85 then - if unit == 'F' then - t = t * 18/10 + 32 - elseif unit == 'K' then - t = t + 27315/100 - end - debugPrint(to_string(addr), t, crc, b9) - if crc==b9 then temp[addr]=t end - status[i] = 2 - end - -- end float version + debugPrint(to_string(addr), t, crc, b9) + if crc==b9 then temp[addr]=t end + status[i] = 2 end end next = next or status[i] == 0 diff --git a/lua_modules/http/httpserver.lua b/lua_modules/http/httpserver.lua index a500613e90..edab6028f8 100644 --- a/lua_modules/http/httpserver.lua +++ b/lua_modules/http/httpserver.lua @@ -86,19 +86,22 @@ do local buf = "" local method, url + local ondisconnect = function(connection) + connection:on("receive", nil) + connection:on("disconnection", nil) + connection:on("sent", nil) + collectgarbage("collect") + end + local cfini = function() - conn:on("receive", nil) - conn:on("disconnection", nil) csend(function() conn:on("sent", nil) conn:close() + ondisconnect(conn) end) end - local ondisconnect = function(connection) - connection:on("sent", nil) - collectgarbage("collect") - end + -- header parser local cnt_len = 0 diff --git a/lua_modules/liquidcrystal/lc-i2c4bit.lua b/lua_modules/liquidcrystal/lc-i2c4bit.lua index dc4011b623..0d286a02ae 100644 --- a/lua_modules/liquidcrystal/lc-i2c4bit.lua +++ b/lua_modules/liquidcrystal/lc-i2c4bit.lua @@ -22,27 +22,27 @@ return function(bus_args) -- The onus is on us to maintain the backlight state local backlight = true - local function send4bitI2C(value, rs_en, rw_en, read) - local function exchange(data, unset_read) - local rv = data + local function exchange(data, read) + local rv = data + i2c.start(busid) + i2c.address(busid, busad, i2c.TRANSMITTER) + i2c.write(busid, bit.set(data, en)) -- set data with en + if read then + i2c.start(busid) -- read 1 byte and go back to tx mode + i2c.address(busid, busad, i2c.RECEIVER) + rv = i2c.read(busid, 1):byte(1) i2c.start(busid) i2c.address(busid, busad, i2c.TRANSMITTER) - i2c.write(busid, bit.set(data, en)) - if read then - i2c.start(busid) - i2c.address(busid, busad, i2c.RECEIVER) - rv = i2c.read(busid, 1):byte(1) - i2c.start(busid) - i2c.address(busid, busad, i2c.TRANSMITTER) - if unset_read then data = bit.bor(bit.bit(rs), - bit.bit(rw), - backlight and bit.bit(bl) or 0) end - i2c.write(busid, bit.set(data, en)) - end - i2c.write(busid, bit.clear(data, en)) - i2c.stop(busid) - return rv end + i2c.write(busid, data) -- lower en + i2c.stop(busid) + return rv + end + + local function send4bitI2C(value, rs_en, rw_en) + local meta = bit.bor(rs_en and bit.bit(rs) or 0, + rw_en and bit.bit(rw) or 0, + backlight and bit.bit(bl) or 0) local lo = bit.bor(bit.isset(value, 0) and bit.bit(d4) or 0, bit.isset(value, 1) and bit.bit(d5) or 0, bit.isset(value, 2) and bit.bit(d6) or 0, @@ -51,11 +51,8 @@ return function(bus_args) bit.isset(value, 5) and bit.bit(d5) or 0, bit.isset(value, 6) and bit.bit(d6) or 0, bit.isset(value, 7) and bit.bit(d7) or 0) - local cmd = bit.bor(rs_en and bit.bit(rs) or 0, - rw_en and bit.bit(rw) or 0, - backlight and bit.bit(bl) or 0) - hi = exchange(bit.bor(cmd, hi), false) - lo = exchange(bit.bor(cmd, lo), true) + hi = exchange(bit.bor(meta, hi), rw_en) + lo = exchange(bit.bor(meta, lo), rw_en) return bit.bor(bit.lshift(bit.isset(lo, d4) and 1 or 0, 0), bit.lshift(bit.isset(lo, d5) and 1 or 0, 1), bit.lshift(bit.isset(lo, d6) and 1 or 0, 2), @@ -66,36 +63,45 @@ return function(bus_args) bit.lshift(bit.isset(hi, d7) and 1 or 0, 7)) end - -- init sequence from datasheet - send4bitI2C(0x33, false, false, false) - send4bitI2C(0x32, false, false, false) + -- init sequence from datasheet (Figure 24) + local function justsend(what) + i2c.start(busid) + i2c.address(busid, busad, i2c.TRANSMITTER) + i2c.write(busid, bit.set(what, en)) + i2c.write(busid, what) + i2c.stop(busid) + end + local three = bit.bor(bit.bit(d4), bit.bit(d5)) + justsend(three) + tmr.delay(5) + justsend(three) + tmr.delay(1) + justsend(three) + tmr.delay(1) + justsend(bit.bit(d5)) + -- we are now primed for the FUNCTIONSET command from the liquidcrystal ctor -- Return backend object return { fourbits = true, command = function (_, cmd) - return send4bitI2C(cmd, false, false, false) + return send4bitI2C(cmd, false, false) end, busy = function(_) - local rv = send4bitI2C(0xff, false, true, true) - send4bitI2C(bit.bor(0x80, bit.clear(rv, 7)), false, false, false) - return bit.isset(rv, 7) + return bit.isset(send4bitI2C(0xff, false, true), 7) end, position = function(_) - local rv = bit.clear(send4bitI2C(0xff, false, true, true), 7) - send4bitI2C(bit.bor(0x80, rv), false, false, false) - return rv + return bit.clear(send4bitI2C(0xff, false, true), 7) end, write = function(_, value) - return send4bitI2C(value, true, false, false) + return send4bitI2C(value, true, false) end, read = function(_) - return send4bitI2C(0xff, true, true, true) + return send4bitI2C(0xff, true, true) end, backlight = function(_, on) backlight = on - local rv = bit.clear(send4bitI2C(0xff, false, true, true), 7) - send4bitI2C(bit.bor(0x80, rv), false, false, false) + send4bitI2C(0, false, false) -- No-op return on end, } diff --git a/lua_tests/mispec.lua b/lua_tests/mispec.lua deleted file mode 100644 index ca143d15d9..0000000000 --- a/lua_tests/mispec.lua +++ /dev/null @@ -1,159 +0,0 @@ -local moduleName = ... or 'mispec' -local M = {} -_G[moduleName] = M - --- Helpers: -function ok(expression, desc) - if expression == nil then expression = false end - desc = desc or 'expression is not ok' - if not expression then - error(desc .. '\n' .. debug.traceback()) - end -end - -function ko(expression, desc) - if expression == nil then expression = false end - desc = desc or 'expression is not ko' - if expression then - error(desc .. '\n' .. debug.traceback()) - end -end - -function eq(a, b) - if type(a) ~= type(b) then - error('type ' .. type(a) .. ' is not equal to ' .. type(b) .. '\n' .. debug.traceback()) - end - if type(a) == 'function' then - return string.dump(a) == string.dump(b) - end - if a == b then return true end - if type(a) ~= 'table' then - error(string.format("%q",tostring(a)) .. ' is not equal to ' .. string.format("%q",tostring(b)) .. '\n' .. debug.traceback()) - end - for k,v in pairs(a) do - if b[k] == nil or not eq(v, b[k]) then return false end - end - for k,v in pairs(b) do - if a[k] == nil or not eq(v, a[k]) then return false end - end - return true -end - -function failwith(message, func, ...) - local status, err = pcall(func, ...) - if status then - local messagePart = "" - if message then - messagePart = " containing \"" .. message .. "\"" - end - error("Error expected" .. messagePart .. '\n' .. debug.traceback()) - end - if (message and not string.find(err, message)) then - error("expected errormessage \"" .. err .. "\" to contain \"" .. message .. "\"" .. '\n' .. debug.traceback() ) - end - return true -end - -function fail(func, ...) - return failwith(nil, func, ...) -end - -local function eventuallyImpl(func, retries, delayMs) - local prevEventually = _G.eventually - _G.eventually = function() error("Cannot nest eventually/andThen.") end - local status, err = pcall(func) - _G.eventually = prevEventually - if status then - M.queuedEventuallyCount = M.queuedEventuallyCount - 1 - M.runNextPending() - else - if retries > 0 then - local t = tmr.create() - t:register(delayMs, tmr.ALARM_SINGLE, M.runNextPending) - t:start() - - table.insert(M.pending, 1, function() eventuallyImpl(func, retries - 1, delayMs) end) - else - M.failed = M.failed + 1 - print("\n ! it failed:", err) - - -- remove all pending eventuallies as spec has failed at this point - for i = 1, M.queuedEventuallyCount - 1 do - table.remove(M.pending, 1) - end - M.queuedEventuallyCount = 0 - M.runNextPending() - end - end -end - -function eventually(func, retries, delayMs) - retries = retries or 10 - delayMs = delayMs or 300 - - M.queuedEventuallyCount = M.queuedEventuallyCount + 1 - - table.insert(M.pending, M.queuedEventuallyCount, function() - eventuallyImpl(func, retries, delayMs) - end) -end - -function andThen(func) - eventually(func, 0, 0) -end - -function describe(name, itshoulds) - M.name = name - M.itshoulds = itshoulds -end - --- Module: -M.runNextPending = function() - local next = table.remove(M.pending, 1) - if next then - node.task.post(next) - next = nil - else - M.succeeded = M.total - M.failed - local elapsedSeconds = (tmr.now() - M.startTime) / 1000 / 1000 - print(string.format( - '\n\nCompleted in %d seconds; %d failed out of %d.', - elapsedSeconds, M.failed, M.total)) - M.pending = nil - M.queuedEventuallyCount = nil - end -end - -M.run = function() - M.pending = {} - M.queuedEventuallyCount = 0 - M.startTime = tmr.now() - M.total = 0 - M.failed = 0 - local it = {} - it.should = function(_, desc, func) - table.insert(M.pending, function() - print('\n * ' .. desc) - M.total = M.total + 1 - if M.pre then M.pre() end - local status, err = pcall(func) - if not status then - print("\n ! it failed:", err) - M.failed = M.failed + 1 - end - if M.post then M.post() end - M.runNextPending() - end) - end - it.initialize = function(_, pre) M.pre = pre end; - it.cleanup = function(_, post) M.post = post end; - M.itshoulds(it) - - print('' .. M.name .. ', it should:') - M.runNextPending() - - M.itshoulds = nil - M.name = nil -end - -print ("loaded mispec") diff --git a/lua_tests/mispec_ws2812.lua b/lua_tests/mispec_ws2812.lua deleted file mode 100644 index 9538ca0c0b..0000000000 --- a/lua_tests/mispec_ws2812.lua +++ /dev/null @@ -1,153 +0,0 @@ -require 'mispec' - -local buffer, buffer1, buffer2 - -local function initBuffer(buffer, ...) - local i,v - for i,v in ipairs({...}) do - buffer:set(i, v, v*2, v*3, v*4) - end - return buffer -end - -local function equalsBuffer(buffer1, buffer2) - return eq(buffer1:dump(), buffer2:dump()) -end - - -describe('WS2812 buffers', function(it) - - it:should('initialize a buffer', function() - buffer = ws2812.newBuffer(9, 3) - ko(buffer == nil) - ok(eq(buffer:size(), 9), "check size") - ok(eq(buffer:dump(), string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)), "initialize with 0") - - failwith("should be a positive integer", ws2812.newBuffer, 9, 0) - failwith("should be a positive integer", ws2812.newBuffer, 9, -1) - failwith("should be a positive integer", ws2812.newBuffer, 0, 3) - failwith("should be a positive integer", ws2812.newBuffer, -1, 3) - end) - - it:should('have correct size', function() - buffer = ws2812.newBuffer(9, 3) - ok(eq(buffer:size(), 9), "check size") - buffer = ws2812.newBuffer(9, 22) - ok(eq(buffer:size(), 9), "check size") - buffer = ws2812.newBuffer(13, 1) - ok(eq(buffer:size(), 13), "check size") - end) - - it:should('fill a buffer with one color', function() - buffer = ws2812.newBuffer(3, 3) - buffer:fill(1,222,55) - ok(eq(buffer:dump(), string.char(1,222,55,1,222,55,1,222,55)), "RGB") - buffer = ws2812.newBuffer(3, 4) - buffer:fill(1,222,55, 77) - ok(eq(buffer:dump(), string.char(1,222,55,77,1,222,55,77,1,222,55,77)), "RGBW") - end) - - it:should('replace correctly', function() - buffer = ws2812.newBuffer(5, 3) - buffer:replace(string.char(3,255,165,33,0,244,12,87,255)) - ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") - - buffer = ws2812.newBuffer(5, 3) - buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 2) - ok(eq(buffer:dump(), string.char(0,0,0,3,255,165,33,0,244,12,87,255,0,0,0)), "RGBW") - - buffer = ws2812.newBuffer(5, 3) - buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5) - ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") - - failwith("Does not fit into destination", function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end) - end) - - it:should('replace correctly issue #2921', function() - local buffer = ws2812.newBuffer(5, 3) - buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -7) - ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") - end) - - it:should('get/set correctly', function() - buffer = ws2812.newBuffer(3, 4) - buffer:fill(1,222,55,13) - ok(eq({buffer:get(2)},{1,222,55,13})) - buffer:set(2, 4,53,99,0) - ok(eq({buffer:get(1)},{1,222,55,13})) - ok(eq({buffer:get(2)},{4,53,99,0})) - ok(eq(buffer:dump(), string.char(1,222,55,13,4,53,99,0,1,222,55,13)), "RGBW") - - failwith("index out of range", function() buffer:get(0) end) - failwith("index out of range", function() buffer:get(4) end) - failwith("index out of range", function() buffer:set(0,1,2,3,4) end) - failwith("index out of range", function() buffer:set(4,1,2,3,4) end) - failwith("number expected, got no value", function() buffer:set(2,1,2,3) end) --- failwith("extra values given", function() buffer:set(2,1,2,3,4,5) end) - end) - - it:should('fade correctly', function() - buffer = ws2812.newBuffer(1, 3) - buffer:fill(1,222,55) - buffer:fade(2) - ok(buffer:dump() == string.char(0,111,27), "RGB") - buffer:fill(1,222,55) - buffer:fade(3, ws2812.FADE_OUT) - ok(buffer:dump() == string.char(0,222/3,55/3), "RGB") - buffer:fill(1,222,55) - buffer:fade(3, ws2812.FADE_IN) - ok(buffer:dump() == string.char(3,255,165), "RGB") - buffer = ws2812.newBuffer(1, 4) - buffer:fill(1,222,55, 77) - buffer:fade(2, ws2812.FADE_OUT) - ok(eq(buffer:dump(), string.char(0,111,27,38)), "RGBW") - end) - - it:should('mix correctly issue #1736', function() - buffer1 = ws2812.newBuffer(1, 3) - buffer2 = ws2812.newBuffer(1, 3) - buffer1:fill(10,22,54) - buffer2:fill(10,27,55) - buffer1:mix(256/8*7,buffer1,256/8,buffer2) - ok(eq({buffer1:get(1)}, {10,23,54})) - end) - - it:should('mix saturation correctly ', function() - buffer1 = ws2812.newBuffer(1, 3) - buffer2 = ws2812.newBuffer(1, 3) - - buffer1:fill(10,22,54) - buffer2:fill(10,27,55) - buffer1:mix(256/2,buffer1,-256,buffer2) - ok(eq({buffer1:get(1)}, {0,0,0})) - - buffer1:fill(10,22,54) - buffer2:fill(10,27,55) - buffer1:mix(25600,buffer1,256/8,buffer2) - ok(eq({buffer1:get(1)}, {255,255,255})) - - buffer1:fill(10,22,54) - buffer2:fill(10,27,55) - buffer1:mix(-257,buffer1,255,buffer2) - ok(eq({buffer1:get(1)}, {0,5,1})) - end) - - it:should('mix with strings correctly ', function() - buffer1 = ws2812.newBuffer(1, 3) - buffer2 = ws2812.newBuffer(1, 3) - - buffer1:fill(10,22,54) - buffer2:fill(10,27,55) - buffer1:mix(-257,buffer1:dump(),255,buffer2:dump()) - ok(eq({buffer1:get(1)}, {0,5,1})) - end) - - it:should('power', function() - buffer = ws2812.newBuffer(2, 4) - buffer:fill(10,22,54,234) - ok(eq(buffer:power(), 2*(10+22+54+234))) - end) - -end) - -mispec.run() diff --git a/lua_tests/mispec_ws2812_2.lua b/lua_tests/mispec_ws2812_2.lua deleted file mode 100644 index a9b4056bbc..0000000000 --- a/lua_tests/mispec_ws2812_2.lua +++ /dev/null @@ -1,149 +0,0 @@ -require 'mispec' - -local buffer, buffer1, buffer2 - -local function initBuffer(buffer, ...) - local i,v - for i,v in ipairs({...}) do - buffer:set(i, v, v*2, v*3, v*4) - end - return buffer -end - -local function equalsBuffer(buffer1, buffer2) - return eq(buffer1:dump(), buffer2:dump()) -end - - -describe('WS2812 buffers', function(it) - - it:should('shift LOGICAL', function() - - buffer1 = ws2812.newBuffer(4, 4) - buffer2 = ws2812.newBuffer(4, 4) - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,0,0,7,8) - buffer1:shift(2) - ok(equalsBuffer(buffer1, buffer2), "shift right") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,9,12,0,0) - buffer1:shift(-2) - ok(equalsBuffer(buffer1, buffer2), "shift left") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,7,0,8,12) - buffer1:shift(1, nil, 2,3) - ok(equalsBuffer(buffer1, buffer2), "shift middle right") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,7,9,0,12) - buffer1:shift(-1, nil, 2,3) - ok(equalsBuffer(buffer1, buffer2), "shift middle left") - - -- bounds checks, handle gracefully as string:sub does - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,8,9,12,0) - buffer1:shift(-1, ws2812.SHIFT_LOGICAL, 0,5) - ok(equalsBuffer(buffer1, buffer2), "shift left out of bound") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,0,7,8,9) - buffer1:shift(1, ws2812.SHIFT_LOGICAL, 0,5) - ok(equalsBuffer(buffer1, buffer2), "shift right out of bound") - - end) - - it:should('shift LOGICAL issue #2946', function() - buffer1 = ws2812.newBuffer(4, 4) - buffer2 = ws2812.newBuffer(4, 4) - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,0,0,0,0) - buffer1:shift(4) - ok(equalsBuffer(buffer1, buffer2), "shift all right") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,0,0,0,0) - buffer1:shift(-4) - ok(equalsBuffer(buffer1, buffer2), "shift all left") - - failwith("shifting more elements than buffer size", function() buffer1:shift(10) end) - failwith("shifting more elements than buffer size", function() buffer1:shift(-6) end) - end) - - it:should('shift CIRCULAR', function() - buffer1 = ws2812.newBuffer(4, 4) - buffer2 = ws2812.newBuffer(4, 4) - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,9,12,7,8) - buffer1:shift(2, ws2812.SHIFT_CIRCULAR) - ok(equalsBuffer(buffer1, buffer2), "shift right") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,9,12,7,8) - buffer1:shift(-2, ws2812.SHIFT_CIRCULAR) - ok(equalsBuffer(buffer1, buffer2), "shift left") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,7,9,8,12) - buffer1:shift(1, ws2812.SHIFT_CIRCULAR, 2,3) - ok(equalsBuffer(buffer1, buffer2), "shift middle right") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,7,9,8,12) - buffer1:shift(-1, ws2812.SHIFT_CIRCULAR, 2,3) - ok(equalsBuffer(buffer1, buffer2), "shift middle left") - - -- bounds checks, handle gracefully as string:sub does - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,8,9,12,7) - buffer1:shift(-1, ws2812.SHIFT_CIRCULAR, 0,5) - ok(equalsBuffer(buffer1, buffer2), "shift left out of bound") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,12,7,8,9) - buffer1:shift(1, ws2812.SHIFT_CIRCULAR, 0,5) - ok(equalsBuffer(buffer1, buffer2), "shift right out of bound") - - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,12,7,8,9) - buffer1:shift(1, ws2812.SHIFT_CIRCULAR, -12,12) - ok(equalsBuffer(buffer1, buffer2), "shift right way out of bound") - - end) - - it:should('sub', function() - buffer1 = ws2812.newBuffer(4, 4) - buffer2 = ws2812.newBuffer(4, 4) - initBuffer(buffer1,7,8,9,12) - buffer1 = buffer1:sub(4,3) - ok(eq(buffer1:size(), 0), "sub empty") - - buffer1 = ws2812.newBuffer(4, 4) - buffer2 = ws2812.newBuffer(2, 4) - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,9,12) - buffer1 = buffer1:sub(3,4) - ok(equalsBuffer(buffer1, buffer2), "sub") - - buffer1 = ws2812.newBuffer(4, 4) - buffer2 = ws2812.newBuffer(4, 4) - initBuffer(buffer1,7,8,9,12) - initBuffer(buffer2,7,8,9,12) - buffer1 = buffer1:sub(-12,33) - ok(equalsBuffer(buffer1, buffer2), "out of bounds") - end) - - - - ---[[ -ws2812.buffer:__concat() ---]] - -end) - -mispec.run() diff --git a/lua_tests/mispec_ws2812_effects.lua b/lua_tests/mispec_ws2812_effects.lua deleted file mode 100644 index 3fc8d95b8d..0000000000 --- a/lua_tests/mispec_ws2812_effects.lua +++ /dev/null @@ -1,31 +0,0 @@ -require 'mispec' - -local buffer, buffer1, buffer2 - -describe('WS2812_effects', function(it) - - it:should('set_speed', function() - buffer = ws2812.newBuffer(9, 3) - ws2812_effects.init(buffer) - - ws2812_effects.set_speed(0) - ws2812_effects.set_speed(255) - - failwith("should be", ws2812_effects.set_speed, -1) - failwith("should be", ws2812_effects.set_speed, 256) - end) - - it:should('set_brightness', function() - buffer = ws2812.newBuffer(9, 3) - ws2812_effects.init(buffer) - - ws2812_effects.set_brightness(0) - ws2812_effects.set_brightness(255) - - failwith("should be", ws2812_effects.set_brightness, -1) - failwith("should be", ws2812_effects.set_brightness, 256) - end) - -end) - -mispec.run() diff --git a/mkdocs.yml b/mkdocs.yml index e7e4c4287c..d606d8eaa8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ pages: - 'pcm' : 'modules/pcm.md' - 'perf': 'modules/perf.md' - 'pipe': 'modules/pipe.md' + - 'pixbuf': 'modules/pixbuf.md' - 'pwm' : 'modules/pwm.md' - 'pwm2' : 'modules/pwm2.md' - 'rfswitch' : 'modules/rfswitch.md' diff --git a/tests/.gitattributes b/tests/.gitattributes new file mode 100644 index 0000000000..a8c75d0c6a --- /dev/null +++ b/tests/.gitattributes @@ -0,0 +1,13 @@ +# Enforce Unix newlines +*.css text eol=lf +*.html text eol=lf +*.js text eol=lf +*.json text eol=lf +*.less text eol=lf +*.md text eol=lf +*.svg text eol=lf +*.yml text eol=lf +*.py text eol=lf +*.sh text eol=lf +*.tcl text eol=lf +*.expect text eol=lf diff --git a/tests/HardwareTestHarness.md b/tests/HardwareTestHarness.md new file mode 100644 index 0000000000..da81ac0ce9 --- /dev/null +++ b/tests/HardwareTestHarness.md @@ -0,0 +1,181 @@ +Hardware Test Harness +===================== + +There is an implementation of the hardware testing design which is a small 4in x 4in board with positions for +two Wemos D! Mini ESP8266 boards, a breadboard area and a number of positions for peripherals. The schematics +are in [schematic](Test-harness-schematic-v1.pdf) and a rendering of the board is ![Board Render](Test-Harness-Render-V1.png). + + +The test harness runs from a dedicated host computer, which is expected +to have reset- and programming-capable UART links to both ESP8266 +devices, as found on almost all ESP8266 boards with USB to UART +adapters, but the host does not necessarily need to use USB to connect, +so long as TXD, RXD, DTR, and RTS are wired across. + +The alternate pins on the primary D1 Mini (DUT0) are cross wired to the +RX and TX pins on the secondary D1 Mini (DUT1) and these are enabled by +a pin on the MCP23017. + +Build Notes +----------- + +The only thing that needs to be done is to solder on 0.1" headers at the required +positions. Typically D1 Minis come with 2 sets of 8 pin headers, both male and female. +I solder the female headers to the board, and the maie headers to the D1 minis. Other, +mostly 4 pin, headers can be soldered at the other positions. The 7 pin header for +the color sensor (TCS34725) requires some care as the board needs to be mounted +upside down so that the sensor is directly above the WS2812. + +The screw holes at the corners are for M3 screws. A standard adhesive rubber foot can +also be used. There are no components on the underside of the test board, so not much clearance +is required (only the length of the various headers soldered on the board). + +Power +----- + +The board is powered by either (or both) D1 Mini USB connection. Given the cross connects +between the two D1 Minis, I think that all the tests can be conducted from DUT0, but +it is probably easier to connected both of the D1 Minis via USB to the test runner. + +There is a small resistor between the two 5 volt rails to prevent large currents +if the two USB feeds are at slughtly different voltages. The 3.3 volt rails are +directly connected together. If the regulators produce slightly different voltages, +then the one producing the higher voltage will end up providing all the power for the +3.3 volt devices. + +Peripherals +----------- + +### I2C Bus + +There is an I2C bus hanging off DUT 0. Attached hardware is used both as +tests of modules directly and also to facilitate testing other modules +(e.g., gpio). + +Most of the positions on the board are connected to the DUT1 I2C bus. + +#### MCP23017: I/O Expander + +At address 0x20. An 16-bit tristate GPIO expander, this chip is used to +test I2C, GPIO, and ADC functionality. This chip's interconnections are +as follows: + +MPC23017 | Purpose +---------|-------------------------------------------------------------- +/RESET |DUT0 reset. This resets the chip whenever the host computer resets DUT 0 over its serial link (using DTR/RTS). +B 0 |4K7 resistor to DUT 0 ADC. +B 1 |2K2 resistor to DUT 0 ADC. +B 2 |Direct to DUT1 RST +B 3 |Direct to DUT1 D3 +B 4 |When low, connects the alternate UART pins on DUT0 to RX,TX on DUT1 +B 5 |DUT1 GPIO16/WAKE via 4K7 resitor +B 6 |DUT0 GPIO13 via 4K4 resistor and DUT1 GPIO15 via 4K7 resistor (also feeds in the primary TX from DUT1 when enabled by B4) +B 7 |DUT0 GPIO15 via 4K7 resistor and DUT1 GPIO13 via 4K7 resistor (also feeds the primary RX on DUT1 when enabled by B4) + +Notes: + +- DUT 0's ADC pin is connected via a 2K2 reistor to this chip's port + B, pin 1 and via a 4K7 resistor to port B, pin 0. This gives us the + ability to produce approximately 0 (both pins low), 1.1 (pin 0 high, + pin 1 low), 2.2 (pin 1 high, pin 0 low), and 3.3V (both pins high) + on the ADC pin. +- Port B pins 6 and 7 sit on the UART cross-wiring between DUT 0 and + DUT 1. The 23017 will be tristated for inter-DUT UART tests, but + these +- All of port A, remain available for expansion and are routed to the breadboard area. + +#### WS2812s + +There are three WS2812s connected on DUT1/D4. The last Ws2812 is positioned so that a TCS34725 module +can be mounted upside down over it to read out the color of the WS2812. That device is connected to +the I2C port on DUT0. A suitable board is [CJMCU-34725 TCS34725 Color Sensor RGB color sensor development board module](https://www.aliexpress.com/item/32412698433.html). The illuminating +LED is connected to the INT pin and so you can disable the LED under software control. + +#### Oled Displays + +Each of the D1 Minis is connected to a position for a 128x64 OLED display, again on the primary I2C bus. + +#### Servo + +On DUT1 pin D4/GPIO 2 there is a connection to a position for a small servo. The servo is powered by the +5V voltage rail. + +#### DHTxx + +On DUT1 pin D6/GPIO 12 there is a connection to a position for a DHTxx device. The silk screen indicates the +orientation of the device. + +#### DS18B20 + +There are two positions for DS18B20s -- one with the VCC pin connected and one without. The data pin is +connected to DUT1 pin D5/GPIO 14. + +#### I2C devices with VCC/GND/SCL/SDA pin order + +There are three positions for I2C devices which have the pins in the VCC/GND/SCL/SDA order. These +are on the DUT1 I2 bus. + +#### I2C devices with other pin orders + +There are three positions for I2C devices with other pin orders. Each of these positions is next +to a crossbar switch and so four blobs of solder can configure each of these headers into any +desired pin order. As far as I can tell, most of the cheap modules use the VCC/GND/SCL/SDA order. + + +Breadboard Area +=============== + +All the pins on each D1 Mini and the A port of the MCP23017 are brought out to a breadboard +area. This can be used to solder components and/or wires, or even a header could be soldered +on to transfer all the signals to a conventional breadboard. + + +ESP8266 Device 0 Connections +---------------------------- + +ESP | Usage +----------|---------------------------------------------------------- +D3/GPIO 0 |Used to enter programming mode; otherwise unused in test environment. +TX/GPIO 1 |Primary UART transmit; reserved for host communication +D4/GPIO 2 |[reserved for 1-Wire] [+ reserved for 23017 INT[AB] connections] +RX/GPIO 3 |Primary UART recieve; reserved for host communication +D2/GPIO 4 |I2C SDA. Connected to MCP23017, Oled display and the TCS34725 if present. +D1/GPIO 5 |I2C SCL +GPIO 6 |[Reserved for on-chip flash] +GPIO 7 |[Reserved for on-chip flash] +GPIO 8 |[Reserved for on-chip flash] +GPIO 9 |[Reserved for on-chip flash] +GPIO 10 |[Reserved for on-chip flash] +GPIO 11 |[Reserved for on-chip flash] +D6/GPIO 12 | +D7/GPIO 13 |Secondary UART RX; DUT 1 GPIO 15, I/O expander B 6 +D5/GPIO 14 | +D8/GPIO 15 |Secondary UART TX; DUT 1 GPIO 13, I/O expander B 7 +D0/GPIO 16 | +A0/ADC 0 |Resistor divider with I/O expander + +ESP8266 Device 1 Connections +---------------------------- + +ESP | Usage +----------|---------------------------------------------------------- +D3/GPIO 0 |Used to enter programming mode; otherwise unused in test environment. +TX/GPIO 1 |Primary UART transmit; reserved for host communication +D4/GPIO 2 |Connected to chain of 3 WS2812s. Also connected to the servo position. +RX/GPIO 3 |Primary UART recieve; reserved for host communication +D2/GPIO 4 |I2C SDA. Connected to all the other I2C positions on the board +D1/GPIO 5 |I2C SCL +GPIO 6 |[Reserved for on-chip flash] +GPIO 7 |[Reserved for on-chip flash] +GPIO 8 |[Reserved for on-chip flash] +GPIO 9 |[Reserved for on-chip flash] +GPIO 10 |[Reserved for on-chip flash] +GPIO 11 |[Reserved for on-chip flash] +D6/GPIO 12 |Connected to data pin for DHTxx +D7/GPIO 13 |Secondary UART RX; DUT 0 GPIO 15, I/O exp B 7 via 4K7 Also used as HSPI MOSI for SPI tests +D5/GPIO 14 |Connected to data pin for DS18B20s. +D8/GPIO 15 |Secondary UART TX; DUT 0 GPIO 13, I/O exp B 6 via 4K7 Also used as HSPI /CS for SPI tests +D0/GPIO 16 |I/O expander B 5 via 4K7 resistor, for deep-sleep tests +A0/ADC 0 | + + diff --git a/tests/NTest/NTest.lua b/tests/NTest/NTest.lua new file mode 100644 index 0000000000..416b6e4391 --- /dev/null +++ b/tests/NTest/NTest.lua @@ -0,0 +1,331 @@ +local function TERMINAL_HANDLER(e, test, msg, errormsg) + if errormsg then + errormsg = ": "..errormsg + else + errormsg = "" + end + if e == 'start' then + print("######## "..e.."ed "..test.." tests") + elseif e == 'pass' then + print(" "..e.." "..test..': '..msg) + elseif e == 'fail' then + print(" ==> "..e.." "..test..': '..msg..errormsg) + elseif e == 'except' then + print(" ==> "..e.." "..test..': '..msg..errormsg) + elseif e == 'finish' then + print("######## "..e.."ed "..test.." tests") + else + print(e.." "..test) + end +end + +-- implement pseudo task handling for on host testing +local drain_post_queue = function() end + +if not node then -- assume we run on host, not on MCU + local post_queue = {{},{},{}} + + drain_post_queue = function() + while #post_queue[1] + #post_queue[2] + #post_queue[3] > 0 do + for i = 3, 1, -1 do + if #post_queue[i] > 0 then + local f = table.remove(post_queue[i], 1) + if f then + f() + end + break + end + end + end + end + + -- luacheck: push ignore 121 122 (setting read-only global variable) + node = {} + node.task = {LOW_PRIORITY = 1, MEDIUM_PRIORITY = 2, HIGH_PRIORITY = 3} + node.task.post = function (p, f) + table.insert(post_queue[p], f) + end + + node.setonerror = function(fn) node.Host_Error_Func = fn end -- luacheck: ignore 142 + -- luacheck: pop +end + + + +--[[ +if equal returns true +if different returns {msg = ""} + this will be handled spechially by ok and nok +--]] +local function deepeq(a, b) + local function notEqual(m) + return { msg=m } + end + + -- Different types: false + if type(a) ~= type(b) then return notEqual("type 1 is "..type(a)..", type 2 is "..type(b)) end + -- Functions + if type(a) == 'function' then + if string.dump(a) == string.dump(b) then + return true + else + return notEqual("functions differ") + end + end + -- Primitives and equal pointers + if a == b then return true end + -- Only equal tables could have passed previous tests + if type(a) ~= 'table' then return notEqual("different "..type(a).."s expected "..a.." vs. "..b) end + -- Compare tables field by field + for k,v in pairs(a) do + if b[k] == nil then return notEqual("key "..k.."only contained in left part") end + local result = deepeq(v, b[k]) + if type(result) == 'table' then return result end + end + for k,v in pairs(b) do + if a[k] == nil then return notEqual("key "..k.."only contained in right part") end + local result = deepeq(a[k], v) + if type(result) == 'table' then return result end + end + return true +end + +-- Compatibility for Lua 5.1 and Lua 5.2 +local function args(...) + return {n=select('#', ...), ...} +end + +local function spy(f) + local mt = {} + setmetatable(mt, {__call = function(s, ...) + s.called = s.called or {} + local a = args(...) + table.insert(s.called, {...}) + if f then + local r + r = args(pcall(f, unpack(a, 1, a.n))) + if not r[1] then + s.errors = s.errors or {} + s.errors[#s.called] = r[2] + else + return unpack(r, 2, r.n) + end + end + end}) + return mt +end + +local function getstackframe() + -- debug.getinfo() does not exist in NodeMCU Lua 5.1 + if debug.getinfo then + return debug.getinfo(5, 'S').short_src:match("([^\\/]*)$")..":"..debug.getinfo(5, 'l').currentline + end + + local msg + msg = debug.traceback() + msg = msg:match("\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t([^\t]*): in") -- Get 5th stack frame + msg = msg:match(".-([^\\/]*)$") -- cut off path of filename + return msg +end + +local function assertok(handler, name, invert, cond, msg) + local errormsg + -- check if cond is return object of 'eq' call + if type(cond) == 'table' and cond.msg then + errormsg = cond.msg + cond = false + end + if not msg then + msg = getstackframe() + end + + if invert then + cond = not cond + end + if cond then + handler('pass', name, msg) + else + handler('fail', name, msg, errormsg) + error('_*_TestAbort_*_') + end +end + +local function fail(handler, name, func, expected, msg) + local status, err = pcall(func) + if not msg then + msg = getstackframe() + end + if status then + local messageParts = {"Expected to fail with Error"} + if expected then + messageParts[2] = " containing \"" .. expected .. "\"" + end + handler('fail', name, msg, table.concat(messageParts, "")) + error('_*_TestAbort_*_') + end + if (expected and not string.find(err, expected)) then + err = err:match(".-([^\\/]*)$") -- cut off path of filename + handler('fail', name, msg, "expected errormessage \"" .. err .. "\" to contain \"" .. expected .. "\"") + error('_*_TestAbort_*_') + end + handler('pass', name, msg) +end + +local nmt = { + env = _G, + outputhandler = TERMINAL_HANDLER +} +nmt.__index = nmt + +return function(testrunname) + + local pendingtests = {} + local started + + local N = setmetatable({}, nmt) + + local function runpending() + if pendingtests[1] ~= nil then + node.task.post(node.task.LOW_PRIORITY, function() + pendingtests[1](runpending) + end) + else + N.outputhandler('finish', testrunname) + end + end + + local function copyenv(dest, src) + dest.eq = src.eq + dest.spy = src.spy + dest.ok = src.ok + dest.nok = src.nok + dest.fail = src.fail + end + + local function testimpl(name, f, async) + local testfn = function(next) + + local prev = {} + copyenv(prev, N.env) + + local handler = N.outputhandler + + local restore = function(err) + if err then + err = err:match(".-([^\\/]*)$") -- cut off path of filename + if not err:match('_*_TestAbort_*_') then + handler('except', name, err) + end + end + if node then node.setonerror() end + copyenv(N.env, prev) + handler('end', name) + table.remove(pendingtests, 1) + collectgarbage() + if next then next() end + end + + local function wrap(method, ...) + method(handler, name, ...) + end + + local function cbError(err) + err = err:match(".-([^\\/]*)$") -- cut off path of filename + if not err:match('_*_TestAbort_*_') then + handler('except', name, err) + end + restore() + end + + local env = N.env + env.eq = deepeq + env.spy = spy + env.ok = function (cond, msg) wrap(assertok, false, cond, msg) end + env.nok = function(cond, msg) wrap(assertok, true, cond, msg) end + env.fail = function (func, expected, msg) wrap(fail, func, expected, msg) end + + handler('begin', name) + node.setonerror(cbError) + local ok, err = pcall(f, async and restore) + if not ok then + err = err:match(".-([^\\/]*)$") -- cut off path of filename + if not err:match('_*_TestAbort_*_') then + handler('except', name, err) + end + if async then + restore() + end + end + + if not async then + restore() + end + end + + if not started then + N.outputhandler('start', testrunname) + started = true + end + + + table.insert(pendingtests, testfn) + if #pendingtests == 1 then + runpending() + drain_post_queue() + end + end + + function N.test(name, f) + testimpl(name, f) + end + + function N.testasync(name, f) + testimpl(name, f, true) + end + + local currentCoName + + function N.testco(name, func) + -- local t = tmr.create(); + local co + N.testasync(name, function(Next) + currentCoName = name + + local function getCB(cbName) + return function(...) -- upval: co, cbName + local result, err = coroutine.resume(co, cbName, ...) + if (not result) then + if (name == currentCoName) then + currentCoName = nil + Next(err) + else + N.outputhandler('fail', name, "Found stray Callback '"..cbName.."' from test '"..name.."'") + end + elseif coroutine.status(co) == "dead" then + currentCoName = nil + Next() + end + end + end + + local function waitCb() + return coroutine.yield() + end + + co = coroutine.create(function(wr, wa) + func(wr, wa) + end) + + local result, err = coroutine.resume(co, getCB, waitCb) + if (not result) then + currentCoName = nil + Next(err) + elseif coroutine.status(co) == "dead" then + currentCoName = nil + Next() + end + end) + end + + return N +end diff --git a/tests/NTest/NTest.md b/tests/NTest/NTest.md new file mode 100644 index 0000000000..2daac3ff52 --- /dev/null +++ b/tests/NTest/NTest.md @@ -0,0 +1,202 @@ +# NTest +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2020-11-01 | [Gregor Hartmann](https://github.com/HHHartmann) | [Gregor Hartmann](https://github.com/HHHartmann) | [NTest.lua](NTest.lua) | + +NTest is a test system for NodeMCU which is originally based on gambiarra. It is designed to run on chip but the selftest also runs on the host using luac.cross. + +!!! warning + This module is too big to load by standard `require` function or compile on ESP8266 using `node.compile()`. The only option to load and use it is to use [LFS](../lfs.md). + +## Example + +``` Lua +-- Simple synchronous test +local tests = require("NTest")("first testrun") + +tests.test('Check dogma', function() + ok(2+2 == 5, 'two plus two equals five') +end) + +-- A more advanced asynchronous test +tests.testasync('Do it later', function(done) + someAsyncFn(function(result) + ok(result == expected) + done() -- this starts the next async test + end) +end) + +-- An asynchronous test using a coroutine +tests.testco('Do it in place', function(getCB, waitCB) + someAsyncFn(getCB("callback")) + local CBName = waitCB() + ok(CBName, "callback") +end) +``` + +## API + +`require('NTest')` returns an new function which must be called with a string. + +``` Lua +local new = require('NTest') +``` + +`new(testrunname:string)` returns an object with the following functions: + +``` Lua +local tests = new("first testrun") +``` + +`test(name:string, f:function)` allows you to define a new test: + +``` Lua +tests.test('My sync test', function() +end) +``` + +`testasync(name:string, f:function(done:function))` allows you to define a new asynchronous test: +To tell NTest that the test is finished you need to call the function `done` which gets passed in. +In async scenarios the test function will usually terminate quickly but the test is still waiting for +some callback to be called before it is finished. + +``` Lua +tests.testasync('My async test', function(done) + done() +end) +``` + +`testco(name:string, f:function(getCB:function, waitCB:function))` allows you to define a new asynchronous +test which runs in a coroutine: +This allows handling callbacks in the test in a linear way. simply use getCB to create a new callback stub. +waitCB blocks the test until the callback is called and returns the parameters. + +``` Lua +tests.testco('My coroutine test', function(getCB, waitCB) +end) + +tests.testco('My coroutine test with callback', function(getCB, waitCB) + local t = tmr.create(); + t:alarm(200, tmr.ALARM_AUTO, getCB("timer")) + local name, tCB = waitCB() + ok(eq(name, "timer"), "CB name matches") + + name, tCB = waitCB() + ok(eq(name, "timer"), "CB name matches again") + + tCB:stop() + + ok(true, "coroutine end") +end) + +``` + +All test functions also define some helper functions that are added when the test is executed - `ok`, `nok`, `fail`, `eq` and `spy`. + +`ok`, `nok`, `fail` are assert functions which will break the test if the condition is not met. + +`ok(cond:bool, [msg:string])`. It takes any boolean condition and an optional assertion message. If no message is defined - current filename and line will be used. If the condition evaluetes to thuthy nothing happens. +If it is falsy the message is printed and the test is aborted. The next test will then be executed. + +``` Lua +ok(1 == 2) -- prints 'foo.lua:42' +ok(1 == 2, 'one equals one') -- prints 'one equals one' +ok(1 == 1) -- prints nothing +``` + +`nok(cond:bool, [msg:string])` is a shorthand for `ok(not cond, 'msg')`. + +``` Lua +nok(1 == 1) -- prints 'foo.lua:42' +nok(1 == 1, 'one equals one') -- prints 'one equals one' +``` + +`fail(func:function, [expected:string], [msg:string])` tests a function for failure. If expected is given the errormessage poduced by the function must also contain the given string else `fail` will fail the test. If no message is defined the current filename and line will be used. + +``` Lua +local function f() error("my error") end +fail(f, "expected error", "Failed with incorrect error") + -- fails with 'Failed with incorrect error' and + -- 'expected errormessage "foo.lua:2: my error" to contain "expected error"' +``` + +`eq(a, b)` is a helper to deeply compare lua variables. It supports numbers, strings, booleans, nils, functions and tables. It's mostly useful within ok() and nok(): +If the variables are equal it returns `true` else it returns `{msg=''}` This is recognized by `ok` and `nok` and results in also logging the reason for difference. + +``` Lua +ok(eq(1, 1)) +ok(eq({a='b',c='d'}, {c='d',a='b'}) +ok(eq('foo', 'bar')) -- will fail +``` + +`spy([f])` creates function wrappers that remember each call (arguments, errors) but behaves much like the real function. Real function is optional, in this case spy will return nil, but will still record its calls. +Spies are most helpful when passing them as callbacks and testing that they were called with correct values. + +``` Lua +local f = spy(function(s) return #s end) +ok(f('hello') == 5) +ok(f('foo') == 3) +ok(#f.called == 2) +ok(eq(f.called[1], {'hello'}) +ok(eq(f.called[2], {'foo'}) +f(nil) +ok(f.errors[3] ~= nil) +``` + +## Reports + +Another useful feature is that you can customize test reports as you need. The default `outputhandler` just more or less prints out a basic report. You can easily override (or augment by wrapping, e.g.) this behavior as well as add any other information you need (number of passed/failed assertions, time the test took etc): + +Events are: +`start` when testing starts +`finish` when all tests have finished +`begin` Will be called before each test +`end` Will be called after each test +`pass` Test has passed +`fail` Test has failed with not fulfilled assert (ok, nok, fail) +`except` Test has failed with unexpected error + + +``` Lua +local passed = 0 +local failed = 0 + +tests.outputhandler = function(event, testfunc, msg) + if event == 'begin' then + print('Started test', testfunc) + passed = 0 + failed = 0 + elseif event == 'end' then + print('Finished test', testfunc, passed, failed) + elseif event == 'pass' then + passed = passed + 1 + elseif event == 'fail' then + print('FAIL', testfunc, msg) + failed = failed + 1 + elseif event == 'except' then + print('ERROR', testfunc, msg) + end +end +``` + +Additionally, you can pass a different environment to keep `_G` unpolluted: +You need to set it, so the helper functions mentioned above can be added before calling the test function. + +``` Lua +local myenv = {} +tests.env = myenv + +tests.test('Some test', function() + myenv.ok(myenv.eq(...)) + local f = myenv.spy() +end) +``` + +You can restore `env` or `outputhandler` to their defaults by setting their values to `nil`. + + +## Appendix + +This Library is for NodeMCU versions Lua 5.1 and Lua 5.3. + +It is based on https://bitbucket.org/zserge/gambiarra and includes bugfixes, substantial extensions of functionality and adaptions to NodeMCU requirements. diff --git a/tests/NTest/NTest_NTest.lua b/tests/NTest/NTest_NTest.lua new file mode 100644 index 0000000000..f0b714346b --- /dev/null +++ b/tests/NTest/NTest_NTest.lua @@ -0,0 +1,531 @@ +local N = require('NTest')("selftest") +local orig_node = node +local metatest +local async +local expected = {} +local failed, passed = 0,0 +local load_tests2 +local function load_tests() + -- + -- Basic tests + -- + metatest('simple assert passes', function() + ok(2 == 2, '2==2') + end, {'2==2'}, {}) + + metatest('simple negative assert fails', function() + nok(2 == 2, '2==2') + nok(false, 'unreachable code') + end, {}, {'2==2'}) + + metatest('simple assert fails', function() + ok(1 == 2, '1~=2') + ok(true, 'unreachable code') + end, {}, {'1~=2'}) + + metatest('simple negative assert passes', function() + nok(1 == 2, '1~=2') + end, {'1~=2'}, {}) + + metatest('ok without a message', function() + ok(1 == 1) + ok(1 == 2) + end, {'NTest_NTest.lua:31'}, {'NTest_NTest.lua:32'}) + + metatest('nok without a message', function() + nok(1 == "") + nok(1 == 1) + end, {'NTest_NTest.lua:36'}, {'NTest_NTest.lua:37'}) + + -- + -- Equality tests + -- + metatest('eq nil', function() + ok(eq(nil, nil), 'nil == nil') + nok(eq("", nil), '"" != nil') + nok(eq(nil, ""), 'nil != ""') + end, {'nil == nil', '"" != nil', 'nil != ""'}, {}) + + metatest('eq primitives', function() + ok(eq('foo', 'foo'), 'str == str') + nok(eq('foo', 'bar'), 'str != str') + ok(eq(123, 123), 'num == num') + nok(eq(123, 12), 'num != num') + end, {'str == str', 'str != str', 'num == num', 'num != num'}, {}) + + metatest('eq arrays', function() + ok(eq({}, {}), 'empty') + ok(eq({1, 2}, {1, 2}), 'equal') + nok(eq({1, 2}, {2, 1}), 'swapped') + nok(eq({1, 2, 3}, {1, 2}), 'longer') + nok(eq({1, 2}, {1, 2, 3}), 'shorter') + end, {'empty', 'equal', 'swapped', 'longer', 'shorter'}, {}) + + metatest('eq objects', function() + ok(eq({}, {}), 'empty') + ok(eq({a=1,b=2}, {a=1,b=2}), 'equal') + ok(eq({b=2,a=1}, {a=1,b=2}), 'swapped') + nok(eq({a=1,b=2}, {a=1,b=3}), 'not equal') + end, {'empty', 'equal', 'swapped', 'not equal'}, {}) + + metatest('eq nested objects', function() + ok(eq({ + ['1'] = { name = 'mhc', age = 28 }, + ['2'] = { name = 'arb', age = 26 } + }, { + ['1'] = { name = 'mhc', age = 28 }, + ['2'] = { name = 'arb', age = 26 } + }), 'equal') + ok(eq({ + ['1'] = { name = 'mhc', age = 28 }, + ['2'] = { name = 'arb', age = 26 } + }, { + ['1'] = { name = 'mhc', age = 28 }, + ['2'] = { name = 'arb', age = 27 } + }), 'not equal') + end, {'equal'}, {'not equal', 'different numbers expected 26 vs. 27'}) + + metatest('eq functions', function() + ok(eq(function(x) return x end, function(x) return x end), 'equal') + nok(eq(function(z) return x + z end, function(z) return y + z end), 'wrong variable') -- luacheck: ignore + nok(eq(function(x) return x*2 end, function(x) return x+2 end), 'wrong code') + end, {'equal', 'wrong variable', 'wrong code'}, {}) + + metatest('eq different types', function() + local eqos = eq({a=1,b=2}, "text") + ok(eq(eqos.msg, "type 1 is table, type 2 is string"), 'object/string') + local eqfn = eq(function(x) return x end, 12) + ok(eq(eqfn.msg, "type 1 is function, type 2 is number"), 'function/int') + nok(eq(12, "Hallo"), 'int/string') + end, {"object/string", "function/int", 'int/string'}, {}) + + -- + -- Spies + -- + metatest('spy called', function() + local f = spy() + ok(not f.called or #f.called == 0, 'not called') + f() + ok(f.called, 'called') + ok(#f.called == 1, 'called once') + f() + ok(#f.called == 2, 'called twice') + end, {'not called', 'called', 'called once', 'called twice'}, {}) + + metatest('spy with arguments', function() + local x = 0 + local function setX(n) x = n end + local f = spy(setX) + f(1) + ok(x == 1, 'x == 1') + ok(eq(f.called, {{1}}), 'called with 1') + f(42) + ok(x == 42, 'x == 42') + ok(eq(f.called, {{1}, {42}}), 'called with 42') + end, {'x == 1', 'called with 1', 'x == 42', 'called with 42'}, {}) + + metatest('spy with nils', function() + local function nils(a, _, b) return a, nil, b, nil end + local f = spy(nils) + local r1, r2, r3, r4 = f(1, nil, 2) + ok(eq(f.called, {{1, nil, 2}}), 'nil in args') + ok(r1 == 1 and r2 == nil and r3 == 2 and r4 == nil, 'nil in returns') + end, {'nil in args', 'nil in returns'}, {}) + + metatest('spy with exception', function() + local function throwSomething(s) + if s ~= 'nopanic' then error('panic: '..s) end + end + local f = spy(throwSomething) + f('nopanic') + ok(f.errors == nil, 'no errors yet') + f('foo') + ok(eq(f.called, {{'nopanic'}, {'foo'}}), 'args ok') + ok(f.errors[1] == nil and f.errors[2] ~= nil, 'thrown ok') + end, {'no errors yet', 'args ok', 'thrown ok'}, {}) + + metatest('another spy with exception', function() + local f = spy(function() local a = unknownVariable + 1 end) -- luacheck: ignore + f() + ok(f.errors[1], 'exception thrown') + end, {'exception thrown'}, {}) + + metatest('spy should return a value', function() + local f = spy(function() return 5 end) + ok(f() == 5, 'spy returns a value') + local g = spy() + ok(g() == nil, 'default spy returns undefined') + end, {'spy returns a value', 'default spy returns undefined'}, {}) + + -- + -- fail tests + -- + metatest('fail with correct errormessage', function() + fail(function() error("my error") end, "my error", "Failed with correct error") + ok(true, 'reachable code') + end, {'Failed with correct error', 'reachable code'}, {}) + + metatest('fail with incorrect errormessage', function() + fail(function() error("my error") end, "different error", "Failed with incorrect error") + ok(true, 'unreachable code') + end, {}, {'Failed with incorrect error', + 'expected errormessage "NTest_NTest.lua:169: my error" to contain "different error"'}) + + metatest('fail with incorrect errormessage default message', function() + fail(function() error("my error") end, "different error") + ok(true, 'unreachable code') + end, {}, {'NTest_NTest.lua:175', + 'expected errormessage "NTest_NTest.lua:175: my error" to contain "different error"'}) + + metatest('fail with not failing code', function() + fail(function() end, "my error", "did not fail") + ok(true, 'unreachable code') + end, {}, {"did not fail", 'Expected to fail with Error containing "my error"'}) + + metatest('fail with failing code', function() + fail(function() error("my error") end, nil, "Failed as expected") + ok(true, 'reachable code') + end, {'Failed as expected', 'reachable code'}, {}) + + metatest('fail with not failing code', function() + fail(function() end, nil , "did not fail") + ok(true, 'unreachable code') + end, {}, {"did not fail", 'Expected to fail with Error'}) + + metatest('fail with not failing code default message', function() + fail(function() end) + ok(true, 'unreachable code') + end, {}, {"NTest_NTest.lua:196", 'Expected to fail with Error'}) + + metatest('=== load more tests ===', function() + load_tests2() + end, {}, {}) + +end + +load_tests2 = function() + -- + -- except tests + -- + metatest('error should panic', function() + error("lua error") + ok(true, 'unreachable code') + end, {}, {}, {'NTest_NTest.lua:211: lua error'}) + + -- + -- called function except + -- + + local function subfuncerror() + error("lua error") + end + + metatest('subroutine error should panic', function() + subfuncerror() + ok(true, 'unreachable code') + end, {}, {}, {'NTest_NTest.lua:220: lua error'}) + + local function subfuncok() + ok(false) + end + + metatest('subroutine ok should fail', function() + subfuncok() + ok(true, 'unreachable code') + end, {}, {'NTest_NTest.lua:229'}) + + --drain_post_queue() + + -- + -- Async tests + -- + metatest('async test', function(next) + async(function() + ok(true, 'bar') + async(function() + ok(true, 'baz') + next() + end) + end) + ok(true, 'foo') + end, {'foo', 'bar', 'baz'}, {}, {}, true) + + metatest('async test without actually async', function(next) + ok(true, 'bar') + next() + end, {'bar'}, {}, {}, true) + + metatest('async fail in main', function(--[[ next ]]) + ok(false, "async fail") + ok(true, 'unreachable code') + end, {}, {'async fail'}, {}, true) + + --drain_post_queue() + + metatest('another async test after async queue drained', function(next) + async(function() + ok(true, 'bar') + next() + end) + ok(true, 'foo') + end, {'foo', 'bar'}, {}, {}, true) + + -- + -- except tests async + -- + metatest('async except in main', function(--[[ next ]]) + error("async except") + ok(true, 'unreachable code') + end, {}, {}, {'NTest_NTest.lua:277: async except'}, true) + + metatest('async fail in callback', function(next) + async(function() + ok(false, "async fail") + next() + end) + ok(true, 'foo') + end, {'foo'}, {'async fail'}, {}, true) + + metatest('async except in callback', function(next) + async(function() + error("async Lua error") + next() + end) + ok(true, 'foo') + end, {'foo'}, {}, {'NTest_NTest.lua:291: async Lua error'}, true) + + -- + -- sync after async test + -- + local marker = 0 + metatest('set marker async', function(next) + async(function() + marker = "marked" + ok(true, 'async bar') + next() + end) + ok(true, 'foo') + end, {'foo', 'async bar'}, {}, {}, true) + + metatest('check marker async', function() + ok(eq(marker, "marked"), "marker check") + end, {"marker check"}, {}) + + -- + -- coroutine async tests + -- + metatest('coroutine pass', function(--[[ getCB, waitCB ]]) + ok(true, "passing") + end, {"passing"}, {}, {}, "co") + + metatest('coroutine fail in main', function(--[[ getCB, waitCB ]]) + ok(false, "coroutine fail") + ok(true, 'unreachable code') + end, {}, {'coroutine fail'}, {}, "co") + + metatest('coroutine fail in main', function(--[[ getCB, waitCB ]]) + nok(true, "coroutine fail") + nok(false, 'unreachable code') + end, {}, {'coroutine fail'}, {}, "co") + + metatest('coroutine fail error', function(--[[ getCB, waitCB ]]) + fail(function() error("my error") end, "my error", "Failed with correct error") + fail(function() error("my error") end, "other error", "Failed with other error") + ok(true, 'unreachable code') + end, {'Failed with correct error'}, {'Failed with other error', + 'expected errormessage "NTest_NTest.lua:333: my error" to contain "other error"'}, {}, "co") + + metatest('coroutine except in main', function(--[[ getCB, waitCB ]]) + error("coroutine except") + ok(true, 'unreachable code') + end, {}, {}, {'NTest_NTest.lua:339: coroutine except'}, "co") + + --local function coasync(f) table.insert(post_queue, 1, f) end + local function coasync(f, p1, p2) node.task.post(node.task.MEDIUM_PRIORITY, function() f(p1,p2) end) end + + metatest('coroutine with callback', function(getCB, waitCB) + coasync(getCB("testCb")) + local name = waitCB() + ok(eq(name, "testCb"), "cb") + end, {"cb"}, {}, {}, "co") + + metatest('coroutine with callback with values', function(getCB, waitCB) + coasync(getCB("testCb"), "p1", 2) + local name, p1, p2 = waitCB() + ok(eq(name, "testCb"), "cb") + ok(eq(p1, "p1"), "p1") + ok(eq(p2, 2), "p2") + end, {"cb", "p1", "p2"}, {}, {}, "co") + + metatest('coroutine fail after callback', function(getCB, waitCB) + coasync(getCB("testCb")) + local name = waitCB() + ok(eq(name, "testCb"), "cb") + ok(false, "fail") + ok(true, 'unreachable code') + end, {"cb"}, {"fail"}, {}, "co") + + metatest('coroutine except after callback', function(getCB, waitCB) + coasync(getCB("testCb")) + local name = waitCB() + ok(eq(name, "testCb"), "cb") + error("error") + ok(true, 'unreachable code') + end, {"cb"}, {}, {"NTest_NTest.lua:372: error"}, "co") + + --- detect stray callbacks after the test has finished + local strayCb + local function rewrap() + coasync(strayCb) + end + + metatest('leave stray callback', function(getCB--[[ , waitCB ]]) + strayCb = getCB("testa") + coasync(rewrap) + end, {}, {}, {}, "co") + + metatest('test after stray cb', function(--[[ getCB, waitCB ]]) + end, {}, {"Found stray Callback 'testa' from test 'leave stray callback'"}, {}, "co") + + + + -- + -- Finalize: check test results + -- + metatest("finishing up pending tests", function() + for i = 1,#expected -1 do + print("--- FAIL "..expected[i].name..' (pending)') + failed = failed + 1 + end + print("failed: "..failed, "passed: "..passed) + node = orig_node -- luacheck: ignore 121 (setting read-only global variable) + end, {}, {}) + +end -- load_tests() + + +local cbWrap = function(cb) return cb end + +if not node.LFS then -- assume we run on host, not on MCU. node is already defined by NTest if running on host + cbWrap = function(cb) + return function(...) + local ok, p1,p2,p3,p4 = pcall(cb, ...) + if not ok then + if node.Host_Error_Func then -- luacheck: ignore 143 + node.Host_Error_Func(p1) -- luacheck: ignore 143 + else + print(p1, "::::::::::::: reboot :::::::::::::") + end + else + return p1,p2,p3,p4 + end + end + end +end + +-- Helper function to print arrays +local function stringify(t) + local s = '' + for i=1,#(t or {}) do + s = s .. '"' .. t[i] .. '"' .. ', ' + end + return s:gsub('..$', '') +end + +local pass +-- Set meta test handler +N.outputhandler = function(e, test, msg, errormsg) + local function consumemsg(msg, area) -- luacheck: ignore + if not expected[1][area][1] then + print("--- FAIL "..expected[1].name..' ('..area..'ed): unexpected "'.. + msg..'"') + pass = false + return + end + + if msg ~= expected[1][area][1] then + print("--- FAIL "..expected[1].name..' ('..area..'ed): expected "'.. + expected[1][area][1]..'" vs "'.. + msg..'"') + pass = false + end + table.remove(expected[1][area], 1) + end + + local function consumeremainder(area) + if #expected[1][area] > 0 then + print("--- FAIL "..expected[1].name..' ('..area..'ed): expected ['.. + stringify(expected[1][area])..']') + pass = false + end + end + + if e == 'begin' then + pass = true + elseif e == 'end' then + consumeremainder("pass") + consumeremainder("fail") + consumeremainder("except") + if pass then + print("+++ Pass "..expected[1].name) + passed = passed + 1 + else + failed = failed + 1 + end + table.remove(expected, 1) + elseif e == 'pass' then + consumemsg(msg, "pass") + if errormsg then consumemsg(errormsg, "pass") end + elseif e == 'fail' then + consumemsg(msg, "fail") + if errormsg then consumemsg(errormsg, "fail") end + elseif e == 'except' then + consumemsg(msg, "except") + if errormsg then consumemsg(errormsg, "except") end + elseif e == 'start' or e == 'finish' then -- luacheck: ignore + -- ignore + else + print("Extra output: ", e, test, msg, errormsg) + end +end + +local async_queue = {} +async = function(f) table.insert(async_queue, cbWrap(f)) end +local function async_next() + local f = table.remove(async_queue, 1) + if f then + f() + end +end + +local function drain_async_queue() + while #async_queue > 0 do + async_next() + end +end + +metatest = function(name, f, expectedPassed, expectedFailed, expectedExcept, asyncMode) + table.insert(expected, { + name = name, + pass = expectedPassed, + fail = expectedFailed, + except = expectedExcept or {} + }) + local ff = f + if asyncMode then + ff = function(...) + f(...) + drain_async_queue() + end + if (asyncMode == "co") then + N.testco(name,ff) + else + N.testasync(name, ff) + end + else + N.test(name, ff) + end +end + + +load_tests() diff --git a/tests/NTest_adc_env.lua b/tests/NTest_adc_env.lua new file mode 100644 index 0000000000..6a3267f0d7 --- /dev/null +++ b/tests/NTest_adc_env.lua @@ -0,0 +1,62 @@ +-- Walk the ADC through a stepped triangle wave using the attached voltage +-- divider and I2C GPIO expander. + +local N = ... +N = (N or require "NTest")("adc-env") + +-- TODO: Preflight test that we are in the correct environment with an I2C +-- expander in the right place with the right connections. + +-- TODO: Use the mcp23017 module in the main tree rather than hand-coding +-- the commands + +N.test('setup', function() + -- Configure the ADC + if adc.force_init_mode(adc.INIT_ADC) + then + node.restart() + error "Must reboot to get to ADC mode" + end + + -- Configure the I2C bus + i2c.setup(0, 2, 1, i2c.FAST) + + -- Set the IO expander port B to channels 0 and 1 as outputs + i2c.start(0) + ok(i2c.address(0, 0x20, i2c.TRANSMITTER)) + i2c.write(0, 0x01, 0xFC) + i2c.stop(0) +end) + +-- set the two-bit voltage divider output value to v (in 0,1,2,3) +local function setv(v) + assert (0 <= v and v <= 3) + i2c.start(0) + i2c.address(0, 0x20, i2c.TRANSMITTER) + i2c.write(0, 0x15, v) + i2c.stop(0) +end + +-- read out the ADC and compare to given range +local function checkadc(min, max) + local v = adc.read(0) + return ok(min <= v and v <= max, ("read adc: %d <= %d <= %d"):format(min,v,max)) +end + +-- Since we have a rail-to-rail 4-tap DAC, as it were, give us some one-sided +-- wiggle around either rail and some two-sided wiggle around both middle stops +local vmin = { 0, 300, 700, 1000 } +local vmax = { 24, 400, 800, 1024 } + +-- Set the DAC, wait a brief while for physics, and then read the ADC +local function mktest(fs, i) + N.test(fs:format(i), function() + setv(i) + tmr.delay(10) + checkadc(vmin[i+1], vmax[i+1]) + end) +end + +-- test all four stops on the way up, and the three to come back down +for i=0,3 do mktest("%d up", i) end +for i=2,0,-1 do mktest("%d down", i) end diff --git a/tests/NTest_file.lua b/tests/NTest_file.lua new file mode 100644 index 0000000000..c4cb823db5 --- /dev/null +++ b/tests/NTest_file.lua @@ -0,0 +1,240 @@ +local N = ... +N = (N or require "NTest")("file") + +local function cleanup() + file.remove("testfile") + file.remove("testfile2") + local testfiles = {"testfile1&", "testFILE2"} + for _, n in ipairs(testfiles) do + file.remove(n,n) + end +end + +N.test('exist', function() + cleanup() + nok(file.exists("non existing file"), "non existing file") + + file.putcontents("testfile", "testcontents") + ok(file.exists("testfile"), "existing file") +end) + + +N.test('fscfg', function() + cleanup() + local start, size = file.fscfg() + ok(start, "start") + ok(size, "size") +end) + +N.test('fsinfo', function() + cleanup() + local remaining, used, total = file.fsinfo() + ok(remaining, "remaining") + ok(used, "used") + ok(total, "total") + ok(eq(remaining+used, total), "size maths") +end) + +N.test('getcontents', function() + cleanup() + local testcontent = "some content \0 and more" + file.putcontents("testfile", testcontent) + local content = file.getcontents("testfile") + ok(eq(testcontent, content),"contents") +end) + +N.test('getcontents non existent file', function() + cleanup() + nok(file.getcontents("non existing file"), "non existent file") +end) + +N.test('getcontents more than 1K', function() + cleanup() + local f = file.open("testfile", "w") + for i = 1,100 do -- luacheck: ignore + f:write("some text to test") + end + f:close() + local content = file.getcontents("testfile") + ok(eq(#content, 1700), "long contents") +end) + +N.test('read more than 1K', function() + cleanup() + local f = file.open("testfile", "w") + for i = 1,100 do -- luacheck: ignore + f:write("some text to test") + end + f:close() + f = file.open("testfile","r") + local buffer = f:read() + ok(eq(#buffer, 1024), "first block") + buffer = f:read() + f:close() + ok(eq(#buffer, 1700-1024), "second block") +end) + +local function makefile(name, num128) + local s128 = "16 bytes written" + s128 = s128..s128..s128..s128 + s128 = s128..s128 + local f = file.open(name, "w") + for i = 1, num128 do -- luacheck: ignore + f:write(s128) + end + f:close() +end + +N.test('read 7*128 bytes', function() + cleanup() + makefile("testfile", 7) + local f = file.open("testfile","r") + local buffer = f:read() + f:close() + nok(eq(buffer, nil), "nil first block") + ok(eq(#buffer, 128*7), "length first block") +end) + +N.test('read 8*128 bytes long file', function() + cleanup() + makefile("testfile", 8) + local f = file.open("testfile","r") + local buffer = f:read() + nok(eq(buffer, nil), "nil first block") + ok(eq(#buffer, 128*8), "size first block") + buffer = f:read() + f:close() + ok(eq(buffer, nil), "nil second block") +end) + +N.test('read 9*128 bytes', function() + cleanup() + makefile("testfile", 9) + local f = file.open("testfile","r") + local buffer = f:read() + nok(eq(buffer, nil), "nil first block") + ok(eq(#buffer, 1024), "size first block") + buffer = f:read() + f:close() + nok(eq(buffer, nil), "nil second block") + ok(eq(#buffer, 128*9-1024), "size second block") +end) + +N.test('list', function() + cleanup() + + local function count(files) + local filecount = 0 + for _,_ in pairs(files) do filecount = filecount+1 end + return filecount + end + + local files + local function testfile(name) + ok(eq(files[name],#name), "length matches name length") + end + + local testfiles = {"testfile1&", "testFILE2"} + for _, n in ipairs(testfiles) do + file.putcontents(n,n) + end + + files = file.list("testfile%.*") + ok(eq(count(files), 1), "found file") + testfile("testfile1&") + + files = file.list("^%l*%u+%d%.-") + ok(eq(count(files), 1), "found file regexp") + testfile("testFILE2") + + files = file.list() + ok(count(files) >= 2, "several files found") +end) + +N.test('open non existing', function() + cleanup() + local function testopen(test, filename, mode) + test(file.open(filename, mode), mode) + file.close() + file.remove(filename) + end + + testopen(nok, "testfile", "r") + testopen(ok, "testfile", "w") + testopen(ok, "testfile", "a") + testopen(nok, "testfile", "r+") + testopen(ok, "testfile", "w+") + testopen(ok, "testfile", "a+") +end) + +N.test('open existing', function() + cleanup() + local function testopen(mode, position) + file.putcontents("testfile", "testcontent") + ok(file.open("testfile", mode), mode) + file.write("") + ok(eq(file.seek(), position), "seek check") + file.close() + end + + testopen("r", 0) + testopen("w", 0) + testopen("a", 11) + testopen("r+", 0) + testopen("w+", 0) + testopen("a+", 11) +end) + +N.test('remove', function() + cleanup() + file.putcontents("testfile", "testfile") + + ok(file.remove("testfile") == nil, "existing file") + ok(file.remove("testfile") == nil, "non existing file") +end) + +N.test('rename', function() + cleanup() + file.putcontents("testfile", "testfile") + + ok(file.rename("testfile", "testfile2"), "rename existing") + nok(file.exists("testfile"), "old file removed") + ok(file.exists("testfile2"), "new file exists") + + nok(file.rename("testfile", "testfile3"), "rename non existing") + + file.putcontents("testfile", "testfile") + + nok(file.rename("testfile", "testfile2"), "rename to existing") + ok(file.exists("testfile"), "from file exists") + ok(file.exists("testfile2"), "to file exists") +end) + +N.test('stat existing file', function() + cleanup() + file.putcontents("testfile", "testfile") + + local stat = file.stat("testfile") + ok(stat, "stat existing") + ok(eq(stat.size, 8), "size") + ok(eq(stat.name, "testfile"), "name") + ok(stat.time, "no time") + ok(eq(stat.time.year, 1970), "year") + ok(eq(stat.time.mon, 01), "mon") + ok(eq(stat.time.day, 01), "day") + ok(eq(stat.time.hour, 0), "hour") + ok(eq(stat.time.min, 0), "min") + ok(eq(stat.time.sec, 0), "sec") + nok(stat.is_dir, "is_dir") + nok(stat.is_rdonly, "is_rdonly") + nok(stat.is_hidden, "is_hidden") + nok(stat.is_sys, "is_sys") + nok(stat.is_arch, "is_arch") +end) + +N.test('stat non existing file', function() + cleanup() + local stat = file.stat("not existing file") + + ok(stat == nil, "stat empty") +end) diff --git a/tests/NTest_gpio_env.lua b/tests/NTest_gpio_env.lua new file mode 100644 index 0000000000..8d15d73bab --- /dev/null +++ b/tests/NTest_gpio_env.lua @@ -0,0 +1,89 @@ +-- Walk the GPIO subsystem through its paces, using the attached I2C GPIO chip +-- +-- Node GPIO 13 (index 7) is connected to I2C expander channel B6; node OUT +-- Node GPIO 15 (index 8) is connected to I2C expander channel B7; node IN + +local N = ... +N = (N or require "NTest")("gpio-env") + +-- TODO: Preflight test that we are in the correct environment with an I2C +-- expander in the right place with the right connections. + +-- TODO: Use the mcp23017 module in the main tree rather than hand-coding +-- the commands + +N.test('setup', function() + -- Set gpio pin directions + gpio.mode(8, gpio.INPUT) + gpio.mode(7, gpio.OUTPUT, gpio.FLOAT) + + -- Configure the I2C bus + i2c.setup(0, 2, 1, i2c.FAST) + + -- Set the IO expander port B to channel 7 as output, 6 as input + i2c.start(0) + ok(i2c.address(0, 0x20, i2c.TRANSMITTER)) + i2c.write(0, 0x01, 0x7F) + i2c.stop(0) +end) + +local function seti2cb7(v) + i2c.start(0) + i2c.address(0, 0x20, i2c.TRANSMITTER) + i2c.write(0, 0x15, v and 0x80 or 0x00) + i2c.stop(0) +end + +local function geti2cb6() + i2c.start(0) + i2c.address(0, 0x20, i2c.TRANSMITTER) + i2c.write(0, 0x13) + i2c.start(0) + i2c.address(0, 0x20, i2c.RECEIVER) + local v = i2c.read(0, 1):byte(1) + i2c.stop(0) + return (bit.band(v,0x40) ~= 0) +end + +N.test('gpio read 0', function() + seti2cb7(false) + ok(eq(0, gpio.read(8))) +end) + +N.test('gpio read 1', function() + seti2cb7(true) + ok(eq(1, gpio.read(8))) +end) + +N.test('i2c read 0', function() + gpio.write(7, 0) + ok(eq(false, geti2cb6())) +end) + +N.test('i2c read 1', function() + gpio.write(7, 1) + ok(eq(true, geti2cb6())) +end) + +N.testasync('gpio toggle trigger 1', function(next) + seti2cb7(false) + tmr.delay(10) + gpio.trig(8, "both", function(l,_,c) + ok(c == 1 and l == 1) + return next() + end) + seti2cb7(true) +end, true) + +N.testasync('gpio toggle trigger 2', function(next) + gpio.trig(8, "both", function(l,_,c) + ok(c == 1 and l == 0) + return next() + end) + seti2cb7(false) +end, true) + +N.test('gpio toggle trigger end', function() + gpio.trig(8, "none") + ok(true) +end) diff --git a/tests/NTest_lcd_i2c4bit.lua b/tests/NTest_lcd_i2c4bit.lua new file mode 100644 index 0000000000..e9aa543de3 --- /dev/null +++ b/tests/NTest_lcd_i2c4bit.lua @@ -0,0 +1,83 @@ +-- Run LiquidCrystal through some basic tests. Requires `liquidcrystal.lua` +-- and `l2-i2c4bit.lua` available available to `require`. +-- +-- This file ought to be named "NTest_liquidcrystal_i2c4bit" or something, +-- but it has its current name due to our default SPIFFS filename length limit. + +local N = ... +N = (N or require "NTest")("liquidcrystal-i2c4bit") + +local metalcd +local metaback +local backend +local lcd + +collectgarbage() +print("HEAP init", node.heap()) + +metalcd = require "liquidcrystal" +collectgarbage() print("HEAP constructor imported ", node.heap()) + +metaback = require "lc-i2c4bit" +collectgarbage() print("HEAP backend imported ", node.heap()) + +backend = metaback({ + address = 0x27, + id = 0, + speed = i2c.SLOW, + sda = 2, + scl = 1, +}) +collectgarbage() print("HEAP backend built", node.heap()) + +lcd = metalcd(backend, false, true, 20) +collectgarbage() print("HEAP lcd built", node.heap()) + +print("waiting for LCD to be unbusy before testing...") +while lcd:busy() do end + +N.test("custom character", function() + local glyph = { 0x1F, 0x15, 0x1B, 0x15, 0x1F, 0x10, 0x10, 0x0 } + lcd:customChar(0, glyph) + ok(eq(glyph,lcd:readCustom(0)), "read back") +end) + +N.test("draw and readback", function() + lcd:cursorMove(0) + lcd:write("abc") + lcd:cursorMove(10,1) + lcd:write("de") + lcd:cursorMove(10,2) + lcd:write("fg") + lcd:cursorMove(12,3) + lcd:write("hi\000") + lcd:cursorMove(18,4) + lcd:write("jk") + + lcd:home() ok(eq(0x61, lcd:read()), "read back 'a'") + ok(eq(0x62, lcd:read()), "read back 'b'") + lcd:cursorMove(11,1) ok(eq(0x65, lcd:read()), "read back 'e'") + lcd:cursorMove(11,2) ok(eq(0x67, lcd:read()), "read back 'g'") + lcd:cursorMove(13,3) ok(eq(0x69, lcd:read()), "read back 'i'") + lcd:cursorMove(14,3) ok(eq(0x00, lcd:read()), "read back 0" ) + lcd:cursorMove(19,4) ok(eq(0x6B, lcd:read()), "read back 'k'") + +end) + +N.test("update home", function() + lcd:home() lcd:write("l") + lcd:home() ok(eq(0x6C, lcd:read())) +end) + +N.testasync("clear", function(next) + -- clear and poll busy + lcd:clear() + tmr.create():alarm(5, tmr.ALARM_SEMI, function(tp) + if lcd:busy() then tp:start() else next() end + end) + lcd:home() -- work around busy polling incrementing position (XXX) + ok(eq(0x20, lcd:read()), "is space") + ok(eq(1, lcd:position())) -- having just read 1 from home, we should be at 1 +end) + + diff --git a/tests/NTest_lua.lua b/tests/NTest_lua.lua new file mode 100644 index 0000000000..13ebe34614 --- /dev/null +++ b/tests/NTest_lua.lua @@ -0,0 +1,6 @@ +local N = require "NTest" ("Lua detail tests") + +N.test('typeerror', function() + fail(function() math.abs("") end, "number expected, got string", "string") + fail(function() math.abs() end, "number expected, got no value", "no value") +end) diff --git a/tests/NTest_pixbuf.lua b/tests/NTest_pixbuf.lua new file mode 100644 index 0000000000..e07e4a3e2a --- /dev/null +++ b/tests/NTest_pixbuf.lua @@ -0,0 +1,288 @@ +local N = ... +N = (N or require "NTest")("pixbuf") + +local function initBuffer(buf, ...) + for i,v in ipairs({...}) do + buf:set(i, v, v*2, v*3, v*4) + end + return buf +end + +N.test('initialize a buffer', function() + local buffer = pixbuf.newBuffer(9, 3) + nok(buffer == nil) + ok(eq(buffer:size(), 9), "check size") + ok(eq(buffer:dump(), string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)), "initialize with 0") + + fail(function() pixbuf.newBuffer(9, -1) end, "should be a positive integer") + fail(function() pixbuf.newBuffer(0, 3) end, "should be a positive integer") + fail(function() pixbuf.newBuffer(-1, 3) end, "should be a positive integer") +end) + +N.test('have correct size', function() + local buffer = pixbuf.newBuffer(9, 3) + ok(eq(buffer:size(), 9), "check size") + buffer = pixbuf.newBuffer(9, 4) + ok(eq(buffer:size(), 9), "check size") +end) + +N.test('fill a buffer with one color', function() + local buffer = pixbuf.newBuffer(3, 3) + buffer:fill(1,222,55) + ok(eq(buffer:dump(), string.char(1,222,55,1,222,55,1,222,55)), "RGB") + buffer = pixbuf.newBuffer(3, 4) + buffer:fill(1,222,55,77) + ok(eq(buffer:dump(), string.char(1,222,55,77,1,222,55,77,1,222,55,77)), "RGBW") +end) + +N.test('replace correctly', function() + local buffer = pixbuf.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255)) + ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") + + buffer = pixbuf.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 2) + ok(eq(buffer:dump(), string.char(0,0,0,3,255,165,33,0,244,12,87,255,0,0,0)), "RGBW") + + buffer = pixbuf.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5) + ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") + + fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end, + "does not fit into destination") +end) + +N.test('replace correctly issue #2921', function() + local buffer = pixbuf.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -7) + ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") +end) + +N.test('get/set correctly', function() + local buffer = pixbuf.newBuffer(3, 4) + buffer:fill(1,222,55,13) + ok(eq({buffer:get(2)},{1,222,55,13})) + buffer:set(2, 4,53,99,0) + ok(eq({buffer:get(1)},{1,222,55,13})) + ok(eq({buffer:get(2)},{4,53,99,0})) + ok(eq(buffer:dump(), string.char(1,222,55,13,4,53,99,0,1,222,55,13)), "RGBW") + + fail(function() buffer:get(0) end, "index out of range") + fail(function() buffer:get(4) end, "index out of range") + fail(function() buffer:set(0,1,2,3,4) end, "index out of range") + fail(function() buffer:set(4,1,2,3,4) end, "index out of range") + fail(function() buffer:set(2,1,2,3) end, "number expected, got no value") + fail(function() buffer:set(2,1,2,3,4,5) end, "extra values given") +end) + +N.test('get/set multiple with string', function() + -- verify that :set does indeed return its input + local buffer = pixbuf.newBuffer(4, 3):set(1,"ABCDEF") + buffer:set(3,"LMNOPQ") + ok(eq(buffer:dump(), "ABCDEFLMNOPQ")) + + fail(function() buffer:set(4,"AAAAAA") end, "string size will exceed strip length") + fail(function() buffer:set(2,"AAAAA") end, "string does not contain whole LEDs") +end) + +N.test('fade correctly', function() + local buffer = pixbuf.newBuffer(1, 3) + buffer:fill(1,222,55) + buffer:fade(2) + ok(buffer:dump() == string.char(0,111,27), "RGB") + buffer:fill(1,222,55) + buffer:fade(3, pixbuf.FADE_OUT) + ok(buffer:dump() == string.char(0,math.floor(222/3),math.floor(55/3)), "RGB") + buffer:fill(1,222,55) + buffer:fade(3, pixbuf.FADE_IN) + ok(buffer:dump() == string.char(3,255,165), "RGB") + buffer = pixbuf.newBuffer(1, 4) + buffer:fill(1,222,55, 77) + buffer:fade(2, pixbuf.FADE_OUT) + ok(eq(buffer:dump(), string.char(0,111,27,38)), "RGBW") +end) + +N.test('mix correctly issue #1736', function() + local buffer1 = pixbuf.newBuffer(1, 3) + local buffer2 = pixbuf.newBuffer(1, 3) + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(256/8*7,buffer1,256/8,buffer2) + ok(eq({buffer1:get(1)}, {10,23,54})) +end) + +N.test('mix saturation correctly ', function() + local buffer1 = pixbuf.newBuffer(1, 3) + local buffer2 = pixbuf.newBuffer(1, 3) + + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(256/2,buffer1,-256,buffer2) + ok(eq({buffer1:get(1)}, {0,0,0})) + + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(25600,buffer1,256/8,buffer2) + ok(eq({buffer1:get(1)}, {255,255,255})) + + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(-257,buffer1,255,buffer2) + ok(eq({buffer1:get(1)}, {0,5,1})) +end) + +N.test('power', function() + local buffer = pixbuf.newBuffer(2, 4) + buffer:fill(10,22,54,234) + ok(eq(buffer:power(), 2*(10+22+54+234))) +end) + +N.test('shift LOGICAL', function() + local buffer1 = pixbuf.newBuffer(4, 4) + local buffer2 = pixbuf.newBuffer(4, 4) + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,0,7,8) + ok(buffer1 ~= buffer2, "disequality pre shift") + buffer1:shift(2) + ok(buffer1 == buffer2, "shift right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12,0,0) + buffer1:shift(-2) + ok(buffer1 == buffer2, "shift left") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,0,8,12) + buffer1:shift(1, nil, 2,3) + ok(buffer1 == buffer2, "shift middle right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,9,0,12) + buffer1:shift(-1, nil, 2,3) + ok(buffer1 == buffer2, "shift middle left") + + -- bounds checks, handle gracefully as string:sub does + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,8,9,12,0) + buffer1:shift(-1, pixbuf.SHIFT_LOGICAL, 0,5) + ok(buffer1 == buffer2, "shift left out of bound") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,7,8,9) + buffer1:shift(1, pixbuf.SHIFT_LOGICAL, 0,5) + ok(buffer1 == buffer2, "shift right out of bound") + +end) + +N.test('shift LOGICAL issue #2946', function() + local buffer1 = pixbuf.newBuffer(4, 4) + local buffer2 = pixbuf.newBuffer(4, 4) + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,0,0,0) + buffer1:shift(4) + ok(buffer1 == buffer2, "shift all right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,0,0,0) + buffer1:shift(-4) + ok(buffer1 == buffer2, "shift all left") + + fail(function() buffer1:shift(10) end, "shifting more elements than buffer size") + fail(function() buffer1:shift(-6) end, "shifting more elements than buffer size") +end) + +N.test('shift CIRCULAR', function() + local buffer1 = pixbuf.newBuffer(4, 4) + local buffer2 = pixbuf.newBuffer(4, 4) + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12,7,8) + buffer1:shift(2, pixbuf.SHIFT_CIRCULAR) + ok(buffer1 == buffer2, "shift right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12,7,8) + buffer1:shift(-2, pixbuf.SHIFT_CIRCULAR) + ok(buffer1 == buffer2, "shift left") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,9,8,12) + buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, 2,3) + ok(buffer1 == buffer2, "shift middle right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,9,8,12) + buffer1:shift(-1, pixbuf.SHIFT_CIRCULAR, 2,3) + ok(buffer1 == buffer2, "shift middle left") + + -- bounds checks, handle gracefully as string:sub does + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,8,9,12,7) + buffer1:shift(-1, pixbuf.SHIFT_CIRCULAR, 0,5) + ok(buffer1 == buffer2, "shift left out of bound") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,12,7,8,9) + buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, 0,5) + ok(buffer1 == buffer2, "shift right out of bound") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,12,7,8,9) + buffer1:shift(1, pixbuf.SHIFT_CIRCULAR, -12,12) + ok(buffer1 == buffer2, "shift right way out of bound") + +end) + +N.test('sub', function() + local buffer1 = pixbuf.newBuffer(4, 4) + initBuffer(buffer1,7,8,9,12) + buffer1 = buffer1:sub(4,3) + ok(eq(buffer1:size(), 0), "sub empty") + + local buffer2 = pixbuf.newBuffer(2, 4) + buffer1 = pixbuf.newBuffer(4, 4) + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12) + buffer1 = buffer1:sub(3,4) + ok(buffer1 == buffer2, "sub") + + buffer1 = pixbuf.newBuffer(4, 4) + buffer2 = pixbuf.newBuffer(4, 4) + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,8,9,12) + buffer1 = buffer1:sub(-12,33) + ok(buffer1 == buffer2, "out of bounds") +end) + +N.test('map', function() + local buffer1 = pixbuf.newBuffer(4, 4) + buffer1:fill(65,66,67,68) + + buffer1:map(function(a,b,c,d) return b,a,c,d end) + ok(eq("BACDBACDBACDBACD", buffer1:dump()), "swizzle") + + local buffer2 = pixbuf.newBuffer(4, 1) + buffer2:map(function(b,a,c,d) return c end, buffer1) -- luacheck: ignore + ok(eq("CCCC", buffer2:dump()), "projection") + + local buffer3 = pixbuf.newBuffer(4, 3) + buffer3:map(function(b,a,c,d) return a,b,d end, buffer1) -- luacheck: ignore + ok(eq("ABDABDABDABD", buffer3:dump()), "projection 2") + + buffer1:fill(70,71,72,73) + buffer1:map(function(c,a,b,d) return a,b,c,d end, buffer2, nil, nil, buffer3) + ok(eq("ABCDABCDABCDABCD", buffer1:dump()), "zip") + + buffer1 = pixbuf.newBuffer(2, 4) + buffer1:fill(70,71,72,73) + buffer2:set(1,"ABCD") + buffer3:set(1,"EFGHIJKLM") + buffer1:map(function(c,a,b,d) return a,b,c,d end, buffer2, 1, 2, buffer3, 2) + ok(eq("HIAJKLBM", buffer1:dump()), "partial zip") +end) + +--[[ +pixbuf.buffer:__concat() +--]] diff --git a/tests/NTest_tmr.lua b/tests/NTest_tmr.lua new file mode 100644 index 0000000000..570d75b5e4 --- /dev/null +++ b/tests/NTest_tmr.lua @@ -0,0 +1,99 @@ +local N = ... +N = (N or require "NTest")("tmr") + +N.testasync('SINGLE alarm', function(next) + local t = tmr.create(); + local count = 0 + t:alarm(200, tmr.ALARM_SINGLE, + function() + count = count + 1 + ok(count <= 1, "only 1 invocation") + next() + end) + + ok(true, "sync end") +end) + +N.testasync('SEMI alarm', function(next) + local t = tmr.create(); + local count = 0 + t:alarm(200, tmr.ALARM_SEMI, + function(tp) + count = count + 1 + if count <= 1 then + tp:start() + return + end + ok(eq(count, 2), "only 2 invocations") + next() + end) + ok(true, "sync end") +end) + +N.testasync('AUTO alarm', function(next) + local t = tmr.create(); + local count = 0 + t:alarm(200, tmr.ALARM_AUTO, + function(tp) + count = count + 1 + if count == 2 then + tp:stop() + return next() + end + ok(count < 2, "only 2 invocations") + end) + ok(true, "sync end") +end) + +N.testco('SINGLE alarm coroutine', function(getCB, waitCB) + local t = tmr.create(); + t:alarm(200, tmr.ALARM_SINGLE, getCB("timer")) + + local name, timer = waitCB() + ok(eq("timer", name), "CB name matches") + ok(eq(t, timer), "CB tmr instance matches") + + ok(true, "coroutine end") +end) + +N.testco('SEMI alarm coroutine', function(getCB, waitCB) + local t = tmr.create(); + t:alarm(200, tmr.ALARM_SEMI, getCB("timer")) + + local name, timer = waitCB() + ok(eq("timer", name), "CB name matches") + ok(eq(t, timer), "CB tmr instance matches") + + timer:start() + + name, timer = waitCB() + ok(eq("timer", name), "CB name matches again") + ok(eq(t, timer), "CB tmr instance matches again") + + ok(true, "coroutine end") +end) + +N.testco('AUTO alarm coroutine', function(getCB, waitCB) + local t = tmr.create(); + t:alarm(200, tmr.ALARM_AUTO, getCB("timer")) + + local name, timer = waitCB() + ok(eq("timer", name), "CB name matches") + ok(eq(t, timer), "CB tmr instance matches") + + name, timer = waitCB() + ok(eq("timer", name), "CB name matches again") + ok(eq(t, timer), "CB tmr instance matches again") + + timer:stop() + + ok(true, "coroutine end") +end) + +N.test('softwd set positive and negative values', function() + tmr.softwd(22) + tmr.softwd(0) + tmr.softwd(-1) -- disable it again + tmr.softwd(-22) -- disable it again +end) + diff --git a/tests/NTest_ws2812.lua b/tests/NTest_ws2812.lua new file mode 100644 index 0000000000..98dc0f4005 --- /dev/null +++ b/tests/NTest_ws2812.lua @@ -0,0 +1,262 @@ +local N = ... +N = (N or require "NTest")("ws2812 buffers") + +local buffer, buffer1, buffer2 + +local function initBuffer(buf, ...) + for i,v in ipairs({...}) do + buf:set(i, v, v*2, v*3, v*4) + end + return buf +end + +local function equalsBuffer(buf1, buf2) + return eq(buf1:dump(), buf2:dump()) +end + + +N.test('initialize a buffer', function() + buffer = ws2812.newBuffer(9, 3) + nok(buffer == nil) + ok(eq(buffer:size(), 9), "check size") + ok(eq(buffer:dump(), string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)), "initialize with 0") + + fail(function() ws2812.newBuffer(9, 0) end, "should be a positive integer") + fail(function() ws2812.newBuffer(9, -1) end, "should be a positive integer") + fail(function() ws2812.newBuffer(0, 3) end, "should be a positive integer") + fail(function() ws2812.newBuffer(-1, 3) end, "should be a positive integer") +end) + +N.test('have correct size', function() + buffer = ws2812.newBuffer(9, 3) + ok(eq(buffer:size(), 9), "check size") + buffer = ws2812.newBuffer(9, 22) + ok(eq(buffer:size(), 9), "check size") + buffer = ws2812.newBuffer(13, 1) + ok(eq(buffer:size(), 13), "check size") +end) + +N.test('fill a buffer with one color', function() + buffer = ws2812.newBuffer(3, 3) + buffer:fill(1,222,55) + ok(eq(buffer:dump(), string.char(1,222,55,1,222,55,1,222,55)), "RGB") + buffer = ws2812.newBuffer(3, 4) + buffer:fill(1,222,55, 77) + ok(eq(buffer:dump(), string.char(1,222,55,77,1,222,55,77,1,222,55,77)), "RGBW") +end) + +N.test('replace correctly', function() + buffer = ws2812.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255)) + ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") + + buffer = ws2812.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 2) + ok(eq(buffer:dump(), string.char(0,0,0,3,255,165,33,0,244,12,87,255,0,0,0)), "RGBW") + + buffer = ws2812.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -5) + ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") + + fail(function() buffer:replace(string.char(3,255,165,33,0,244,12,87,255), 4) end, "does not fit into destination") +end) + +N.test('replace correctly issue #2921', function() + buffer = ws2812.newBuffer(5, 3) + buffer:replace(string.char(3,255,165,33,0,244,12,87,255), -7) + ok(eq(buffer:dump(), string.char(3,255,165,33,0,244,12,87,255,0,0,0,0,0,0)), "RGBW") +end) + +N.test('get/set correctly', function() + buffer = ws2812.newBuffer(3, 4) + buffer:fill(1,222,55,13) + ok(eq({buffer:get(2)},{1,222,55,13})) + buffer:set(2, 4,53,99,0) + ok(eq({buffer:get(1)},{1,222,55,13})) + ok(eq({buffer:get(2)},{4,53,99,0})) + ok(eq(buffer:dump(), string.char(1,222,55,13,4,53,99,0,1,222,55,13)), "RGBW") + + fail(function() buffer:get(0) end, "index out of range") + fail(function() buffer:get(4) end, "index out of range") + fail(function() buffer:set(0,1,2,3,4) end, "index out of range") + fail(function() buffer:set(4,1,2,3,4) end, "index out of range") + fail(function() buffer:set(2,1,2,3) end, "number expected, got no value") +-- fail(function() buffer:set(2,1,2,3,4,5) end, "extra values given") +end) + +N.test('fade correctly', function() + buffer = ws2812.newBuffer(1, 3) + buffer:fill(1,222,55) + buffer:fade(2) + ok(buffer:dump() == string.char(0,111,27), "RGB") + buffer:fill(1,222,55) + buffer:fade(3, ws2812.FADE_OUT) + ok(buffer:dump() == string.char(0,math.floor(222/3),math.floor(55/3)), "RGB") + buffer:fill(1,222,55) + buffer:fade(3, ws2812.FADE_IN) + ok(buffer:dump() == string.char(3,255,165), "RGB") + buffer = ws2812.newBuffer(1, 4) + buffer:fill(1,222,55, 77) + buffer:fade(2, ws2812.FADE_OUT) + ok(eq(buffer:dump(), string.char(0,111,27,38)), "RGBW") +end) + +N.test('mix correctly issue #1736', function() + buffer1 = ws2812.newBuffer(1, 3) + buffer2 = ws2812.newBuffer(1, 3) + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(256/8*7,buffer1,256/8,buffer2) + ok(eq({buffer1:get(1)}, {10,23,54})) +end) + +N.test('mix saturation correctly ', function() + buffer1 = ws2812.newBuffer(1, 3) + buffer2 = ws2812.newBuffer(1, 3) + + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(256/2,buffer1,-256,buffer2) + ok(eq({buffer1:get(1)}, {0,0,0})) + + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(25600,buffer1,256/8,buffer2) + ok(eq({buffer1:get(1)}, {255,255,255})) + + buffer1:fill(10,22,54) + buffer2:fill(10,27,55) + buffer1:mix(-257,buffer1,255,buffer2) + ok(eq({buffer1:get(1)}, {0,5,1})) +end) + +N.test('power', function() + buffer = ws2812.newBuffer(2, 4) + buffer:fill(10,22,54,234) + ok(eq(buffer:power(), 2*(10+22+54+234))) +end) + +N.test('shift LOGICAL', function() + + buffer1 = ws2812.newBuffer(4, 4) + buffer2 = ws2812.newBuffer(4, 4) + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,0,7,8) + buffer1:shift(2) + ok(equalsBuffer(buffer1, buffer2), "shift right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12,0,0) + buffer1:shift(-2) + ok(equalsBuffer(buffer1, buffer2), "shift left") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,0,8,12) + buffer1:shift(1, nil, 2,3) + ok(equalsBuffer(buffer1, buffer2), "shift middle right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,9,0,12) + buffer1:shift(-1, nil, 2,3) + ok(equalsBuffer(buffer1, buffer2), "shift middle left") + + -- bounds checks, handle gracefully as string:sub does + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,8,9,12,0) + buffer1:shift(-1, ws2812.SHIFT_LOGICAL, 0,5) + ok(equalsBuffer(buffer1, buffer2), "shift left out of bound") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,7,8,9) + buffer1:shift(1, ws2812.SHIFT_LOGICAL, 0,5) + ok(equalsBuffer(buffer1, buffer2), "shift right out of bound") + +end) + +N.test('shift LOGICAL issue #2946', function() + buffer1 = ws2812.newBuffer(4, 4) + buffer2 = ws2812.newBuffer(4, 4) + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,0,0,0) + buffer1:shift(4) + ok(equalsBuffer(buffer1, buffer2), "shift all right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,0,0,0,0) + buffer1:shift(-4) + ok(equalsBuffer(buffer1, buffer2), "shift all left") + + fail(function() buffer1:shift(10) end, "shifting more elements than buffer size") + fail(function() buffer1:shift(-6) end, "shifting more elements than buffer size") +end) + +N.test('shift CIRCULAR', function() + buffer1 = ws2812.newBuffer(4, 4) + buffer2 = ws2812.newBuffer(4, 4) + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12,7,8) + buffer1:shift(2, ws2812.SHIFT_CIRCULAR) + ok(equalsBuffer(buffer1, buffer2), "shift right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12,7,8) + buffer1:shift(-2, ws2812.SHIFT_CIRCULAR) + ok(equalsBuffer(buffer1, buffer2), "shift left") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,9,8,12) + buffer1:shift(1, ws2812.SHIFT_CIRCULAR, 2,3) + ok(equalsBuffer(buffer1, buffer2), "shift middle right") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,9,8,12) + buffer1:shift(-1, ws2812.SHIFT_CIRCULAR, 2,3) + ok(equalsBuffer(buffer1, buffer2), "shift middle left") + + -- bounds checks, handle gracefully as string:sub does + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,8,9,12,7) + buffer1:shift(-1, ws2812.SHIFT_CIRCULAR, 0,5) + ok(equalsBuffer(buffer1, buffer2), "shift left out of bound") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,12,7,8,9) + buffer1:shift(1, ws2812.SHIFT_CIRCULAR, 0,5) + ok(equalsBuffer(buffer1, buffer2), "shift right out of bound") + + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,12,7,8,9) + buffer1:shift(1, ws2812.SHIFT_CIRCULAR, -12,12) + ok(equalsBuffer(buffer1, buffer2), "shift right way out of bound") + +end) + +N.test('sub', function() + buffer1 = ws2812.newBuffer(4, 4) + buffer2 = ws2812.newBuffer(4, 4) + initBuffer(buffer1,7,8,9,12) + buffer1 = buffer1:sub(4,3) + ok(eq(buffer1:size(), 0), "sub empty") + + buffer1 = ws2812.newBuffer(4, 4) + buffer2 = ws2812.newBuffer(2, 4) + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,9,12) + buffer1 = buffer1:sub(3,4) + ok(equalsBuffer(buffer1, buffer2), "sub") + + buffer1 = ws2812.newBuffer(4, 4) + buffer2 = ws2812.newBuffer(4, 4) + initBuffer(buffer1,7,8,9,12) + initBuffer(buffer2,7,8,9,12) + buffer1 = buffer1:sub(-12,33) + ok(equalsBuffer(buffer1, buffer2), "out of bounds") +end) + + +--[[ +ws2812.buffer:__concat() +--]] diff --git a/tests/NTest_ws2812_effects.lua b/tests/NTest_ws2812_effects.lua new file mode 100644 index 0000000000..4ffb80e7d9 --- /dev/null +++ b/tests/NTest_ws2812_effects.lua @@ -0,0 +1,27 @@ +local N = ... +N = (N or require "NTest")("ws2812_effects") + +local buffer + + +N.test('set_speed', function() + buffer = ws2812.newBuffer(9, 3) + ws2812_effects.init(buffer) + + ws2812_effects.set_speed(0) + ws2812_effects.set_speed(255) + + fail(function() ws2812_effects.set_speed(-1) end, "should be") + fail(function() ws2812_effects.set_speed(256) end, "should be") +end) + +N.test('set_brightness', function() + buffer = ws2812.newBuffer(9, 3) + ws2812_effects.init(buffer) + + ws2812_effects.set_brightness(0) + ws2812_effects.set_brightness(255) + + fail(function() ws2812_effects.set_brightness(-1) end, "should be") + fail(function() ws2812_effects.set_brightness(256) end, "should be") +end) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..ffcb6e621d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,217 @@ +# Introduction + +Welcome to the NodeMCU self-test suite. Here you will find our growing effort +to ensure that our software behaves as we think it should and that we do not +regress against earlier versions. + +Our tests are written using [NTest](./NTest/NTest.md), a lightweight yet +featureful framework for specifying unit tests. + +# Building and Running Test Software on NodeMCU Devices + +Naturally, to test NodeMCU on its intended hardware, you will need one or more +NodeMCU-capable boards. At present, the test environment is specified using +two ESP8266 Devices Under Test (DUTs), but we envision expanding this to mixed +ESP8266/ESP32 environments as well. + +Test programs live beside this file. While many test programs run on the +NodeMCU DUTs, but there is reason to want to orchestrate DUTs and the +environment using the host. Files matching the glob `NTest_*.lua` are intended +for on-DUT execution. + +## Manual Test Invocation + +At the moment, the testing regime and host-based orchestration is still in +development, and so things are a little more manual than perhaps desired. The +`NTest`-based test programs all assume that they can `require "NTest"`, and so +the easiest route to success is to + +* build an LFS image containing + + * [package.loader support for LFS](../lua_examples/lfs/_init.lua) + + * [NTest itself](./NTest/NTest.lua) + + * Any additional Lua support modules required (e.g., [mcp23017 + support](../lua_modules/mcp23017/mcp23017.lua) ) + +* build a firmware with the appropriate C modules + +* program the board with your firmware and LFS images + +* ensure that `package.loader` is patched appropriately on startup + +* transfer the `NTest_foo` program you wish to run to the device SPIFFS + (or have included it in the LFS). + +* at the interpreter prompt, say `dofile("NTest_foo.lua")` (or + `node.LFS.get("NTest_foo")()`) to run the `foo` test program. + +## Experimental Host Orchestration + +Enthusiastic testers are encouraged to try using our very new, very +experimental host test runner, [tap-driver.expect](./tap-driver.expect). To +use this program, in addition to the above, the LFS environment should contain +[NTestTapOut](./tests/utils/NTestTapOut.lua), an output adapter for `NTest`, +making it speak a slight variant of the [Test Anything +Protocol](https://testanything.org/). This structured output is scanned for +by the script on the host. + +You'll need `expect` and TCL and some TCL libraries available; on Debian, that +amounts to + + apt install tcl tcllib tclx8.4 expect + +This program should be invoked from beside this file with something like + + TCLLIBPATH=./expectnmcu ./tap-driver.expect -serial /dev/ttyUSB3 -lfs ./lfs.img NTest_file.lua + +This will... + +* transfer and install the specified LFS module (and reboot the device to load LFS) + +* transfer the test program + +* run the test program with `NTest` shimmed to use the `NTestTapOut` output + handler + +* summarize the results + +* return 0 if and only if all tests have passed + +This tool is quite flexible and takes a number of other options and flags +controlling aspects of its behavior: + +* Additional files, Lua or otherwise, may be transferred by specifing them + before the test to run (e.g., `./tap-driver.expect a.lua b.lua + NTest_foo.lua`); dually, a `-noxfer` flag will suppress transferring even the + last file. All transferred files are moved byte-for-byte to the DUT's + SPIFFS with names, but not directory components, preserved. + +* The `-lfs LFS.img` option need not be specified and, if not given, any + existing `LFS` image will remain on the device for use by the test. + +* A `-nontestshim` flag will skip attempting to shim the given test program + with `NTestTapOut`; the test program is expected to provide its own TAP + output. The `-tpfx` argument can be used to override the leading `TAP: ` + sigil used by the `NTestTapOut` output handler. + +* A `-runfunc` option indicates that the last argument is not a file to + transfer but rather a function to be run. It will be invoked at the REPL + with a single argument, the shimmed `NTest` constructor, unless `-nontestshim` + is given, in which case the argument will be `nil`. + +* A `-notests` option suppresses running tests (making the tool merely another + option for loading files to the device). + +Transfers will be significantly faster if +[pipeutils](../lua_examples/pipeutils.lua) is available to `require` on the +DUT, but a fallback strategy exists if not. We suggest either including +`pipeutils` in LFS images, in SPIFFS, or as the first file to be transferred. + +# NodeMCU Testing Environment + +Herein we define the environment our testing framework expects to see +when it runs. It is composed of two ESP8266 devices, each capable of +holding an entire NodeMCU firmware, LFS image, and SPIFFS file system, +as well as additional peripheral hardware. It is designed to fit +comfortably on a breadboard and so should be easily replicated and +integrated into any firmware validation testing. + +The test harness runs from a dedicated host computer, which is expected +to have reset- and programming-capable UART links to both ESP8266 +devices, as found on almost all ESP8266 boards with USB to UART +adapters, but the host does not necessarily need to use USB to connect, +so long as TXD, RXD, DTR, and RTS are wired across. + +A particular implementation of this can be found at [Test Harness](HardwareTestHarness.html). + +## Peripherals + +### I2C Bus + +There is an I2C bus hanging off DUT 0. Attached hardware is used both as +tests of modules directly and also to facilitate testing other modules +(e.g., gpio). + +#### MCP23017: I/O Expander + +At address 0x20. An 16-bit tristate GPIO expander, this chip is used to +test I2C, GPIO, and ADC functionality. This chip's interconnections are +as follows: + +MPC23017 | Purpose +---------|-------------------------------------------------------------- +/RESET |DUT0 reset. This resets the chip whenever the host computer resets DUT 0 over its serial link (using DTR/RTS). +B 0 |4K7 resistor to DUT 0 ADC. +B 1 |2K2 resistor to DUT 0 ADC. +B 5 |DUT1 GPIO16/WAKE via 4K7 resitor +B 6 |DUT0 GPIO13 via 4K7 resistor and DUT1 GPIO15 via 4K7 resistor +B 7 |DUT0 GPIO15 via 4K7 resistor and DUT1 GPIO13 via 4K7 resistor + +Notes: + +- DUT 0's ADC pin is connected via a 2K2 reistor to this chip's port + B, pin 1 and via a 4K7 resistor to port B, pin 0. This gives us the + ability to produce approximately 0 (both pins low), 1.1 (pin 0 high, + pin 1 low), 2.2 (pin 1 high, pin 0 low), and 3.3V (both pins high) + on the ADC pin. +- Port B pins 6 and 7 sit on the UART cross-wiring between DUT 0 and + DUT 1. The 23017 will be tristated for inter-DUT UART tests, but + these +- Port B pins 2, 3, and 4, as well as all of port A, remain available + for expansion. +- The interrupt pins are not yet routed, but could be. We reserve DUT + 0 GPIO 2 for this purpose with the understanding that the 23017's + interrupt functionality will be disabled (INTA, INTB set to + open-drain, GPINTEN set to 0) when not explicitly under test. + +ESP8266 Device 0 Connections +---------------------------- + +ESP | Usage +----------|---------------------------------------------------------- +GPIO 0 |Used to enter programming mode; otherwise unused in test environment. +GPIO 1 |Primary UART transmit; reserved for host communication +GPIO 2 |[reserved for 1-Wire] [+ reserved for 23017 INT[AB] connections] +GPIO 3 |Primary UART recieve; reserved for host communication +GPIO 4 |I2C SDA +GPIO 5 |I2C SCL +GPIO 6 |[Reserved for on-chip flash] +GPIO 7 |[Reserved for on-chip flash] +GPIO 8 |[Reserved for on-chip flash] +GPIO 9 |[Reserved for on-chip flash] +GPIO 10 |[Reserved for on-chip flash] +GPIO 11 |[Reserved for on-chip flash] +GPIO 12 | +GPIO 13 |Secondary UART RX; DUT 1 GPIO 15, I/O expander B 6 +GPIO 14 | +GPIO 15 |Secondary UART TX; DUT 1 GPIO 13, I/O expander B 7 +GPIO 16 | +ADC 0 |Resistor divider with I/O expander + +ESP8266 Device 1 Connections +---------------------------- + +ESP | Usage +----------|---------------------------------------------------------- +GPIO 0 |Used to enter programming mode; otherwise unused in test environment. +GPIO 1 |Primary UART transmit; reserved for host communication +GPIO 2 |[Reserved for WS2812] +GPIO 3 |Primary UART recieve; reserved for host communication +GPIO 4 | +GPIO 5 | +GPIO 6 |[Reserved for on-chip flash] +GPIO 7 |[Reserved for on-chip flash] +GPIO 8 |[Reserved for on-chip flash] +GPIO 9 |[Reserved for on-chip flash] +GPIO 10 |[Reserved for on-chip flash] +GPIO 11 |[Reserved for on-chip flash] +GPIO 12 |HSPI MISO +GPIO 13 |Secondary UART RX; DUT 0 GPIO 15, I/O exp B 7 via 4K7 Also used as HSPI MOSI for SPI tests +GPIO 14 |HSPI CLK +GPIO 15 |Secondary UART TX; DUT 0 GPIO 13, I/O exp B 6 via 4K7 Also used as HSPI /CS for SPI tests +GPIO 16 |I/O expander B 5 via 4K7 resistor, for deep-sleep tests +ADC 0 | + + diff --git a/tests/Test-harness-schematic-v1.pdf b/tests/Test-harness-schematic-v1.pdf new file mode 100644 index 0000000000..453133ba36 Binary files /dev/null and b/tests/Test-harness-schematic-v1.pdf differ diff --git a/tests/expectnmcu/core.tcl b/tests/expectnmcu/core.tcl new file mode 100644 index 0000000000..66ec5299ee --- /dev/null +++ b/tests/expectnmcu/core.tcl @@ -0,0 +1,136 @@ +namespace eval expectnmcu::core { + set panicre "powered by Lua \[0-9.\]+ on SDK \[0-9.\]+" + set promptstr "\n> " + + namespace export reboot waitboot connect + namespace export send_exp_prompt send_exp_res_prompt send_exp_prompt_c +} + +package require cmdline + +# Use DTR/RTS signaling to reboot the device +## I'm not sure why we have to keep resetting the mode, but so it goes. +proc ::expectnmcu::core::reboot { dev } { + set victimfd [open ${dev} ] + set mode [fconfigure ${victimfd} -mode ] + fconfigure ${victimfd} -mode ${mode} -ttycontrol {DTR 0 RTS 1} + sleep 0.1 + fconfigure ${victimfd} -mode ${mode} -ttycontrol {DTR 0 RTS 0} + close ${victimfd} +} + +proc ::expectnmcu::core::waitboot { victim } { + expect { + -i ${victim} "Formatting file system" { + set timeout 120 + exp_continue + } + -i ${victim} "powered by Lua" { } + timeout { return -code error "Timeout" } + } + # Catch nwf's system bootup, in case we're testing an existing system, + # rather than a blank firmware. + expect { + -i ${victim} -re "Reset delay!.*${::expectnmcu::core::promptstr}" { + send -i ${victim} "stop(true)\n" + expect -i ${victim} -ex ${::expectnmcu::core::promptstr} + } + -i ${victim} -ex ${::expectnmcu::core::promptstr} { } + timeout { return -code error "Timeout" } + } + + # Do a little more active synchronization with the DUT: send it a command + # and wait for the side-effect of that command to happen, thereby ensuring + # that the next prompt we see is after this point in the input. + send -i ${victim} "print(\"a\",\"z\")\n" + expect { + -i ${victim} -ex "a\tz" { } + } + expect { + -i ${victim} -ex ${::expectnmcu::core::promptstr} { } + timeout { return -code error "Timeout" } + } +} + +# Establish a serial connection to the device via socat. Takes +# -baud=N, -reboot=0/1/dontwait, -waitboot=0/1 optional parameters +proc ::expectnmcu::core::connect { dev args } { + set opts { + { baud.arg 115200 } + { reboot.arg 1 } + } + array set arg [::cmdline::getoptions args $opts] + + spawn "socat" "STDIO" "${dev},b${arg(baud)},raw,crnl" + close -onexec 1 -i ${spawn_id} + set victim ${spawn_id} + + # XXX? + set victimfd [open ${dev} ] + set mode [fconfigure ${victimfd} -mode ${arg(baud)},n,8,1 ] + + if { ${arg(reboot)} != 0 } { + ::expectnmcu::core::reboot ${dev} + if { ${arg(reboot)} != "dontwait" } { + ::expectnmcu::core::waitboot ${victim} + } + } + + close ${victimfd} + + return ${victim} +} + +# This one is somewhat "for experts only" -- it expects that you have either +# consumed whatever command you flung at the node or that you have some reason +# to not be concerned with its echo (and return) +proc ::expectnmcu::core::exp_prompt { sid } { + expect { + -i ${sid} -ex ${::expectnmcu::core::promptstr} { } + -i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout" } + } +} + +proc ::expectnmcu::core::send_exp_prompt { sid cmd } { + send -i ${sid} -- "${cmd}\n" + expect { + -i ${sid} -ex "${cmd}" { } + timeout { return -code error "Timeout" } + } + expect { + -i ${sid} -ex ${::expectnmcu::core::promptstr} { } + -i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout" } + } +} + +proc ::expectnmcu::core::send_exp_res_prompt { sid cmd res } { + send -i ${sid} -- "${cmd}\n" + expect { + -i ${sid} -ex "${cmd}" { } + timeout { return -code error "Timeout" } + } + expect { + -i ${sid} -re "${res}.*${::expectnmcu::core::promptstr}" { } + -i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + -i ${sid} -ex ${::expectnmcu::core::promptstr} { return -code error "Prompt before expected response" } + timeout { return -code error "Timeout" } + } +} + +proc ::expectnmcu::core::send_exp_prompt_c { sid cmd } { + send -i ${sid} -- "${cmd}\n" + expect { + -i ${sid} -ex "${cmd}" { } + timeout { return -code error "Timeout" } + } + expect { + -i ${sid} -ex "\n>> " { } + -i ${sid} -ex ${::expectnmcu::core::promptstr} { return -code error "Non-continuation prompt" } + -i ${sid} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout" } + } +} + +package provide expectnmcu::core 1.0 diff --git a/tests/expectnmcu/pkgIndex.tcl b/tests/expectnmcu/pkgIndex.tcl new file mode 100644 index 0000000000..64dbff6987 --- /dev/null +++ b/tests/expectnmcu/pkgIndex.tcl @@ -0,0 +1,12 @@ +# Tcl package index file, version 1.1 +# This file is generated by the "pkg_mkIndex" command +# and sourced either when an application starts up or +# by a "package unknown" script. It invokes the +# "package ifneeded" command to set up package-related +# information so that packages will be loaded automatically +# in response to "package require" commands. When this +# script is sourced, the variable $dir must contain the +# full path name of this file's directory. + +package ifneeded expectnmcu::core 1.0 [list source [file join $dir core.tcl]] +package ifneeded expectnmcu::xfer 1.0 [list source [file join $dir xfer.tcl]] diff --git a/tests/expectnmcu/xfer.tcl b/tests/expectnmcu/xfer.tcl new file mode 100644 index 0000000000..6c9b4139b6 --- /dev/null +++ b/tests/expectnmcu/xfer.tcl @@ -0,0 +1,148 @@ +namespace eval expectnmcu::xfer { +} + +package require expectnmcu::core + +# Open remote file `which` on `dev` in `mode` as Lua object `dfh` +proc ::expectnmcu::xfer::open { dev dfh which mode } { + ::expectnmcu::core::send_exp_prompt ${dev} "${dfh} = nil" + ::expectnmcu::core::send_exp_prompt ${dev} "${dfh} = file.open(\"${which}\",\"${mode}\")" + ::expectnmcu::core::send_exp_res_prompt ${dev} "=type(${dfh})" "userdata" +} + +# Close Lua file object `dfh` on `dev` +proc ::expectnmcu::xfer::close { dev dfh } { + ::expectnmcu::core::send_exp_prompt ${dev} "${dfh}:close()" +} + +# Write to `dfh` on `dev` at `where` `what`, using base64 as transport +# +# This does not split lines; write only short amounts of data. +proc ::expectnmcu::xfer::pwrite { dev dfh where what } { + send -i ${dev} -- [string cat \ + "do local d,e = encoder.fromBase64(\"[binary encode base64 -maxlen 0 ${what}]\");" \ + "${dfh}:seek(\"set\",${where});" \ + "print(${dfh}:write(d));" \ + "end\n" \ + ] + expect { + -i ${dev} -re "true\[\r\n\]+> " { } + -i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + -i ${dev} -ex "\n> " { return -code error "Bad result from pwrite" } + timeout { return -code error "Timeout while waiting for pwrite" } + } +} + +# Read `howmuch` byetes from `dfh` on `dev` at `where`, using base64 +# as transport. This buffers the whole data and its base64 encoding +# in device RAM; read only short strings. +proc ::expectnmcu::xfer::pread { dev dfh where howmuch } { + send -i ${dev} -- "${dfh}:seek(\"set\",${where}); print(encoder.toBase64(${dfh}:read(${howmuch})))\n" + expect { + -i ${dev} -re "\\)\\)\\)\[\r\n\]+(\[^\r\n\]+)\[\r\n\]+> " { + return [binary decode base64 ${expect_out(1,string)}] + } + -i ${dev} -ex "\n> " { return -code error "No reply to pread" } + -i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout while pread-ing" } + } +} + +# Check for pipeutils on the target device +proc ::expectnmcu::xfer::haspipeutils { dev } { + send -i ${dev} -- "local ok, pu = pcall(require, \"pipeutils\"); print(ok and type(pu) == \"table\" and pu.chunker and pu.debase64 and true or false)\n" + expect { + -i ${dev} -re "\[\r\n\]+false\[\r\n\]+> " { return 0 } + -i ${dev} -re "\[\r\n\]+true\[\r\n\]+> " { return 1 } + -i ${dev} -ex "\n> " { return -code error "No reply to pipeutils probe" } + -i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout while probing for pipeutils" } + } +} + +# Send local file `lfn` to the remote filesystem on `dev` and name it `rfn`. +# Use `dfo` as the Lua handle to the remote file for the duration of writing, +# (and `nil` it out afterwards) +proc ::expectnmcu::xfer::sendfile { dev lfn rfn {dfo "xfo"} } { + package require sha256 + + set has_pipeutils [::expectnmcu::xfer::haspipeutils ${dev} ] + + set ltf [::open ${lfn} ] + fconfigure ${ltf} -translation binary + file stat ${lfn} lfstat + ::expectnmcu::xfer::open ${dev} ${dfo} "${rfn}.sf" "w+" + + if { ${has_pipeutils} } { + # Send over a loader program + ::expectnmcu::core::send_exp_prompt_c ${dev} "do" + ::expectnmcu::core::send_exp_prompt_c ${dev} " local pu = require \"pipeutils\"" + ::expectnmcu::core::send_exp_prompt_c ${dev} " local ch = pu.chunker(function(d) ${dfo}:write(d) end, 256)" + ::expectnmcu::core::send_exp_prompt_c ${dev} " local db = pu.debase64(ch.write, function(ed,ee)" + ::expectnmcu::core::send_exp_prompt_c ${dev} " if ed:match(\"^%.\[\\r\\n\]*$\") then ch.flush() print(\"F I N\")" + ::expectnmcu::core::send_exp_prompt_c ${dev} " else print(\"ABORT\", ee, ed) end" + ::expectnmcu::core::send_exp_prompt_c ${dev} " uart.on(\"data\") end)" + # TODO: make echo use CRC not full string; probably best add to crypto module + ::expectnmcu::core::send_exp_prompt_c ${dev} " uart.on(\"data\", \"\\n\", function(x) db.write(x); uart.write(0, \"OK: \", x) end, 0)" + ::expectnmcu::core::send_exp_prompt ${dev} "end" + set xln 90 + } else { + set xln 48 + } + + set lho [sha2::SHA256Init] + + set fpos 0 + while { 1 } { + send_user ">> xfer ${fpos} of ${lfstat(size)}\n" + set data [read ${ltf} ${xln}] + sha2::SHA256Update ${lho} ${data} + if { ${has_pipeutils} } { + set estr [binary encode base64 -maxlen 0 ${data}] + send -i ${dev} -- "${estr}\n" + expect { + -i ${dev} -ex "OK: ${estr}" { expect -i ${dev} -re "\[\r\n\]+" {} } + -i ${dev} -ex "\n> " { return -code error "Prompt while sending data" } + -i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout while sending data" } + } + } else { + ::expectnmcu::xfer::pwrite ${dev} ${dfo} ${fpos} ${data} + } + set fpos [expr $fpos + ${xln}] + if { [string length ${data}] != ${xln} } { break } + } + + if { ${has_pipeutils} } { + send -i ${dev} -- ".\n" + expect { + -i ${dev} -re "F I N\[\r\n\]+" { } + -i ${dev} -ex "\n> " { return -code error "Prompt while awaiting acknowledgement" } + -i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout while awaiting acknowledgement" } + } + } + + ::close ${ltf} + ::expectnmcu::xfer::close ${dev} ${dfo} + ::expectnmcu::core::send_exp_prompt ${dev} "${dfo} = nil" + + set exphash [sha2::Hex [sha2::SHA256Final ${lho}]] + + send -i ${dev} "=encoder.toHex(crypto.fhash(\"sha256\",\"${rfn}.sf\"))\n" + expect { + -i ${dev} -re "\[\r\n\]+(\[a-f0-9\]+)\[\r\n\]+> " { + if { ${expect_out(1,string)} != ${exphash} } { + return -code error \ + "Sendfile checksum mismatch: ${expect_out(1,string)} != ${exphash}" + } + } + -i ${dev} -re ${::expectnmcu::core::panicre} { return -code error "Panic!" } + timeout { return -code error "Timeout while verifying checksum" } + } + + ::expectnmcu::core::send_exp_prompt ${dev} "file.remove(\"${rfn}\")" + ::expectnmcu::core::send_exp_res_prompt ${dev} "=file.rename(\"${rfn}.sf\", \"${rfn}\")" "true" +} + +package provide expectnmcu::xfer 1.0 diff --git a/tests/tap-driver.expect b/tests/tap-driver.expect new file mode 100755 index 0000000000..eed538160e --- /dev/null +++ b/tests/tap-driver.expect @@ -0,0 +1,266 @@ +#!/usr/bin/env expect + +# Push a file to the device, run it, and watch the tests run +# +# A typical invocation looks like: +# TCLLIBPATH=./expectnmcu ./tap-driver.expect -serial /dev/ttyUSB3 ./mispec.lua ./mispec_file.lua +# +# For debugging the driver itself, it may be useful to invoke expect with -d, +# which will give a great deal of diagnostic information about the expect state +# machine's internals: +# +# TCLLIBPATH=./expectnmcu expect -d ./tap-driver.expect ... +# +# The -debug option will turn on some additional reporting from this driver program, as well. + + +package require expectnmcu::core +package require expectnmcu::xfer + +package require cmdline +set cmd_parameters { + { serial.arg "/dev/ttyUSB0" "Set the serial interface name" } + { tpfx.arg "TAP: " "Set the expected TAP test prefix" } + { lfs.arg "" "Flash a file to LFS" } + { noxfer "Do not send files, just run script" } + { runfunc "Last argument is function, not file" } + { notests "Don't run tests, just xfer files" } + { nontestshim "Don't shim NTest when testing" } + { debug "Enable debugging reporting" } +} +set cmd_usage "- A NodeMCU Lua-based-test runner" +if {[catch {array set cmdopts [cmdline::getoptions ::argv $cmd_parameters $cmd_usage]}]} { + send_user [cmdline::usage $cmd_parameters $cmd_usage] + send_user "\n Additional arguments should be files be transferred\n" + send_user " The last file transferred will be run with `dofile`\n" + exit 0 +} + +if { ${cmdopts(noxfer)} } { + if { [ llength ${::argv} ] > 1 } { + send_user "No point in more than one argument if noxfer given\n" + exit 1 + } +} { + set xfers ${::argv} + + if { ${cmdopts(runfunc)} } { + # Last argument is command, not file to xfer + set xfers [lreplace xfers end end] + } + + foreach arg ${xfers} { + if { ! [file exists ${arg}] } { + send_user "File ${arg} does not exist\n" + exit 1 + } + } +} + +if { ${cmdopts(lfs)} ne "" } { + if { ! [file exists ${cmdopts(lfs)}] } { + send_user "LFS file does not exist\n" + exit 1 + } +} + +proc sus { what } { send_user "\n===> ${what} <===\n" } +proc sui { what } { send_user "\n---> ${what} <---\n" } +proc sud { what } { + upvar 1 cmdopts cmdopts + if { ${cmdopts(debug)} } { send_user "\n~~~> ${what} <~~~\n" } +} + +set victim [::expectnmcu::core::connect ${cmdopts(serial)}] +sus "Machine has booted" + +if { ${cmdopts(lfs)} ne "" } { + ::expectnmcu::xfer::sendfile ${victim} ${cmdopts(lfs)} "tap-driver.lfs" + send -i ${victim} "=node.LFS.reload(\"tap-driver.lfs\")\n" + ::expectnmcu::core::waitboot ${victim} +} + +if { ! ${cmdopts(noxfer)} } { + foreach arg ${xfers} { + ::expectnmcu::xfer::sendfile ${victim} ${arg} [file tail ${arg}] + } +} + +set tfn [file tail [lindex ${::argv} end ] ] + +if { ${cmdopts(notests)} || ${tfn} eq "" } { + sus "No tests requested, and so operations are completed" + exit 0 +} + +sus "Files transferred; running ${tfn}" + +if { ! ${cmdopts(nontestshim)} } { + ::expectnmcu::core::send_exp_prompt_c ${victim} "function ntshim(...)" + ::expectnmcu::core::send_exp_prompt_c ${victim} " local test = (require \"NTest\")(...)" + ::expectnmcu::core::send_exp_prompt_c ${victim} " test.outputhandler = require\"NTestTapOut\"" + ::expectnmcu::core::send_exp_prompt_c ${victim} " return test" + ::expectnmcu::core::send_exp_prompt ${victim} "end" +} else { + sui "Not shimming NTest output; test must report its own TAP messages" +} + +# ntshim may be nil at this point if -nontestshim was given; that's fine +if { ${cmdopts(runfunc)} } { + send -i ${victim} "[ lindex ${::argv} end ](ntshim)\n" + expect -i ${victim} -re "\\(ntshim\\)\[\r\n\]+" { } +} else { + send -i ${victim} "assert(loadfile(\"${tfn}\"))(ntshim)\n" + expect -i ${victim} -re "assert\\(loadfile\\(\"${tfn}\"\\)\\)\\(ntshim\\)\[\r\n\]+" { } +} + +set tpfx ${cmdopts(tpfx)} +set toeol "\[^\n\]*(?=\n)" + +# Wait for the test to start and tell us how many +# success lines we should expect +set ntests 0 +set timeout 10 +expect { + -i ${victim} -re "${tpfx}1\\.\\.(\\d+)(?=\r?\n)" { + global ntests + set ntests $expect_out(1,string) + } + -i ${victim} -re "${tpfx}Bail out!${toeol}" { + sus "Bail out before start" + exit 2 + } + -i ${victim} -re ${::expectnmcu::core::panicre} { + sus "Panic!" + exit 2 + } + # A prefixed line other than a plan (1..N) or bailout means we've not got + # a plan. Leave ${ntests} at 0 and proceed to run the protocol. + -i ${victim} -notransfer -re "${tpfx}${toeol}" { } + # -i ${victim} -ex "\n> " { + # sus "Prompt before start!" + # exit 2 + # } + # Consume other outputs and discard as if they were comments + # This must come as the last pattern that looks at input + -i ${victim} -re "(?p).${toeol}" { exp_continue } + timeout { + send_user "Failure: time out getting started\n" + exit 2 + } +} + +if { ${ntests} == 0 } { + sus "System did not report plan; will look for summary at end" +} else { + sus "Expecting ${ntests} test results" +} + +set timeout 60 +set exitwith 0 +set failures 0 +for {set this 1} {${ntests} == 0 || ${this} <= ${ntests}} {incr this} { + expect { + -i ${victim} -re "${tpfx}#${toeol}" { + sud "Harness got comment: ${expect_out(buffer)}" + exp_continue + } + -i ${victim} -re "${tpfx}ok (\\d+)\\D${toeol}" { + sud "Harness acknowledge OK! ${this} ${expect_out(1,string)}" + set tid ${expect_out(1,string)} + if { ${tid} != "" && ${tid} != ${this} } { + sui "WARNING: Test reporting misaligned at ${this} (got ${tid})" + } + } + -i ${victim} -re "${tpfx}ok #${toeol}" { + sud "Harness acknowledge anonymous ok! ${this}" + } + -i ${victim} -re "${tpfx}not ok (\\d+)\\D${toeol}" { + sud "Failure in simulation after ${this} ${expect_out(1,string)}" + set tid ${expect_out(1,string)} + if { ${tid} != "" && ${tid} != ${this} } { + sui "WARNING: Test reporting misaligned at ${this}" + } + set exitwith [expr max(${exitwith},1)] + incr failures + } + -i ${victim} -re "${tpfx}not ok #${toeol}" { + sud "Failure (anonymous) in simulation after ${this}" + set exitwith [expr max(${exitwith},1)] + incr failures + } + -i ${victim} -re "${tpfx}Bail out!${toeol}" { + sus "Bail out after ${this} tests" + exit 2 + } + -i ${victim} -re "${tpfx}POST 1\\.\\.(\\d+)(?=\r?\n)" { + # A post-factual plan; this must be the end of testing + global ntests + set ntests ${expect_out(1,string)} + if { ${ntests} != ${this} } { + sus "Postfix plan claimed ${ntests} but we saw ${this}" + set exitwith [expr max(${exitwith},2)] + incr failures + } + # break out of for loop + set this ${ntests} + } + -i ${victim} -re "${tpfx}${toeol}" { + sus "TAP line not understood!" + exit 2 + } + # -i ${victim} -ex ${::expectnmcu::core::promptstr} { + # sus "Prompt while running tests!" + # exit 2 + # } + -i ${victim} -re ${::expectnmcu::core::panicre} { + sus "Panic!" + exit 2 + } + # Consume other outputs and discard as if they were comments + # This must come as the last pattern that looks at input + -re "(?p).${toeol}" { exp_continue } + timeout { + send_user "Failure: time out\n" + exit 2 + } + } +} + +# We think we're done running tests; send a final command for synchronization +send -i ${victim} "print(\"f\",\"i\",\"n\")\n" +expect -i ${victim} -re "print\\(\"f\",\"i\",\"n\"\\)\[\r\n\]+" { } +expect { + -i ${victim} -ex "f\ti\tn" { } + + -i ${victim} -re "${tpfx}#${toeol}" { + sud "Harness got comment: ${expect_out(buffer)}" + exp_continue + } + + -i ${victim} -re "${tpfx}Bail out!${toeol}" { + sus "Bail out after all tests finished" + exit 2 + } + -i ${victim} -re "${tpfx}${toeol}" { + sus "Unexpected TAP output after tests finished" + exit 2 + } + -i ${victim} -re ${::expectnmcu::core::panicre} { + sus "Panic!" + exit 2 + } + + -re "(?p).${toeol}" { exp_continue } + timeout { + send_user "Failure: time out\n" + exit 2 + } +} + +if { ${exitwith} == 0 } { + sus "All tests reported in OK" +} else { + sus "${failures} TEST FAILURES; REVIEW LOGS" +} +exit ${exitwith} diff --git a/tests/utils/NTestTapOut.lua b/tests/utils/NTestTapOut.lua new file mode 100644 index 0000000000..6adfc674f6 --- /dev/null +++ b/tests/utils/NTestTapOut.lua @@ -0,0 +1,30 @@ +-- This is a NTest output handler that formats its output in a way that +-- resembles the Test Anything Protocol (though prefixed with "TAP: " so we can +-- more readily find it in comingled output streams). + +local nrun +return function(e, test, msg, err) + msg = msg or "" + err = err or "" + if e == "pass" then + print(("\nTAP: ok %d %s # %s"):format(nrun, test, msg)) + nrun = nrun + 1 + elseif e == "fail" then + print(("\nTAP: not ok %d %s # %s: %s"):format(nrun, test, msg, err)) + nrun = nrun + 1 + elseif e == "except" then + print(("\nTAP: not ok %d %s # exn; %s: %s"):format(nrun, test, msg, err)) + nrun = nrun + 1 + elseif e == "abort" then + print(("\nTAP: Bail out! %d %s # exn; %s: %s"):format(nrun, test, msg, err)) + elseif e == "start" then + -- We don't know how many tests we plan to run, so emit a comment instead + print(("\nTAP: # STARTUP %s"):format(test)) + nrun = 1 + elseif e == "finish" then + -- Ah, now, here we go; we know how many tests we ran, so signal completion + print(("\nTAP: POST 1..%d"):format(nrun)) + elseif #msg ~= 0 or #err ~= 0 then + print(("\nTAP: # %s: %s: %s"):format(test, msg, err)) + end +end diff --git a/tools/check_docs_module_linkage.sh b/tools/check_docs_module_linkage.sh new file mode 100755 index 0000000000..7f586160d4 --- /dev/null +++ b/tools/check_docs_module_linkage.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# get all linked module docs for mkdocs.yml +grep "modules/" mkdocs.yml | sed "s/ *- .*: *'//" | sed "s/'//" | sort > /tmp/doc + +# get all module and lua_module *.md files +find docs/modules/ docs/lua-modules/ -name "*.md" | sed "sxdocs/xx" | sort > /tmp/files + +diff /tmp/doc /tmp/files && echo "all *.md files are reflected in mkdocs.yml" + diff --git a/tools/luacheck_NTest_config.lua b/tools/luacheck_NTest_config.lua new file mode 100644 index 0000000000..98bd1ecd0d --- /dev/null +++ b/tools/luacheck_NTest_config.lua @@ -0,0 +1,11 @@ +stds.nodemcu_libs = {} +loadfile ("tools/luacheck_config.lua")(stds) +local empty = { } + +stds.nodemcu_libs.read_globals.ok = empty +stds.nodemcu_libs.read_globals.nok = empty +stds.nodemcu_libs.read_globals.eq = empty +stds.nodemcu_libs.read_globals.fail = empty +stds.nodemcu_libs.read_globals.spy = empty + +std = "lua51+lua53+nodemcu_libs" diff --git a/tools/luacheck_config.lua b/tools/luacheck_config.lua index 80f303e191..4857c24ee7 100644 --- a/tools/luacheck_config.lua +++ b/tools/luacheck_config.lua @@ -1,3 +1,4 @@ +stds = stds or ... -- set stds if this script is called by another config script local empty = { } local read_write = {read_only = false} @@ -423,6 +424,7 @@ stds.nodemcu_libs = { restore = empty, setcpufreq = empty, setpartitiontable = empty, + setonerror = empty, sleep = empty, stripdebug = empty, writercr = empty, @@ -485,6 +487,22 @@ stds.nodemcu_libs = { new = empty } }, + pipe = { + fields = { + create = empty + } + }, + pixbuf = { + fields = { + FADE_IN = empty, + FADE_OUT = empty, + SHIFT_CIRCULAR = empty, + SHIFT_LOGICAL = empty, + init = empty, + newBuffer = empty, + write = empty + } + }, pwm = { fields = { close = empty, @@ -607,7 +625,8 @@ stds.nodemcu_libs = { PROG = empty, STOP = empty, UP = empty, - sendcommand = empty + sendcommand = empty, + listen = empty } }, spi = { @@ -942,10 +961,8 @@ stds.nodemcu_libs = { }, pack = empty, unpack = empty, - size = empty, - package = {fields = {seeall = read_write}}, - _ENV = empty + package = {fields = {seeall = read_write}} } } -std = "lua51+nodemcu_libs" \ No newline at end of file +std = "lua51+lua53+nodemcu_libs" diff --git a/tools/travis/run-luacheck-linux.sh b/tools/travis/run-luacheck-linux.sh index 929f92e4d2..2e3b3d6c81 100755 --- a/tools/travis/run-luacheck-linux.sh +++ b/tools/travis/run-luacheck-linux.sh @@ -68,5 +68,8 @@ fi echo "Static analysys of" find lua_modules lua_examples -iname "*.lua" -print0 | xargs -0 echo - (find lua_modules lua_examples -iname "*.lua" -print0 | xargs -0 luacheck --config tools/luacheck_config.lua) || exit + +echo "Static analysys of" +find tests -iname "*.lua" -print0 | xargs -0 echo +(find tests -iname "*.lua" -print0 | xargs -0 luacheck --config tools/luacheck_NTest_config.lua) || exit diff --git a/tools/travis/run-luacheck-windows.sh b/tools/travis/run-luacheck-windows.sh index 682510de66..acea9c9f9d 100644 --- a/tools/travis/run-luacheck-windows.sh +++ b/tools/travis/run-luacheck-windows.sh @@ -9,5 +9,8 @@ fi echo "Static analysys of" find lua_modules lua_examples -iname "*.lua" -print0 | xargs -0 echo - (find lua_modules lua_examples -iname "*.lua" -print0 | xargs -0 cache/luacheck.exe --config tools/luacheck_config.lua) || exit + +echo "Static analysys of" +find tests -iname "*.lua" -print0 | xargs -0 echo +(find tests -iname "*.lua" -print0 | xargs -0 cache/luacheck.exe --config tools/luacheck_NTest_config.lua) || exit diff --git a/tools/update_buildinfo.sh b/tools/update_buildinfo.sh index 7caa09e5d5..747caee92a 100755 --- a/tools/update_buildinfo.sh +++ b/tools/update_buildinfo.sh @@ -38,8 +38,12 @@ cat > $TEMPFILE << EndOfMessage #ifdef LUA_NUMBER_INTEGRAL #define BUILDINFO_BUILD_TYPE "integer" #else +#ifdef LUA_NUMBER_64BITS +#define BUILDINFO_BUILD_TYPE "double" +#else #define BUILDINFO_BUILD_TYPE "float" #endif +#endif #define USER_PROLOG "$USER_PROLOG" #define BUILDINFO_BRANCH "$BRANCH"
native
Build luac.crossBuild luac.cross not needed if you use Terry's webservice or Docker to later compile LFS image not needed if you use Terry's webservice or Docker to later compile LFS image not needed if you use Terry's webservice or Docker to later compile LFS imagenative native
download from release
Compile Lua into
LFS image
webservice