├── webUI.png ├── .github ├── FUNDING.yml └── workflows │ └── platformio_ci.yml ├── scripts ├── extra_linker_flags.py └── apply_patches.py ├── lib ├── LinakDeskEmbedded │ ├── DeskControllerFactory.h │ ├── Constants.h │ ├── BluetoothConnection.h │ └── BluetoothConnection.cpp ├── LinakDeskCore │ ├── HeightSpeedData.h │ ├── ConnectionInterface.h │ ├── DeskController.h │ └── DeskController.cpp └── README ├── .gitignore ├── test ├── README └── test_common │ └── LinakDesk │ ├── ConnectionMock.h │ └── DeskControllerTest.cpp ├── patches └── gcc-ar-ranlib.patch ├── LICENSE ├── include └── README ├── README.md ├── src ├── html.h └── main.cpp ├── .clang-format └── platformio.ini /webUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krzmaz/LinakDeskEsp32Controller/HEAD/webUI.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [krzmaz] 4 | custom: ["https://www.paypal.me/krzmaz"] 5 | -------------------------------------------------------------------------------- /scripts/extra_linker_flags.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | 3 | # 4 | # Dump build environment (for debug) 5 | # print(env.Dump()) 6 | # 7 | 8 | # make sure linker knows that we need debug info (for decoding backtraces) 9 | env.Append( 10 | LINKFLAGS=[ 11 | "-ggdb", 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /lib/LinakDeskEmbedded/DeskControllerFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "BluetoothConnection.h" 4 | #include "DeskController.h" 5 | 6 | namespace LinakDesk { 7 | class DeskControllerFactory { 8 | public: 9 | DeskControllerFactory(); 10 | virtual ~DeskControllerFactory() = 0; 11 | static DeskController make() { return DeskController(std::make_unique()); }; 12 | }; 13 | } // namespace LinakDesk 14 | -------------------------------------------------------------------------------- /lib/LinakDeskCore/HeightSpeedData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace LinakDesk { 4 | class HeightSpeedData { 5 | public: 6 | HeightSpeedData(unsigned short heightRaw, short speedRaw) : mHeightRaw{heightRaw}, mSpeedRaw{speedRaw} {} 7 | unsigned short getRawHeight() const {return mHeightRaw;} 8 | short getSpeed() const {return mSpeedRaw;} 9 | private: 10 | unsigned short mHeightRaw; 11 | short mSpeedRaw; 12 | }; 13 | } // namespace LinakDesk 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Platformio 35 | .pio 36 | .vscode 37 | 38 | # Other 39 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /patches/gcc-ar-ranlib.patch: -------------------------------------------------------------------------------- 1 | diff --git a/builder/main.py b/builder/main.py 2 | index 7802ec6..985ef81 100644 3 | --- a/builder/main.py 4 | +++ b/builder/main.py 5 | @@ -131,14 +131,14 @@ if mcu == "esp32c3": 6 | env.Replace( 7 | __get_board_f_flash=_get_board_f_flash, 8 | 9 | - AR="%s-elf-ar" % toolchain_arch, 10 | + AR="%s-elf-gcc-ar" % toolchain_arch, 11 | AS="%s-elf-as" % toolchain_arch, 12 | CC="%s-elf-gcc" % toolchain_arch, 13 | CXX="%s-elf-g++" % toolchain_arch, 14 | GDB="%s-elf-gdb" % toolchain_arch, 15 | OBJCOPY=join( 16 | platform.get_package_dir("tool-esptoolpy") or "", "esptool.py"), 17 | - RANLIB="%s-elf-ranlib" % toolchain_arch, 18 | + RANLIB="%s-elf-gcc-ranlib" % toolchain_arch, 19 | SIZETOOL="%s-elf-size" % toolchain_arch, 20 | 21 | ARFLAGS=["rc"], 22 | -------------------------------------------------------------------------------- /test/test_common/LinakDesk/ConnectionMock.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "ConnectionInterface.h" 6 | 7 | namespace LinakDesk { 8 | class ConnectionMock : public ConnectionInterface { 9 | public: 10 | MOCK_METHOD1(connect, bool(const std::string&)); 11 | MOCK_CONST_METHOD0(disconnect, void()); 12 | MOCK_CONST_METHOD0(isConnected, bool()); 13 | MOCK_CONST_METHOD0(getHeight, unsigned short()); 14 | MOCK_CONST_METHOD1(attachHeightSpeedCallback, void(const std::function&)); 15 | MOCK_CONST_METHOD0(detachHeightSpeedCallback, void()); 16 | MOCK_CONST_METHOD0(startMoveTorwards, void()); 17 | MOCK_CONST_METHOD1(moveTorwards, void(unsigned short height)); 18 | MOCK_CONST_METHOD0(stopMove, void()); 19 | }; 20 | 21 | } // namespace LinakDesk 22 | -------------------------------------------------------------------------------- /scripts/apply_patches.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from os.path import join, isfile 4 | 5 | Import("env") 6 | 7 | PLATFORM_DIR = env.PioPlatform().get_dir() 8 | patchflag_path = join(PLATFORM_DIR, ".patching-done") 9 | 10 | # patch file only if we didn't do it before 11 | if not isfile(join(PLATFORM_DIR, ".patching-done")): 12 | original_file = join(PLATFORM_DIR, "builder", "main.py") 13 | patched_file = join("patches", "gcc-ar-ranlib.patch") 14 | 15 | assert isfile(original_file) and isfile(patched_file) 16 | 17 | # If the patching fails, you can change the platform package manually and create 18 | # a file called `.patching-done` to indicate that. For more details see: 19 | # https://github.com/krzmaz/LinakDeskEsp32Controller/issues/13 20 | if env.Execute("patch %s %s" % (original_file, patched_file)) != 0: 21 | raise Exception("Problem while applying platform patches!\n\n" 22 | "See scripts/apply_patches.py for more details!\n") 23 | 24 | def _touch(path): 25 | with open(path, "w") as fp: 26 | fp.write("") 27 | 28 | env.Execute(lambda *args, **kwargs: _touch(patchflag_path)) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Krzysztof Mazur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/LinakDeskCore/ConnectionInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "HeightSpeedData.h" 8 | 9 | namespace LinakDesk { 10 | class ConnectionInterface { 11 | public: 12 | virtual ~ConnectionInterface() = default; 13 | virtual bool connect(const std::string& bluetoothAddress) = 0; 14 | virtual void disconnect() const = 0; 15 | virtual bool isConnected() const = 0; 16 | virtual unsigned short getHeightRaw() const = 0; 17 | virtual unsigned short getHeightMm() const = 0; 18 | virtual void attachHeightSpeedCallback(const std::function& callback) const = 0; 19 | virtual void detachHeightSpeedCallback() const = 0; 20 | virtual void startMoveTorwards() const = 0; 21 | virtual void moveTorwards(unsigned short height) const = 0; 22 | virtual void stopMove() const = 0; 23 | virtual const std::optional& getMemoryPosition(unsigned char positionNumber) const = 0; 24 | virtual bool setMemoryPosition(unsigned char positionNumber, unsigned short value) = 0; 25 | virtual const std::optional& getDeskOffset() const = 0; 26 | }; 27 | } // namespace LinakDesk 28 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /.github/workflows/platformio_ci.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: 7 | - main 8 | schedule: 9 | # run on an arbitrary hour every day (to check that it still builds) 10 | # * is a special character in YAML so you have to quote this string 11 | - cron: '42 10 * * *' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Cache pip 24 | uses: actions/cache@v2 25 | with: 26 | path: ~/.cache/pip 27 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 28 | restore-keys: | 29 | ${{ runner.os }}-pip- 30 | - name: Cache PlatformIO 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.platformio 34 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 35 | 36 | - name: Set up Python 37 | uses: actions/setup-python@v2 38 | 39 | - name: Install PlatformIO 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install --upgrade platformio 43 | 44 | - name: Run PlatformIO 45 | run: pio run -e esp32dev -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /lib/LinakDeskCore/DeskController.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "ConnectionInterface.h" 10 | #include "HeightSpeedData.h" 11 | 12 | namespace LinakDesk { 13 | 14 | class DeskController { 15 | public: 16 | explicit DeskController(std::unique_ptr connection); 17 | ~DeskController(); 18 | 19 | bool connect(std::string bluetoothAddress); 20 | void disconnect(); 21 | bool isConnected() const; 22 | 23 | const std::optional& getMemoryPosition(unsigned char positionNumber) const; 24 | std::optional getMemoryPositionMm(unsigned char positionNumber) const; 25 | bool setMemoryPositionFromCurrentHeight(unsigned char positionNumber); 26 | bool moveToHeightRaw(unsigned short destinationHeight); 27 | bool moveToHeightMm(unsigned short destinationHeight); 28 | unsigned short getHeightRaw() const; 29 | unsigned short getHeightMm() const; 30 | 31 | void loop(); 32 | 33 | static const std::function printingCallback; 34 | static unsigned short sLastHeight; 35 | static short sLastSpeed; 36 | 37 | private: 38 | void endMove(); 39 | void startMoveToHeight(); 40 | std::unique_ptr mConnection; 41 | bool mMoveStartPending = false; 42 | bool mIsMoving = false; 43 | bool mGoingUp = false; 44 | unsigned short mDestinationHeight = 0; 45 | unsigned short mMoveStartHeight = 0; 46 | unsigned short mPreviousHeight = 0; 47 | unsigned long mLastCommandSendTime = 0; 48 | }; 49 | 50 | } // namespace LinakDesk 51 | -------------------------------------------------------------------------------- /lib/LinakDeskEmbedded/Constants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace LinakDesk { 6 | 7 | namespace Constants { 8 | static constexpr unsigned char UserIdCommandData[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 9 | 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16}; 10 | } // namespace Constants 11 | 12 | namespace BleConstants { 13 | static const BLEUUID NameServiceUUID((unsigned short)0x1800); 14 | static const BLEUUID NameCharacteristicUUID((unsigned short)0x2a00u); 15 | static const BLEUUID ControlServiceUUID(0x99fa0001, 0x338a, 0x1024, 0x8a49009c0215f78a); 16 | static const BLEUUID ControlCharacteristicUUID(0x99fa0002, 0x338a, 0x1024, 0x8a49009c0215f78a); 17 | static const BLEUUID DpgServiceUUID(0x99fa0010, 0x338a, 0x1024, 0x8a49009c0215f78a); 18 | static const BLEUUID DpgCharacteristicUUID(0x99fa0011, 0x338a, 0x1024, 0x8a49009c0215f78a); 19 | static const BLEUUID OutputServiceUUID(0x99fa0020, 0x338a, 0x1024, 0x8a49009c0215f78a); 20 | static const BLEUUID OutputCharacteristicUUID(0x99fa0021, 0x338a, 0x1024, 0x8a49009c0215f78a); 21 | static const BLEUUID InputServiceUUID(0x99fa0030, 0x338a, 0x1024, 0x8a49009c0215f78a); 22 | static const BLEUUID InputCharacteristicUUID(0x99fa0031, 0x338a, 0x1024, 0x8a49009c0215f78a); 23 | } // namespace BleConstants 24 | 25 | enum class DpgCommand { 26 | Capabilities = 0x80, 27 | UserID = 0x86, 28 | DeskOffset = 0x81, 29 | ReminderSetting = 0x88, 30 | MemoryPosition1 = 0x89, 31 | MemoryPosition2 = 0x8a, 32 | MemoryPosition3 = 0x8b, 33 | MemoryPosition4 = 0x8c 34 | }; 35 | 36 | } // namespace LinakDesk 37 | -------------------------------------------------------------------------------- /lib/LinakDeskEmbedded/BluetoothConnection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "ConnectionInterface.h" 10 | #include "Constants.h" 11 | 12 | namespace LinakDesk { 13 | class BluetoothConnection : public ConnectionInterface { 14 | public: 15 | explicit BluetoothConnection(); 16 | ~BluetoothConnection(); 17 | bool connect(const std::string& bluetoothAddress) override; 18 | void disconnect() const override; 19 | bool isConnected() const override; 20 | unsigned short getHeightRaw() const override; 21 | unsigned short getHeightMm() const override; 22 | void attachHeightSpeedCallback(const std::function& callback) const override; 23 | void detachHeightSpeedCallback() const override; 24 | void startMoveTorwards() const override; 25 | void moveTorwards(unsigned short height) const override; 26 | void stopMove() const override; 27 | const std::optional& getMemoryPosition(unsigned char positionNumber) const override; 28 | bool setMemoryPosition(unsigned char positionNumber, unsigned short value) override; 29 | const std::optional& getDeskOffset() const override; 30 | 31 | // BLE library allows only one callback to be attached, so we might as well make it static 32 | static std::optional> sHeightSpeedCallback; 33 | 34 | private: 35 | // mimic the calls done by LinakDeskApp after connection 36 | void setupDesk(); 37 | // We need to query the name, otherwise the controller won't react 38 | void queryName() const; 39 | 40 | std::string dpgReadCommand(DpgCommand command); 41 | std::string dpgWriteCommand(DpgCommand command, const unsigned char* data, unsigned char length); 42 | 43 | void loadMemoryPosition(DpgCommand command); 44 | void setMemoryPosition(DpgCommand command, unsigned short value); 45 | 46 | void writeUInt16(BLERemoteCharacteristic* charcteristic, unsigned short value) const; 47 | 48 | std::unique_ptr mBleClient; 49 | bool mIsConnected = false; 50 | std::optional mRawOffset; 51 | std::optional mMemoryPosition1; 52 | std::optional mMemoryPosition2; 53 | std::optional mMemoryPosition3; 54 | 55 | BLERemoteCharacteristic* mOutputChar = nullptr; 56 | BLERemoteCharacteristic* mInputChar = nullptr; 57 | BLERemoteCharacteristic* mControlChar = nullptr; 58 | BLERemoteCharacteristic* mDpgChar = nullptr; 59 | }; 60 | 61 | } // namespace LinakDesk 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinakDeskEsp32Controller [![PlatformIO CI](https://github.com/krzmaz/LinakDeskEsp32Controller/actions/workflows/platformio_ci.yml/badge.svg?branch=main)](https://github.com/krzmaz/LinakDeskEsp32Controller/actions/workflows/platformio_ci.yml) 2 | 3 | The goal of this project is creating an easy to use bluetooth bridge for my Ikea Idasen desk, that uses a Linak controller. 4 | 5 | I often switch computers, that's why the desktop controllers do not suit me, but they were a great source of information - you can see the links in the references section. 6 | Now it also offers Alexa support for voice controlling your desk! 7 | 8 | # Features 9 | * HTTP API and Web UI for: 10 | * Moving to a desired height 11 | * Getting the current height (also using the offset from Linak controller to get reading in cm) 12 | * Editing memory positions (Helpful to make the desk stop on correct height when moving manually) 13 | * Amazon Alexa integration 14 | 15 | # Web UI 16 | 17 | At the root URL there is an Web UI for moving the desk and setting the favorite positions. 18 | 19 | ![Web UI](./webUI.png "Web UI") 20 | 21 | # Amazon Alexa integration 22 | 23 | The project uses the [fauxmoESP](https://github.com/vintlabs/fauxmoESP) library to emulate a philips hue lightbulb that can be discovered and controlled by Alexa. 24 | Current implementation uses Memory Position 3 as `on` state, and Memory Position 1 as `off` state. 25 | To move the desk to desired position you can just ask Alexa to turn on or off the desk using the desk name set during config, for example: 26 | **Alexa, turn on standing desk** (if you leave the default name - standing desk) 27 | 28 | 29 | # Getting started 30 | 1. Compile the `esp32dev` environment and upload the binary to an esp32. 31 | 2. Connect to the Access Point starting with ESP_[...] 32 | 3. Fill out your WiFi credentials, desk name and desk Bluetooth address 33 | 4. Save your configuration and wait for the ESP to connect to WiFi and your desk. 34 | (For the first connection you will need to press the pairing button on the desk.) 35 | 5. Test it! :) 36 | 37 | Current implementation offers a simple HTTP GET API for getting the height and moving to height: 38 | ``` 39 | standing-desk.local/getHeight 40 | ``` 41 | 42 | ``` 43 | standing-desk.local/moveToHeight?destination=700 44 | ``` 45 | You can also use values in milimeters using: 46 | ``` 47 | standing-desk.local/getHeightMm 48 | ``` 49 | 50 | ``` 51 | standing-desk.local/moveToHeightMm?destination=1000 52 | ``` 53 | Aditionally you can save current height as one of three (1-3) favorite positions to make the desk stop there when moving manually: 54 | ``` 55 | standing-desk.local/saveCurrentPosAsFav?position=3 56 | ``` 57 | 58 | mDNS name will be set from the desk name set in WiFiManager, with the spaces changed to `-`. 59 | 60 | You can use the IP address of the device if you're having problems with mDNS 61 | 62 | To change the settings reboot the ESP twice within 10 seconds and connect to the created WiFi. 63 | 64 | # References 65 | * https://github.com/zewelor/linak_bt_desk/ 66 | * https://github.com/anetczuk/linak_bt_desk/ 67 | * https://github.com/anson-vandoren/linak-desk-spec -------------------------------------------------------------------------------- /src/html.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "DeskController.h" 3 | 4 | extern LinakDesk::DeskController controller; 5 | extern char deskName [32]; 6 | 7 | namespace html { 8 | const char index_html[] = 9 | R"=====( $DeskName$ - Main Menu

$DeskName$

Current height:
$Height$mm
Go to: mm

)====="; 10 | 11 | String processor(const String& var) { 12 | if (var == "DeskName") { 13 | return String(deskName); 14 | } 15 | if (var == "Pos1") { 16 | return String(controller.getMemoryPositionMm(1).value_or(0) / 10); 17 | } 18 | if (var == "Pos2") { 19 | return String(controller.getMemoryPositionMm(2).value_or(0) / 10); 20 | } 21 | if (var == "Pos3") { 22 | return String(controller.getMemoryPositionMm(3).value_or(0) / 10); 23 | } 24 | if (var == "Height") { 25 | return String(controller.getHeightMm()); 26 | } 27 | return String(""); 28 | } 29 | } // namespace html 30 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Right 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: All 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: Never 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: MultiLine 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: false 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Attach 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 120 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: true 60 | DeriveLineEnding: true 61 | DerivePointerAlignment: false 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Regroup 70 | IncludeCategories: 71 | - Regex: '<[[:alnum:]]+>' 72 | Priority: 2 73 | SortPriority: 0 74 | - Regex: '<.*>' 75 | Priority: 3 76 | - Regex: '".*"' 77 | Priority: 4 78 | IncludeIsMainRegex: '(Test)?$' 79 | IncludeIsMainSourceRegex: '' 80 | IndentCaseLabels: false 81 | IndentGotoLabels: true 82 | IndentPPDirectives: None 83 | IndentWidth: 4 84 | IndentWrappedFunctionNames: false 85 | JavaScriptQuotes: Leave 86 | JavaScriptWrapImports: true 87 | KeepEmptyLinesAtTheStartOfBlocks: true 88 | MacroBlockBegin: '' 89 | MacroBlockEnd: '' 90 | MaxEmptyLinesToKeep: 1 91 | NamespaceIndentation: Inner 92 | ObjCBinPackProtocolList: Auto 93 | ObjCBlockIndentWidth: 2 94 | ObjCSpaceAfterProperty: false 95 | ObjCSpaceBeforeProtocolList: true 96 | PenaltyBreakAssignment: 2 97 | PenaltyBreakBeforeFirstCallParameter: 19 98 | PenaltyBreakComment: 300 99 | PenaltyBreakFirstLessLess: 120 100 | PenaltyBreakString: 1000 101 | PenaltyBreakTemplateDeclaration: 10 102 | PenaltyExcessCharacter: 1000000 103 | PenaltyReturnTypeOnItsOwnLine: 60 104 | PointerAlignment: Left 105 | ReflowComments: true 106 | SortIncludes: true 107 | SortUsingDeclarations: true 108 | SpaceAfterCStyleCast: false 109 | SpaceAfterLogicalNot: false 110 | SpaceAfterTemplateKeyword: true 111 | SpaceBeforeAssignmentOperators: true 112 | SpaceBeforeCpp11BracedList: false 113 | SpaceBeforeCtorInitializerColon: true 114 | SpaceBeforeInheritanceColon: true 115 | SpaceBeforeParens: ControlStatements 116 | SpaceBeforeRangeBasedForLoopColon: true 117 | SpaceInEmptyBlock: false 118 | SpaceInEmptyParentheses: false 119 | SpacesBeforeTrailingComments: 1 120 | SpacesInAngles: false 121 | SpacesInConditionalStatement: false 122 | SpacesInContainerLiterals: true 123 | SpacesInCStyleCastParentheses: false 124 | SpacesInParentheses: false 125 | SpacesInSquareBrackets: false 126 | SpaceBeforeSquareBrackets: false 127 | Standard: Latest 128 | StatementMacros: 129 | - Q_UNUSED 130 | - QT_REQUIRE_VERSION 131 | TabWidth: 8 132 | UseCRLF: false 133 | UseTab: Never 134 | ... 135 | 136 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = esp32dev 13 | ; #src_dir = ./googlemock 14 | ; #src_dir = ./googletest 15 | ; src_dir = ./src 16 | 17 | [env:esp32dev] 18 | ; 22 Dec 2021: #feature/arduino-idf-master is 33ff4132ef9a79be50552bd89e6fd44b9518643c 19 | platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-idf-master 20 | platform_packages = 21 | ; use upstream Git version 22 | ; 01 Jan 2022: #2.0.2 is caef4006af491130136b219c1205bdcf8f08bf2b 23 | framework-arduino-esp32 @ https://github.com/espressif/arduino-esp32#2.0.2 24 | 25 | board = esp32dev 26 | framework = arduino 27 | monitor_filters = esp32_exception_decoder ;, log2file 28 | monitor_speed = 115200 29 | extra_scripts = 30 | ; you can disable debug linker flag to reduce binary size (comment out line below), but the backtraces will become less readable 31 | scripts/extra_linker_flags.py 32 | ; fix the platform package to use gcc-ar and gcc-ranlib to enable lto linker plugin 33 | ; more detail: https://embeddedartistry.com/blog/2020/04/13/prefer-gcc-ar-to-ar-in-your-buildsystems/ 34 | pre:scripts/apply_patches.py 35 | 36 | ; Not using OTAs for now, so this can be used to increase the flash capacity 37 | ; board_build.partitions = no_ota.csv 38 | 39 | build_flags = 40 | -DCORE_DEBUG_LEVEL=0 41 | ; ; Debug logs - might need the no_ota partition setting to make the code fit into flash 42 | ; -DCORE_DEBUG_LEVEL=5 43 | ; -DCONFIG_NIMBLE_CPP_ENABLE_RETURN_CODE_TEXT=1 44 | ; -DCONFIG_NIMBLE_CPP_ENABLE_GAP_EVENT_CODE_TEXT=1 45 | 46 | -DTEMPLATE_PLACEHOLDER="\'$\'" 47 | -DCONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED=1 48 | -DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL_DISABLED=1 49 | -DCONFIG_BT_NIMBLE_ROLE_BROADCASTER_DISABLED=1 50 | -DCONFIG_BT_NIMBLE_MAX_BONDS=1 51 | -DCONFIG_BT_NIMBLE_MAX_CCCDS=1 52 | -DCONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 53 | 54 | -DCONFIG_BT_NIMBLE_PINNED_TO_CORE=1 55 | -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 56 | -DCONFIG_ASYNC_TCP_USE_WDT=1 57 | 58 | -std=gnu++17 59 | -Wall 60 | -Wextra 61 | 62 | -flto 63 | build_unflags = 64 | -std=gnu++11 65 | ; re-enable lto - afaik it was only disabled because of https://github.com/espressif/esp-idf/issues/3989 66 | -fno-lto 67 | upload_speed = 921600 68 | ; debug_tool = esp-prog 69 | ; upload_protocol = esp-prog 70 | ; debug_init_break = tbreak setup 71 | lib_ignore = 72 | ; ignore BLE lib as we use NimBLE 73 | ESP32 BLE Arduino 74 | ; ignore LittleFS as it's now part of IDF 75 | LittleFS_esp32 76 | lib_deps = 77 | bblanchon/ArduinoJson @ 6.18.5 78 | khoih-prog/ESPAsync_WiFiManager @ 1.9.8 79 | h2zero/NimBLE-Arduino @ 1.3.4 80 | vintlabs/FauxmoESP @ 3.4.0 81 | arkhipenko/TaskScheduler@^3.6.0 82 | 83 | 84 | [env:googletest_esp32] 85 | platform = https://github.com/platformio/platform-espressif32.git#feature/idf-v4.0 86 | platform_packages = 87 | ; use upstream Git version 88 | framework-arduino-esp32 @ https://github.com/espressif/arduino-esp32#idf-release/v4.2 89 | board = esp32dev 90 | framework = arduino 91 | monitor_speed = 115200 92 | monitor_filters = esp32_exception_decoder 93 | build_flags = -std=gnu++17 94 | build_unflags = -std=gnu++11 95 | test_build_project_src = yes 96 | lib_compat_mode = off 97 | 98 | src_filter = - + 99 | ; src_dir = test/test_common/LinakDesk 100 | lib_ignore = 101 | LinakDeskEmbedded 102 | lib_deps = 103 | # RECOMMENDED 104 | # Accept new functionality in a backwards compatible manner and patches 105 | google/googletest @ ^1.10.0 106 | 107 | [env:native] 108 | platform = native 109 | build_type = debug 110 | build_flags = 111 | -std=gnu++17 112 | -g 113 | -pthread 114 | -Wall 115 | -Wextra 116 | ; -Wpedantic 117 | build_unflags = -std=gnu++11 118 | lib_compat_mode = off 119 | ; test_build_project_src = no 120 | src_filter = - 121 | lib_ignore = 122 | LinakDeskEmbedded 123 | 124 | lib_deps = 125 | # RECOMMENDED 126 | # Accept new functionality in a backwards compatible manner and patches 127 | # mainline of arduino-mock doesn't work for me, but top of 'client' branch does: 128 | https://github.com/balp/arduino-mock#83307c89793ad62ab7d0bf722615103da5a1f46f 129 | googletest @ ^1.10.0 -------------------------------------------------------------------------------- /lib/LinakDeskCore/DeskController.cpp: -------------------------------------------------------------------------------- 1 | #include "DeskController.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace LinakDesk { 10 | 11 | unsigned short DeskController::sLastHeight = std::numeric_limits::max(); 12 | 13 | short DeskController::sLastSpeed = std::numeric_limits::max(); 14 | 15 | DeskController::DeskController(std::unique_ptr connection) : mConnection(std::move(connection)) {} 16 | 17 | DeskController::~DeskController() {} 18 | 19 | bool DeskController::connect(std::string bluetoothAddress) { 20 | if (mConnection->isConnected()) { 21 | Serial.println("[DeskController::connect] Already connected, disconnecting!"); 22 | mConnection->disconnect(); 23 | } 24 | Serial.printf("[DeskController::connect] trying to connect to: %s\n", bluetoothAddress.c_str()); 25 | return mConnection->connect(bluetoothAddress); 26 | } 27 | void DeskController::disconnect() { mConnection->disconnect(); } 28 | 29 | bool DeskController::isConnected() const { return mConnection->isConnected(); } 30 | 31 | unsigned short DeskController::getHeightRaw() const { 32 | if (mIsMoving) { 33 | return sLastHeight; 34 | } 35 | return mConnection->getHeightRaw(); 36 | } 37 | unsigned short DeskController::getHeightMm() const { 38 | if (mIsMoving) { 39 | return (sLastHeight + mConnection->getDeskOffset().value_or(0)) / 10; 40 | } 41 | return mConnection->getHeightMm(); 42 | } 43 | 44 | const std::optional& DeskController::getMemoryPosition(unsigned char positionNumber) const { 45 | return mConnection->getMemoryPosition(positionNumber); 46 | } 47 | 48 | std::optional DeskController::getMemoryPositionMm(unsigned char positionNumber) const { 49 | auto pos = mConnection->getMemoryPosition(positionNumber); 50 | if (pos) { 51 | *pos += mConnection->getDeskOffset().value_or(0); 52 | return std::move(pos); 53 | } 54 | return std::move(pos); 55 | } 56 | 57 | bool DeskController::setMemoryPositionFromCurrentHeight(unsigned char positionNumber) { 58 | if (positionNumber > 0 && positionNumber < 4) { 59 | return mConnection->setMemoryPosition(positionNumber, getHeightRaw()); 60 | } 61 | return false; 62 | } 63 | 64 | bool DeskController::moveToHeightMm(unsigned short destinationHeight) { 65 | auto offset = mConnection->getDeskOffset(); 66 | if (offset) { 67 | return moveToHeightRaw(destinationHeight * 10 - offset.value()); 68 | } 69 | return false; 70 | } 71 | 72 | bool DeskController::moveToHeightRaw(unsigned short destinationHeight) { 73 | if (!isConnected() || mIsMoving) { 74 | return false; 75 | } 76 | mDestinationHeight = destinationHeight; 77 | mMoveStartPending = true; 78 | return true; 79 | } 80 | 81 | void DeskController::startMoveToHeight() { 82 | mMoveStartPending = false; 83 | mMoveStartHeight = mConnection->getHeightRaw(); 84 | if (mMoveStartHeight == mDestinationHeight) { 85 | return; 86 | } 87 | mGoingUp = std::signbit(mMoveStartHeight - mDestinationHeight); 88 | sLastHeight = std::numeric_limits::max(); 89 | sLastSpeed = std::numeric_limits::max(); 90 | 91 | mConnection->attachHeightSpeedCallback(printingCallback); 92 | mConnection->startMoveTorwards(); 93 | 94 | mConnection->moveTorwards(mDestinationHeight); 95 | mLastCommandSendTime = millis(); 96 | mIsMoving = true; 97 | } 98 | 99 | void DeskController::loop() { 100 | if (mMoveStartPending) { 101 | startMoveToHeight(); 102 | return; 103 | } 104 | if (!mIsMoving) { 105 | return; 106 | } 107 | if (millis() - mLastCommandSendTime > 200) { 108 | if (sLastHeight == std::numeric_limits::max()) { 109 | // For some reason the callback wasn't called 110 | endMove(); 111 | return; 112 | } 113 | 114 | if (mPreviousHeight == sLastHeight) { 115 | // We didn't move since the last time, something's wrong 116 | endMove(); 117 | return; 118 | } 119 | 120 | if (sLastHeight != mDestinationHeight) { 121 | if (sLastSpeed == 0) { 122 | // We've stopped at the wrong height, something's wrong 123 | endMove(); 124 | return; 125 | } 126 | if (!std::signbit(sLastSpeed) != mGoingUp) { 127 | // We're moving in the wrong direction, something's wrong 128 | endMove(); 129 | return; 130 | } 131 | 132 | mPreviousHeight = sLastHeight; 133 | mConnection->moveTorwards(mDestinationHeight); 134 | mLastCommandSendTime = millis(); 135 | return; 136 | } 137 | // We reached our destination 138 | endMove(); 139 | } 140 | } 141 | 142 | void DeskController::endMove() { 143 | mConnection->stopMove(); 144 | mConnection->detachHeightSpeedCallback(); 145 | mIsMoving = false; 146 | } 147 | 148 | const std::function DeskController::printingCallback = [](const HeightSpeedData& data) { 149 | Serial.print("Notify callback for HeightSpeed: "); 150 | Serial.print("Height: "); 151 | sLastHeight = data.getRawHeight(); 152 | Serial.print(sLastHeight); 153 | Serial.print(" Speed: "); 154 | sLastSpeed = data.getSpeed(); 155 | Serial.println(sLastSpeed); 156 | }; 157 | 158 | } // namespace LinakDesk 159 | -------------------------------------------------------------------------------- /lib/LinakDeskEmbedded/BluetoothConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "BluetoothConnection.h" 2 | 3 | #include 4 | 5 | namespace { 6 | void adapterCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { 7 | if (LinakDesk::BluetoothConnection::sHeightSpeedCallback && length >= 4) { 8 | uint16_t height = *(uint16_t*)pData; 9 | short speed = *(uint16_t*)(pData + 2); 10 | LinakDesk::HeightSpeedData hsData(height, speed); 11 | LinakDesk::BluetoothConnection::sHeightSpeedCallback->operator()(hsData); 12 | } 13 | } 14 | 15 | IRAM_ATTR static void printStringAsHex(const std::string& input, bool breakLine = true) { 16 | for (const auto& ch : input) { 17 | if (ch < 0x10) { 18 | Serial.print('0'); 19 | } 20 | Serial.print(ch, HEX); 21 | Serial.print(' '); 22 | } 23 | if (breakLine) { 24 | Serial.println(); 25 | } 26 | } 27 | 28 | IRAM_ATTR static void printingNotifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, 29 | size_t length, bool isNotify) { 30 | Serial.print("Notify callback for characteristic: "); 31 | Serial.println(pBLERemoteCharacteristic->getUUID().toString().c_str()); 32 | Serial.print("Data: "); 33 | printStringAsHex(std::string((char*)pData, length)); 34 | } 35 | } // namespace 36 | 37 | namespace LinakDesk { 38 | 39 | std::optional> BluetoothConnection::sHeightSpeedCallback = {}; 40 | 41 | BluetoothConnection::BluetoothConnection() 42 | : mBleClient(BLEDevice::createClient(), [](BLEClient* client) { BLEDevice::deleteClient(client); }) {} 43 | 44 | BluetoothConnection::~BluetoothConnection() { 45 | disconnect(); 46 | if (BLEDevice::getInitialized()) { 47 | BLEDevice::deinit(); 48 | } 49 | } 50 | 51 | bool BluetoothConnection::connect(const std::string& bluetoothAddress) { 52 | if (!BLEDevice::getInitialized()) { 53 | BLEDevice::init("BLE32"); 54 | // set bonding requirement so that the device stores the bonding info 55 | BLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND); 56 | BLEDevice::setSecurityIOCap(BLE_HS_IO_KEYBOARD_ONLY); 57 | } 58 | auto connected = mBleClient->connect(NimBLEAddress(bluetoothAddress, BLE_ADDR_RANDOM)); 59 | if (connected) { 60 | mInputChar = mBleClient->getService(BleConstants::InputServiceUUID) 61 | ->getCharacteristic(BleConstants::InputCharacteristicUUID); 62 | mOutputChar = mBleClient->getService(BleConstants::OutputServiceUUID) 63 | ->getCharacteristic(BleConstants::OutputCharacteristicUUID); 64 | mControlChar = mBleClient->getService(BleConstants::ControlServiceUUID) 65 | ->getCharacteristic(BleConstants::ControlCharacteristicUUID); 66 | mDpgChar = mBleClient->getService(BleConstants::DpgServiceUUID) 67 | ->getCharacteristic(BleConstants::DpgCharacteristicUUID); 68 | setupDesk(); 69 | Serial.println("connected"); 70 | } 71 | return connected; 72 | } 73 | 74 | void BluetoothConnection::disconnect() const { 75 | if (mBleClient->isConnected()) { 76 | mBleClient->disconnect(); 77 | } 78 | } 79 | 80 | bool BluetoothConnection::isConnected() const { return mBleClient->isConnected(); } 81 | 82 | void BluetoothConnection::writeUInt16(BLERemoteCharacteristic* charcteristic, unsigned short value) const { 83 | uint8_t data[2]; 84 | data[0] = value; 85 | data[1] = value >> 8; 86 | charcteristic->writeValue(data, 2, true); 87 | } 88 | 89 | void BluetoothConnection::setupDesk() { 90 | queryName(); 91 | mDpgChar->subscribe(true, printingNotifyCallback); 92 | // basic dpg read comand has the same first and last byte, the middle one is the actual command value 93 | // more info: https://github.com/anson-vandoren/linak-desk-spec/blob/master/dpg_commands.md 94 | 95 | auto deskOffsetCommandOutput = dpgReadCommand(DpgCommand::DeskOffset); 96 | if (deskOffsetCommandOutput[2] == 0x01) { 97 | mRawOffset = *(unsigned short*)(deskOffsetCommandOutput.c_str() + 3); 98 | } 99 | 100 | loadMemoryPosition(DpgCommand::MemoryPosition1); 101 | loadMemoryPosition(DpgCommand::MemoryPosition2); 102 | loadMemoryPosition(DpgCommand::MemoryPosition3); 103 | 104 | dpgReadCommand(DpgCommand::UserID); 105 | 106 | dpgWriteCommand(DpgCommand::UserID, Constants::UserIdCommandData, 16); 107 | 108 | mDpgChar->unsubscribe(); 109 | } 110 | 111 | void BluetoothConnection::queryName() const { 112 | const TickType_t delay = 500 / portTICK_PERIOD_MS; 113 | vTaskDelay(delay); // aparently needed (by the linak controller maybe?) 114 | auto name = mBleClient->getService(BleConstants::NameServiceUUID) 115 | ->getCharacteristic(BleConstants::NameCharacteristicUUID) 116 | ->readValue(); 117 | vTaskDelay(delay); // aparently needed (by the linak controller maybe?) 118 | Serial.println("Name:"); 119 | Serial.println(name.c_str()); 120 | } 121 | 122 | unsigned short BluetoothConnection::getHeightRaw() const { return mOutputChar->readValue(); } 123 | 124 | unsigned short BluetoothConnection::getHeightMm() const { 125 | return (getHeightRaw() + mRawOffset.value_or(0)) / 10; 126 | } 127 | 128 | void BluetoothConnection::startMoveTorwards() const { 129 | writeUInt16(mControlChar, 0xFE); 130 | Serial.println("Control response:"); 131 | printStringAsHex(mControlChar->readValue()); 132 | 133 | writeUInt16(mControlChar, 0xFF); 134 | Serial.println("Control response:"); 135 | printStringAsHex(mControlChar->readValue()); 136 | } 137 | void BluetoothConnection::moveTorwards(unsigned short height) const { 138 | writeUInt16(mInputChar, height); 139 | Serial.println("Input response:"); 140 | printStringAsHex(mInputChar->readValue()); 141 | } 142 | 143 | void BluetoothConnection::attachHeightSpeedCallback(const std::function& callback) const { 144 | sHeightSpeedCallback = callback; 145 | mOutputChar->subscribe(true, adapterCallback, true); 146 | } 147 | 148 | void BluetoothConnection::detachHeightSpeedCallback() const { 149 | sHeightSpeedCallback = {}; 150 | mOutputChar->unsubscribe(true); 151 | } 152 | 153 | void BluetoothConnection::stopMove() const { 154 | writeUInt16(mControlChar, 0xFF); 155 | writeUInt16(mInputChar, 0x8001); 156 | } 157 | 158 | std::string BluetoothConnection::dpgReadCommand(DpgCommand command) { 159 | unsigned char dataToSend[3]{0x7f, static_cast(command), 0x0}; 160 | mDpgChar->writeValue(dataToSend, 3, true); 161 | return mDpgChar->readValue(); 162 | } 163 | 164 | std::string BluetoothConnection::dpgWriteCommand(DpgCommand command, const unsigned char* data, unsigned char length) { 165 | std::vector dataToSend{0x7f, static_cast(command), 0x80, 0x01}; 166 | dataToSend.reserve(4 + length); 167 | std::copy(data, data + length, std::back_inserter(dataToSend)); 168 | mDpgChar->writeValue(dataToSend.data(), dataToSend.size(), true); 169 | return mDpgChar->readValue(); 170 | } 171 | 172 | void BluetoothConnection::loadMemoryPosition(DpgCommand command) { 173 | auto temp = dpgReadCommand(command); 174 | std::optional value; 175 | if (temp[2] == 0x01) { 176 | value = *(unsigned short*)(temp.c_str() + 3); 177 | } 178 | switch (command) { 179 | case DpgCommand::MemoryPosition1: 180 | mMemoryPosition1 = value; 181 | break; 182 | case DpgCommand::MemoryPosition2: 183 | mMemoryPosition2 = value; 184 | break; 185 | case DpgCommand::MemoryPosition3: 186 | mMemoryPosition3 = value; 187 | break; 188 | 189 | default: 190 | Serial.println("loadMemoryPosition called with wrong command!"); 191 | break; 192 | } 193 | } 194 | 195 | void BluetoothConnection::setMemoryPosition(DpgCommand command, unsigned short value) { 196 | uint8_t data[2]; 197 | data[0] = value; 198 | data[1] = value >> 8; 199 | dpgWriteCommand(command, data, 2); 200 | loadMemoryPosition(command); 201 | } 202 | 203 | const std::optional& BluetoothConnection::getMemoryPosition(unsigned char positionNumber) const { 204 | switch (positionNumber) { 205 | case 1: 206 | return mMemoryPosition1; 207 | case 2: 208 | return mMemoryPosition2; 209 | case 3: 210 | return mMemoryPosition3; 211 | 212 | default: 213 | throw std::runtime_error("Bad Memory Position requested!"); 214 | } 215 | } 216 | 217 | bool BluetoothConnection::setMemoryPosition(unsigned char positionNumber, unsigned short value) { 218 | switch (positionNumber) { 219 | case 1: 220 | setMemoryPosition(DpgCommand::MemoryPosition1, value); 221 | loadMemoryPosition(DpgCommand::MemoryPosition1); 222 | return mMemoryPosition1.has_value() && mMemoryPosition1.value() == value; 223 | case 2: 224 | setMemoryPosition(DpgCommand::MemoryPosition2, value); 225 | loadMemoryPosition(DpgCommand::MemoryPosition2); 226 | return mMemoryPosition2.has_value() && mMemoryPosition2.value() == value; 227 | case 3: 228 | setMemoryPosition(DpgCommand::MemoryPosition3, value); 229 | loadMemoryPosition(DpgCommand::MemoryPosition3); 230 | return mMemoryPosition3.has_value() && mMemoryPosition3.value() == value; 231 | 232 | default: 233 | throw std::runtime_error("Bad Memory Position requested!"); 234 | } 235 | } 236 | 237 | const std::optional& BluetoothConnection::getDeskOffset() const { return mRawOffset; } 238 | 239 | } // namespace LinakDesk 240 | -------------------------------------------------------------------------------- /test/test_common/LinakDesk/DeskControllerTest.cpp: -------------------------------------------------------------------------------- 1 | #include "DeskController.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "ConnectionMock.h" 8 | 9 | using namespace testing; 10 | using namespace LinakDesk; 11 | 12 | class DeskControllerTest : public Test { 13 | public: 14 | DeskControllerTest() : DeskControllerTest(std::make_unique>()) {} 15 | 16 | DeskControllerTest(std::unique_ptr> connection) 17 | : mConnectionMock(*connection), mDeskController(std::move(connection)) {} 18 | 19 | void setPrintingCallbackCallsExpecations(const HeightSpeedData& data) { 20 | EXPECT_CALL(*mSerialMock, print(Matcher(StrEq("Notify callback for HeightSpeed: ")))); 21 | EXPECT_CALL(*mSerialMock, print(Matcher(StrEq("Height: ")))); 22 | EXPECT_CALL(*mSerialMock, print(TypedEq(data.getRawHeight()), 10)); 23 | EXPECT_CALL(*mSerialMock, print(Matcher(StrEq(" Speed: ")))); 24 | EXPECT_CALL(*mSerialMock, println(TypedEq(data.getSpeed()), 10)); 25 | } 26 | 27 | StrictMock& mConnectionMock; 28 | DeskController mDeskController; 29 | InSequence mSequence; // ensure the mock calls are in order 30 | 31 | const std::function* mHeightSpeedCallbackPtr; 32 | 33 | // custom deleters to avoid custom destructor 34 | std::unique_ptr> mSerialMock{ 35 | serialMockInstance(), [](SerialMock*) { releaseSerialMock(); }}; // TODO: Change to StrictMock 36 | std::unique_ptr> mArduinoMock{ 37 | arduinoMockInstance(), [](ArduinoMock*) { releaseArduinoMock(); }}; // TODO: Change to StrictMock 38 | }; 39 | 40 | TEST_F(DeskControllerTest, connectingWorksIfNotConnected) { 41 | std::string address = "some:Bt:Address"; 42 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(false)); 43 | EXPECT_CALL(mConnectionMock, connect(address)); 44 | mDeskController.connect(address); 45 | } 46 | TEST_F(DeskControllerTest, connectingDisconnectsFirstIfConnected) { 47 | std::string address = "some:Bt:Address"; 48 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 49 | EXPECT_CALL(mConnectionMock, disconnect()); 50 | EXPECT_CALL(mConnectionMock, connect(address)); 51 | mDeskController.connect(address); 52 | } 53 | 54 | TEST_F(DeskControllerTest, gettingConnectionStatusForwardsCalls) { 55 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 56 | EXPECT_EQ(mDeskController.isConnected(), true); 57 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(false)); 58 | EXPECT_EQ(mDeskController.isConnected(), false); 59 | } 60 | 61 | TEST_F(DeskControllerTest, movingToHeightReturnsFalseIfWereDisconnected) { 62 | unsigned short destinationHeight = 1234u; 63 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(false)); 64 | EXPECT_FALSE(mDeskController.moveToHeight(destinationHeight)); 65 | } 66 | 67 | TEST_F(DeskControllerTest, movingToHeightReturnsTrueIfWereAtPositionAlready) { 68 | unsigned short destinationHeight = 1234u; 69 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 70 | EXPECT_CALL(mConnectionMock, getHeight()).WillOnce(Return(destinationHeight)); 71 | EXPECT_TRUE(mDeskController.moveToHeight(destinationHeight)); 72 | } 73 | 74 | TEST_F(DeskControllerTest, movingToHeightStopsMoveIfPositionDoesntChange) { 75 | unsigned short destinationHeight = 1234u; 76 | unsigned short startHeight = 1234u - 50u; 77 | 78 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 79 | EXPECT_CALL(mConnectionMock, getHeight()).WillOnce(Return(startHeight)); 80 | EXPECT_CALL(mConnectionMock, attachHeightSpeedCallback(_)) 81 | .WillOnce( 82 | [&](const std::function& callback) { mHeightSpeedCallbackPtr = &callback; }); 83 | EXPECT_CALL(mConnectionMock, startMoveTorwards()); 84 | EXPECT_CALL(mConnectionMock, moveTorwards(destinationHeight)); 85 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 86 | mArduinoMock->addMillisRaw(200); 87 | return 300ul; 88 | }); 89 | 90 | mDeskController.moveToHeight(destinationHeight); 91 | 92 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 93 | mArduinoMock->addMillisRaw(200); 94 | return 500ul; 95 | }); 96 | EXPECT_CALL(mConnectionMock, stopMove()); 97 | EXPECT_CALL(mConnectionMock, detachHeightSpeedCallback()); 98 | mDeskController.loop(); 99 | } 100 | 101 | TEST_F(DeskControllerTest, basicMoveToHeightScenarioGoingUp) { 102 | unsigned short destinationHeight = 1234u; 103 | unsigned short startHeight = destinationHeight - 100u; 104 | auto data1 = HeightSpeedData(destinationHeight - 50, 50); 105 | auto data2 = HeightSpeedData(destinationHeight, 0); 106 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 107 | EXPECT_CALL(mConnectionMock, getHeight()).WillOnce(Return(startHeight)); 108 | EXPECT_CALL(mConnectionMock, attachHeightSpeedCallback(_)) 109 | .WillOnce( 110 | [&](const std::function& callback) { mHeightSpeedCallbackPtr = &callback; }); 111 | EXPECT_CALL(mConnectionMock, startMoveTorwards()); 112 | EXPECT_CALL(mConnectionMock, moveTorwards(destinationHeight)).WillOnce([&](unsigned short) { 113 | mHeightSpeedCallbackPtr->operator()(data1); 114 | mHeightSpeedCallbackPtr->operator()(data2); 115 | }); 116 | setPrintingCallbackCallsExpecations(data1); 117 | setPrintingCallbackCallsExpecations(data2); 118 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 119 | mArduinoMock->addMillisRaw(200); 120 | return 300ul; 121 | }); 122 | 123 | mDeskController.moveToHeight(destinationHeight); 124 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 125 | mArduinoMock->addMillisRaw(200); 126 | return 500ul; 127 | }); 128 | EXPECT_CALL(mConnectionMock, stopMove()); 129 | EXPECT_CALL(mConnectionMock, detachHeightSpeedCallback()); 130 | mDeskController.loop(); 131 | } 132 | 133 | TEST_F(DeskControllerTest, basicMoveToHeightScenarioGoingDown) { 134 | unsigned short destinationHeight = 1234u; 135 | unsigned short startHeight = destinationHeight + 100; 136 | auto data1 = HeightSpeedData(destinationHeight + 50, -50); 137 | auto data2 = HeightSpeedData(destinationHeight, 0); 138 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 139 | EXPECT_CALL(mConnectionMock, getHeight()).WillOnce(Return(startHeight)); 140 | EXPECT_CALL(mConnectionMock, attachHeightSpeedCallback(_)) 141 | .WillOnce( 142 | [&](const std::function& callback) { mHeightSpeedCallbackPtr = &callback; }); 143 | EXPECT_CALL(mConnectionMock, startMoveTorwards()); 144 | EXPECT_CALL(mConnectionMock, moveTorwards(destinationHeight)).WillOnce([&](unsigned short) { 145 | mHeightSpeedCallbackPtr->operator()(data1); 146 | mHeightSpeedCallbackPtr->operator()(data2); 147 | }); 148 | setPrintingCallbackCallsExpecations(data1); 149 | setPrintingCallbackCallsExpecations(data2); 150 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 151 | mArduinoMock->addMillisRaw(200); 152 | return 300ul; 153 | }); 154 | 155 | mDeskController.moveToHeight(destinationHeight); 156 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 157 | mArduinoMock->addMillisRaw(200); 158 | return 500ul; 159 | }); 160 | EXPECT_CALL(mConnectionMock, stopMove()); 161 | EXPECT_CALL(mConnectionMock, detachHeightSpeedCallback()); 162 | mDeskController.loop(); 163 | } 164 | 165 | TEST_F(DeskControllerTest, collisionScenarioGoingDown) { 166 | unsigned short destinationHeight = 1234u; 167 | unsigned short startHeight = destinationHeight + 500; 168 | auto data1 = HeightSpeedData(destinationHeight + 400, -50); 169 | auto data2 = HeightSpeedData(destinationHeight + 300, -50); 170 | auto data3 = HeightSpeedData(destinationHeight + 200, -50); 171 | auto data4 = HeightSpeedData(destinationHeight + 300, 50); 172 | auto data5 = HeightSpeedData(destinationHeight + 400, 0); 173 | 174 | EXPECT_CALL(mConnectionMock, isConnected()).WillOnce(Return(true)); 175 | EXPECT_CALL(mConnectionMock, getHeight()).WillOnce(Return(startHeight)); 176 | EXPECT_CALL(mConnectionMock, attachHeightSpeedCallback(_)) 177 | .WillOnce( 178 | [&](const std::function& callback) { mHeightSpeedCallbackPtr = &callback; }); 179 | EXPECT_CALL(mConnectionMock, startMoveTorwards()); 180 | EXPECT_CALL(mConnectionMock, moveTorwards(destinationHeight)).WillOnce([&](unsigned short) { 181 | mHeightSpeedCallbackPtr->operator()(data1); 182 | mHeightSpeedCallbackPtr->operator()(data2); 183 | }); 184 | setPrintingCallbackCallsExpecations(data1); 185 | setPrintingCallbackCallsExpecations(data2); 186 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 187 | mArduinoMock->addMillisRaw(200); 188 | return 300ul; 189 | }); 190 | 191 | mDeskController.moveToHeight(destinationHeight); 192 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 193 | mArduinoMock->addMillisRaw(200); 194 | return 500ul; 195 | }); 196 | 197 | EXPECT_CALL(mConnectionMock, moveTorwards(destinationHeight)).WillOnce([&](unsigned short) { 198 | mHeightSpeedCallbackPtr->operator()(data3); 199 | mHeightSpeedCallbackPtr->operator()(data4); 200 | mHeightSpeedCallbackPtr->operator()(data5); 201 | }); 202 | setPrintingCallbackCallsExpecations(data3); 203 | setPrintingCallbackCallsExpecations(data4); 204 | setPrintingCallbackCallsExpecations(data5); 205 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 206 | mArduinoMock->addMillisRaw(200); 207 | return 700ul; 208 | }); 209 | 210 | mDeskController.loop(); 211 | EXPECT_CALL(*mArduinoMock, millis()).WillOnce([&]() { 212 | mArduinoMock->addMillisRaw(200); 213 | return 900ul; 214 | }); 215 | EXPECT_CALL(mConnectionMock, stopMove()); 216 | EXPECT_CALL(mConnectionMock, detachHeightSpeedCallback()); 217 | mDeskController.loop(); 218 | } 219 | 220 | TEST_F(DeskControllerTest, gettingHeightWorks) { 221 | unsigned short height = 123; 222 | EXPECT_CALL(mConnectionMock, getHeight()).WillOnce(Return(height)); 223 | EXPECT_EQ(mDeskController.getHeight(), height); 224 | } -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "soc/rtc_cntl_reg.h" 2 | #include "soc/soc.h" 3 | #define USE_LITTLEFS true 4 | #define ESP_DRD_USE_LITTLEFS true 5 | #define ESP_DRD_USE_SPIFFS false 6 | #define ESP_DRD_USE_EEPROM false 7 | 8 | #define DRD_TIMEOUT 10 9 | #define DRD_ADDRESS 0 10 | #include 11 | #include 12 | #include //https://github.com/khoih-prog/ESP_DoubleResetDetector 13 | #include //https://github.com/khoih-prog/ESPAsync_WiFiManager 14 | #include 15 | 16 | #include "html.h" 17 | #include 18 | #include 19 | 20 | #define DESK_NAME_MAX_LEN 32 21 | #define DESK_BT_ADDRESS_LEN 18 22 | #define DeskName_Label "DeskName" 23 | #define DeskBtAddress_Label "DeskBtAddress" 24 | #define UserReset_Label "UserReset" 25 | 26 | char deskName[DESK_NAME_MAX_LEN] = "Standing desk"; 27 | char deskBtAddress[DESK_BT_ADDRESS_LEN] = "AA:BB:CC:DD:EE:FF"; 28 | 29 | const char* CONFIG_FILE = "/ConfigSW.json"; 30 | 31 | DoubleResetDetector* drd; 32 | const int PIN_LED = 2; 33 | bool needsConfig = false; 34 | AsyncWebServer server(80); 35 | LinakDesk::DeskController controller = LinakDesk::DeskControllerFactory::make(); 36 | fauxmoESP fauxmo; 37 | Scheduler runner; 38 | 39 | void checkConnection(){ 40 | auto isConnected = controller.isConnected(); 41 | Serial.printf("[checkConnection] (millis: %d), state: %d\n", millis(), isConnected); 42 | if (!isConnected){ 43 | Serial.printf("[checkConnection] Not connected trying to reconnect!\n"); 44 | controller.connect(deskBtAddress); 45 | } 46 | if(!controller.isConnected()){ 47 | Serial.printf("[checkConnection] Reconnect unsuccessful, rebooting!\n"); 48 | ESP.restart(); 49 | } 50 | } 51 | 52 | Task checkConnectionTask(60000, TASK_FOREVER, &checkConnection); 53 | 54 | void moveToHeightHttpHandler(AsyncWebServerRequest* request) { 55 | if (request->hasParam("destination")) { 56 | auto message = request->getParam("destination")->value(); 57 | auto destination = message.toInt(); 58 | if (controller.isConnected() && destination >= 0 && destination < 7000) { 59 | request->send(200, "text/plain", "Moving to: " + message); 60 | controller.moveToHeightRaw(destination); 61 | return; 62 | } 63 | } 64 | request->send(400, "text/plain", "Wrong input"); 65 | } 66 | 67 | void moveToHeightMmHttpHandler(AsyncWebServerRequest* request) { 68 | if (request->hasParam("destination")) { 69 | auto message = request->getParam("destination")->value(); 70 | auto destination = message.toInt(); 71 | if (controller.isConnected() && destination >= 0 && destination < 2000) { 72 | request->send(200, "text/plain", "Moving to: " + message); 73 | controller.moveToHeightMm(destination); 74 | return; 75 | } 76 | } 77 | request->send(400, "text/plain", "Wrong input"); 78 | } 79 | 80 | void saveCurrentHeightAsFavHttpHandler(AsyncWebServerRequest* request) { 81 | if (request->hasParam("position")) { 82 | auto message = request->getParam("position")->value(); 83 | auto position = message.toInt(); 84 | if (controller.isConnected() && position >= 0 && position < 4) { 85 | request->send(200, "text/plain", "Saving current height to position number: " + message); 86 | controller.setMemoryPositionFromCurrentHeight(position); 87 | return; 88 | } 89 | } 90 | request->send(400, "text/plain", "Wrong input"); 91 | } 92 | 93 | void notFound(AsyncWebServerRequest* request) { request->send(404, "text/plain", "Not found"); } 94 | 95 | bool writeConfigFile(bool userReset = false) { 96 | Serial.println("Saving config file"); 97 | 98 | DynamicJsonDocument json(1024); 99 | 100 | // JSONify local configuration parameters 101 | json[DeskName_Label] = deskName; 102 | json[DeskBtAddress_Label] = deskBtAddress; 103 | json[UserReset_Label] = userReset; 104 | 105 | // Open file for writing 106 | File f = FileFS.open(CONFIG_FILE, "w"); 107 | 108 | if (!f) { 109 | Serial.println("Failed to open config file for writing"); 110 | return false; 111 | } 112 | 113 | serializeJsonPretty(json, Serial); 114 | // Write data to file and close it 115 | serializeJson(json, f); 116 | 117 | f.close(); 118 | 119 | Serial.println("\nConfig file was successfully saved"); 120 | return true; 121 | } 122 | 123 | bool readConfigFile() { 124 | // this opens the config file in read-mode 125 | File f = FileFS.open(CONFIG_FILE, "r"); 126 | 127 | if (!f) { 128 | Serial.println("Configuration file not found"); 129 | return false; 130 | } else { 131 | // we could open the file 132 | size_t size = f.size(); 133 | // Allocate a buffer to store contents of the file. 134 | std::unique_ptr buf(new char[size + 1]); 135 | 136 | // Read and store file contents in buf 137 | f.readBytes(buf.get(), size); 138 | // Closing file 139 | f.close(); 140 | // Using dynamic JSON buffer which is not the recommended memory model, but anyway 141 | // See https://github.com/bblanchon/ArduinoJson/wiki/Memory%20model 142 | 143 | DynamicJsonDocument json(1024); 144 | auto deserializeError = deserializeJson(json, buf.get()); 145 | if (deserializeError) { 146 | Serial.println("JSON parseObject() failed"); 147 | return false; 148 | } 149 | serializeJson(json, Serial); 150 | 151 | // Parse all config file parameters, override 152 | // local config variables with parsed values 153 | if (json.containsKey(DeskName_Label)) { 154 | strcpy(deskName, json[DeskName_Label]); 155 | } 156 | if (json.containsKey(DeskBtAddress_Label)) { 157 | strcpy(deskBtAddress, json[DeskBtAddress_Label]); 158 | } 159 | if (json.containsKey(UserReset_Label)) { 160 | if (json[UserReset_Label]) { 161 | needsConfig = true; 162 | } 163 | } 164 | } 165 | Serial.println("\nConfig file was successfully parsed"); 166 | return true; 167 | } 168 | 169 | void initFS() { 170 | // Format FileFS if not yet 171 | if (!FileFS.begin(true)) { 172 | Serial.print(FS_Name); 173 | Serial.println(F(" failed! AutoFormatting.")); 174 | } 175 | } 176 | 177 | void wifiManagerSetup() { 178 | if (!readConfigFile()) { 179 | Serial.println(F("Failed to read ConfigFile, using default values")); 180 | needsConfig = true; 181 | } 182 | 183 | drd = new DoubleResetDetector(DRD_TIMEOUT, DRD_ADDRESS); 184 | if (drd->detectDoubleReset()) { 185 | Serial.println(F("Detected Double Reset")); 186 | needsConfig = true; 187 | } 188 | // Resources for ConfigPortal that won't be used for normal operation 189 | const char customhtml[] PROGMEM = "pattern=\"([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\""; 190 | ESPAsync_WMParameter p_deskName(DeskName_Label, "Desk Name", deskName, DESK_NAME_MAX_LEN); 191 | ESPAsync_WMParameter p_deskBtAddress(DeskBtAddress_Label, "Desk BT address", deskBtAddress, DESK_BT_ADDRESS_LEN, 192 | customhtml); 193 | DNSServer dnsServer; 194 | ESPAsync_WiFiManager ESPAsync_wifiManager(&server, &dnsServer, "LinakDeskEsp32Controller"); 195 | 196 | ESPAsync_wifiManager.addParameter(&p_deskName); 197 | ESPAsync_wifiManager.addParameter(&p_deskBtAddress); 198 | 199 | if (ESPAsync_wifiManager.WiFi_SSID() == "") { 200 | Serial.println(F("No AP credentials")); 201 | needsConfig = true; 202 | } 203 | if (needsConfig) { 204 | Serial.println(F("Starting Config Portal")); 205 | digitalWrite(PIN_LED, HIGH); 206 | if (!ESPAsync_wifiManager.startConfigPortal()) { 207 | Serial.println(F("Not connected to WiFi")); 208 | } else { 209 | Serial.println(F("connected")); 210 | } 211 | 212 | strcpy(deskName, p_deskName.getValue()); 213 | strcpy(deskBtAddress, p_deskBtAddress.getValue()); 214 | writeConfigFile(); 215 | digitalWrite(PIN_LED, LOW); 216 | } else { 217 | WiFi.mode(WIFI_STA); 218 | WiFi.begin(); 219 | } 220 | } 221 | 222 | void setupWebServer() { 223 | // respond to GET requests on URL /heap 224 | server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { 225 | request->send_P(200, "text/html", html::index_html, html::processor); 226 | }); 227 | server.on("/cn", HTTP_GET, [](AsyncWebServerRequest* request) { 228 | writeConfigFile(true); 229 | ESP.restart(); 230 | }); 231 | server.on("/heap", HTTP_GET, 232 | [](AsyncWebServerRequest* request) { request->send(200, "text/plain", String(ESP.getFreeHeap())); }); 233 | server.on("/moveToHeight", HTTP_GET, moveToHeightHttpHandler); 234 | server.on("/moveToHeightMm", HTTP_GET, moveToHeightMmHttpHandler); 235 | server.on("/saveCurrentPosAsFav", HTTP_GET, saveCurrentHeightAsFavHttpHandler); 236 | server.on("/getHeight", HTTP_GET, [](AsyncWebServerRequest* request) { 237 | request->send(200, "text/plain", String(controller.getHeightRaw()).c_str()); 238 | }); 239 | server.on("/getHeightMm", HTTP_GET, [](AsyncWebServerRequest* request) { 240 | request->send(200, "text/plain", String(controller.getHeightMm()).c_str()); 241 | }); 242 | // These two callbacks are required for echo gen1 and gen3 compatibility 243 | server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { 244 | if (fauxmo.process(request->client(), request->method() == HTTP_GET, request->url(), String((char *)data))) return; 245 | // Handle any other body request here... 246 | }); 247 | server.onNotFound([](AsyncWebServerRequest *request) { 248 | String body = (request->hasParam("body", true)) ? request->getParam("body", true)->value() : String(); 249 | if (fauxmo.process(request->client(), request->method() == HTTP_GET, request->url(), body)) return; 250 | return notFound(request); 251 | }); 252 | auto strin = std::string(deskName); 253 | std::replace(strin.begin(), strin.end(), ' ', '-'); 254 | MDNS.begin(strin.c_str()); 255 | MDNS.addService("http", "tcp", 80); 256 | server.begin(); 257 | Serial.println("HTTP server started"); 258 | } 259 | 260 | void setupFauxmo(){ 261 | // Set fauxmoESP to not create an internal TCP server and redirect requests to the server on the defined port 262 | // The TCP port must be 80 for gen3 devices (default is 1901) 263 | // This has to be done before the call to enable() 264 | fauxmo.createServer(false); 265 | fauxmo.setPort(80); // This is required for gen3 devices 266 | 267 | // You have to call enable(true) once you have a WiFi connection 268 | // You can enable or disable the library at any moment 269 | // Disabling it will prevent the devices from being discovered and switched 270 | fauxmo.enable(true); 271 | 272 | // You can use different ways to invoke alexa to modify the devices state: 273 | // "Alexa, turn kitchen on" ("kitchen" is the name of the first device below) 274 | // "Alexa, turn on kitchen" 275 | // "Alexa, set kitchen to fifty" (50 means 50% of brightness) 276 | 277 | // Add virtual devices 278 | fauxmo.addDevice(deskName); 279 | 280 | fauxmo.onSetState([](unsigned char device_id, const char * device_name, bool state, unsigned char value) { 281 | 282 | Serial.printf("[MAIN] Device #%d (%s) state: %s value: %d\n", device_id, device_name, state ? "ON" : "OFF", value); 283 | 284 | if (strcmp(device_name, deskName) == 0) 285 | { 286 | if (controller.getMemoryPosition(1).has_value() && controller.getMemoryPosition(3).has_value()) { 287 | 288 | unsigned char destinationPosition = state ? 3 : 1; 289 | controller.moveToHeightRaw(controller.getMemoryPosition(destinationPosition).value()); 290 | } 291 | /* code */ 292 | } 293 | 294 | }); 295 | } 296 | 297 | void setup() { 298 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector 299 | Serial.begin(115200); 300 | while (!Serial) { 301 | }; 302 | delay(200); 303 | 304 | pinMode(PIN_LED, OUTPUT); 305 | digitalWrite(PIN_LED, LOW); 306 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); // enable brownout detector 307 | 308 | initFS(); 309 | runner.init(); 310 | runner.addTask(checkConnectionTask); 311 | 312 | wifiManagerSetup(); 313 | 314 | if (controller.connect(deskBtAddress)) { 315 | auto before = millis(); 316 | Serial.printf("Current height: %d mm\n", controller.getHeightMm()); 317 | Serial.printf("Getting height and printing it took: %ldms\n", millis() - before); 318 | } 319 | 320 | unsigned long startedAt = millis(); 321 | Serial.print(F("After waiting ")); 322 | int connRes = WiFi.waitForConnectResult(); 323 | float waited = (millis() - startedAt); 324 | Serial.print(waited / 1000); 325 | Serial.print(F(" secs , WiFi connection result is ")); 326 | Serial.println(connRes); 327 | if (WiFi.status() != WL_CONNECTED) { 328 | Serial.println(F("Failed to connect")); 329 | drd->stop(); 330 | delay(10000); 331 | ESP.restart(); 332 | } else { 333 | Serial.print(F("Local IP: ")); 334 | Serial.println(WiFi.localIP()); 335 | setupWebServer(); 336 | setupFauxmo(); 337 | } 338 | checkConnectionTask.enableDelayed(60000); 339 | } 340 | void loop() { 341 | delay(1); 342 | drd->loop(); 343 | controller.loop(); 344 | fauxmo.handle(); 345 | runner.execute(); 346 | } 347 | --------------------------------------------------------------------------------