├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── arduino.json ├── c_cpp_properties.json └── settings.json ├── README.md ├── WiFiConfig.h.sample ├── alzaergo.ino ├── azure-pipelines-1.yml ├── azure-pipelines.yml ├── docs └── control_panel_manual.pdf ├── images ├── control_box.png ├── control_box_pins.png ├── control_panel.png ├── control_panel_pinout.png ├── rj45.png ├── status_flow.png ├── status_flow_annotated.png ├── up_command.png └── up_command_annotated.png ├── notes.txt ├── src ├── AlzaET1Ng.cpp └── AlzaET1Ng.h └── tests ├── AlzaEt1Ng_unittest.cpp ├── CMakeLists.txt └── arduino_mock └── CMakeLists.txt /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is the name of the workflow, visible on GitHub UI. 2 | name: Main build 3 | 4 | on: [push, pull_request] 5 | 6 | # This is the list of jobs that will be run concurrently. 7 | # Since we use a build matrix, the actual number of jobs 8 | # started depends on how many configurations the matrix 9 | # will produce. 10 | jobs: 11 | build-job: 12 | strategy: 13 | matrix: 14 | arduino-board-fqbn: 15 | - esp32:esp32:lolin32 16 | include: 17 | - arduino-boards-fqbn: esp32:esp32:lolin32 18 | platform-url: https://dl.espressif.com/dl/package_esp32_index.json 19 | arduino-platform: "esp32:esp32" 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@main 26 | 27 | - name: Setup Arduino CLI 28 | uses: arduino/setup-arduino-cli@v1.1.1 29 | 30 | # We then install the platform, which one will be determined 31 | # dynamically by the build matrix. 32 | - name: Install platform 33 | run: | 34 | arduino-cli core --additional-urls ${{ matrix.platform-url }} update-index 35 | arduino-cli core --additional-urls ${{ matrix.platform-url }} install ${{ matrix.arduino-platform }} 36 | arduino-cli lib install u8g2 37 | # apt-get install make g++ 38 | 39 | # Finally, we compile the sketch, using the FQBN that was set 40 | # in the build matrix. 41 | - name: Compile Sketch 42 | run: | 43 | cp WiFiConfig.h.sample WiFiConfig.h 44 | arduino-cli compile --log-level info --fqbn ${{ matrix.arduino-board-fqbn }} ./alzaergo 45 | 46 | - name: Install test dependencies 47 | run: | 48 | sudo apt-get install make g++ cmake lcov 49 | 50 | - name: Build tests 51 | run: | 52 | mkdir build && cd build 53 | cmake ../tests 54 | make 55 | cd .. 56 | 57 | - name: Run tests 58 | run: build/test-all 59 | 60 | - name: Collect Coverage Data 61 | run: | 62 | cd build 63 | lcov --capture --directory . --include '*AlzaET1Ng.cpp' --output-file coverage.info 64 | genhtml coverage.info --output-directory coverage_data 65 | 66 | - name: Archive coverage data 67 | uses: actions/upload-artifact@v2 68 | with: 69 | name: code-coverage-report 70 | path: build/coverage_data -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | WifiConfig.h -------------------------------------------------------------------------------- /.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "board": "espressif:esp32:lolin32", 3 | "sketch": "alzaergo.ino", 4 | "programmer": "AVR ISP", 5 | "port": "COM13", 6 | "configuration": "FlashFreq=40,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600" 7 | } -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "/home/mmrazik/arduino-1.8.13/tools/**", 7 | "/home/mmrazik/Arduino/libraries/**", 8 | "/home/mmrazik/arduino-1.8.13/libraries/**", 9 | "/home/mmrazik/arduino-1.8.13/hardware/tools/**", 10 | "/home/mmrazik/arduino-1.8.13/hardware/arduino/avr/**", 11 | "/home/mmrazik/arduino-1.8.12/hardware/arduino/avr/**", 12 | "/home/mmrazik/arduino-1.8.12/hardware/tools/**", 13 | "/home/mmrazik/arduino-1.8.12/libraries/**", 14 | "/home/mmrazik/arduino-1.8.12/tools/**", 15 | ], 16 | "forcedInclude": [ 17 | "/home/mmrazik/arduino-1.8.12/hardware/arduino/avr/cores/arduino/Arduino.h", 18 | ], 19 | "defines": [ 20 | "USBCON" 21 | ], 22 | "intelliSenseMode": "linux-gcc-x64", 23 | "compilerPath": "/usr/bin/gcc", 24 | "cStandard": "gnu17", 25 | "cppStandard": "gnu++14" 26 | }, 27 | { 28 | "name": "Win32", 29 | "includePath": [ 30 | "${workspaceFolder}\\src\\**", 31 | "C:\\Users\\martinmrazik\\OneDrive - Microsoft\\Documents\\Arduino\\hardware\\espressif\\esp32\\**", 32 | "C:\\Program Files (x86)\\Arduino\\tools\\**", 33 | "C:\\Users\\martinmrazik\\OneDrive - Microsoft\\Documents\\Arduino\\libraries\\**", 34 | "C:\\Program Files (x86)\\Arduino\\libraries\\**", 35 | "C:\\Program Files (x86)\\Arduino\\hardware\\tools\\**", 36 | "C:\\Program Files (x86)\\Arduino\\hardware\\arduino\\avr\\**" 37 | ], 38 | "forcedInclude": [], 39 | "defines": [ 40 | "USBCON" 41 | ] 42 | } 43 | ], 44 | "version": 4 45 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "array": "cpp", 4 | "hash_map": "cpp", 5 | "deque": "cpp", 6 | "list": "cpp", 7 | "string": "cpp", 8 | "unordered_map": "cpp", 9 | "vector": "cpp", 10 | "string_view": "cpp", 11 | "initializer_list": "cpp", 12 | "ranges": "cpp", 13 | "mutex": "cpp", 14 | "shared_mutex": "cpp" 15 | } 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AlzaErgo Table ET1 NewGen 2 | This is my attempt to reverse engineer [AlzaErgo Table ET1 NewGen](https://www.alza.cz/alzaergo-table-et1-newgen-white-d5647311.htm) standing desk and add wi-fi functionality using something like Wemos D1/ESP8266. 3 | 4 | The work is in progress. 5 | 6 | # Components 7 | ### Control Panel 8 | Controls height of the desk, show the current height on display, has memory for 3 positions, you can set min/max height, etc. 9 | 10 | 11 | 12 | ### Control Box 13 | Contains the PowerSupply Unit, controls the motors and, gets commands from the Control Panel. 14 | 15 | 16 | 17 | ### Connection 18 | The control box has a RJ-45 port. It seems like all 8 pins are being used but the provided Control Panel is only using 5 of them and does not wire the rest at all. 19 | 20 | 21 | 22 | The wiring of the RJ45 port is as follows: 23 | 24 | ``` 25 | 1. D-TX (green wire) 26 | 2. D-RX (brown wire; control panel transmits here) 27 | 3. unused 28 | 4. KEY-1 (white wire) 29 | 5. G (red wire) 30 | 6. unused 31 | 7. unused 32 | 8. +5V (yellow wire) 33 | ``` 34 | 35 |
36 | 37 | 38 | Control Panel from the back:
39 | 40 | 41 | ## Protocol 42 | The control flow is relatively straightforward and it is an endless loop of request (from control panel) and response (from control box) over 43 | serial line. The serial line operates at 9600 bauds (8bit per frame, 1 stop bit, no parity bit, least significant bit first). 44 | 45 | The request always starts with `0xA5` and uses the following structure:
46 | ``0xA5 [byte1] [byte2] 0x01 [checksum]`` 47 | 48 | The checksum is calculated as (where `&` is bitwise `AND`):
49 | ```(byte1 + byte2 + 0x01) & 0xff``` 50 | 51 | The response always starts with `0x5A` and uses the following structure:
52 | ```0x5A [byte1] [byte2] [byte3] [byte4] [checksum]``` 53 | 54 | Checksum in this case is ```(byte1 + byte2 + byte3 + byte4]) & 0xff```. The 3 bytes in response are the 3 characters displayed on the control panel and an 55 | optional decimal point. 56 | 57 | In addition to the serial communication there is also another channel (`Key`) which indicates a pressed button. 58 | 59 | Here are two examples of communication between the panel and board 60 | 61 | ### Status (no action) 62 | If there is no button pressed the `Key` pin is `LOW`, Control panel sends `0xA5 0x00 0x00 0x01 0x01` ("status") and the Control box replies with what to show on the built-in display (more on that later): 63 | ![Status flow](images/status_flow_annotated.png) 64 | 65 | 66 | Here is another example while pressing the `UP` button on the control panel. In this case the `Key` pin is `HIGH`, control panel sends `0xA5 0x00 0x20 0x01 0x21` and Control box replies with what to show on the built-in display: 67 | ![UP Command](images/up_command_annotated.png) 68 | 69 | 70 | ### Control panel commands (requests) 71 | 72 | Panel sends one of the following commands: 73 | ``` 74 | 0xA5 0x00 0x00 0x01 0x01 Idle/Get current display status 75 | 0xA5 0x00 0x20 0x01 0x21 Move up 76 | 0xA5 0x00 0x40 0x01 0x41 Move down 77 | 0xA5 0x00 0x60 0x01 0x61 UP and Down (used to reset) 78 | 0xA5 0x00 0x01 0x01 0x02 M button 79 | 0xA5 0x00 0x02 0x01 0x03 memory 1 80 | 0xA5 0x00 0x04 0x01 0x05 memory 2 81 | 0xA5 0x00 0x08 0x01 0x09 memory 3 82 | 0xA5 0x00 0x10 0x01 0x11 T button 83 | 0xA5 0x00 0x11 0x01 0x12 M+T (to get into settings) 84 | ``` 85 | 86 | 87 | ### Control Box replies 88 | The control box reply always follow this structure: `0x5A [byte1] [byte2] [byte3] [byte4] [checksum]` and bytes 1 to 3 represent three digits/letters on 7 segment [display](https://en.wikipedia.org/wiki/Seven-segment_display). Each bit in the byte corresponds to one segment. For example 8 is represented by `0b01111111` (or `0b11111111`; the topmost bit is not relevant): 89 | ``` 90 | __0_ 91 | | | 92 | 5 | | 1 93 | |__6_| 94 | | | 95 | 4 | | 2 96 | |____| 97 | 3 98 | ``` 99 | 100 | Digit 6 can be represented as 0b01111101 (or 0b11111101) 101 | ``` 102 | __0_ 103 | | 104 | 5 | 105 | |__6_ 106 | | | 107 | 4 | | 2 108 | |____| 109 | 3 110 | ``` 111 | 112 | The top most bit of the middle byte is signaling the decimal point. 113 | `0x06 0xbf 0x06` translates to `10.1` on display while `0x06 0x3f 0x06` translates to `101` on the display. 114 | 115 | Here is a list of digits (the later always represent the digit and a decimal point) but the control box sends more letters than just this one. 116 | | Digit | Hex | binary | 117 | |--- | --- | --- | 118 | | 0 | `0x3f` or `0xbf` | `0b00111111` or `0b10111111` | 119 | | 1 | `0x06` or `0x86` | `0b00000110` or `0b10000110` | 120 | | 2 | `0x5b` or `0xdb` | `0b01011011` or `0b11011011` | 121 | | 3 | `0x4f` or `0xcf` | `0b01001111` or `0b11001111` | 122 | | 4 | `0x66` or `0xe6` | `0b01100110` or `0b11100110` | 123 | | 5 | `0x6d` or `0xed` | `0b01101101` or `0b11101101` | 124 | | 6 | `0x7d` or `0xfd` | `0b01111101` or `0b11111101` | 125 | | 7 | `0x07` or `0x87` | `0b00000111` or `0b10000111` | 126 | | 8 | `0x7f` or `0xff` | `0b01111111` or `0b11111111` | 127 | | 9 | `0x6f` or `0xef` | `0b01101111` or `0b11101111` | 128 | 129 | The byte sequence of `5A 07 FD 6D 10 81 ` therefore corresponds to table height `76.5` cm (byte `07` corresponds to `7`, byte `FD` corresponds to `6.` and byte `6d` corresponds to `5`). 130 | 131 | `Byte4` can have the following values: 132 | | Value | Description | 133 | |--- |--- | 134 | | `0x00` | Everything on the display is completely off 135 | | `0x01` | Timer indicator is turned on 136 | | `0x10` | 7 segment display is on (3 characters + decimal point) 137 | | `0x11` | 7 segment display is on as well as the timer indicator 138 | 139 | 140 | ### Timing 141 | The request/reply over serial goes in a very quick succession (around 7ms per command or reply) and it seems like the control board is sensitive on the count of commands. E.g. I was unable to unlock the control panel from sleep (by holding `M` for 3 seconds) if the delay between commands was around `250ms`. Similarly the desk goes up/down slower if the delay between commands is longer (despite the fact the `Key` pin is still `HIGH`). 142 | 143 | 144 | ### Standby 145 | After 60 seconds of inactivity the control board starts sending `0x50 0x40 0x40 0x40 0x10 0xD0` which stands for Safety Standby Mode ("---" on the display). This mode can be also activated by holding down the `M` button for 3 seconds. 146 | 147 | After another 9 minutes (10 minutes of inactivity in total) the control board starts sending `0x5A 0x00 0x00 0x00 0x00 0x00` which is a signal to completely shutdown the display of control panel. This can be alternatively the sequence of `0x5A 0x00 0x00 0x00 0x01 0x01` which keeps the timer indicator led on. 148 | 149 | After 18 minutes of inactivity the board sends `0x5A 0xFF 0xFF 0xFF 0x00 0xFD` sequence which is a signal to control panel to stop sending the "status" sequence. From this point the traffic on serial line stops until the board is woken up again (by pushing any of the buttons). -------------------------------------------------------------------------------- /WiFiConfig.h.sample: -------------------------------------------------------------------------------- 1 | const char* ssid = "REPLACE_WITH_YOUR_SSID"; 2 | const char* password = "REPLACE_WITH_YOUR_PASSWORD"; -------------------------------------------------------------------------------- /alzaergo.ino: -------------------------------------------------------------------------------- 1 | //#include 2 | //#include 3 | #include 4 | #define ALZAET1NG_THREADSAFE 5 | #include "src/AlzaET1Ng.h" 6 | #include 7 | #include "WiFiConfig.h" 8 | #include "esp_http_server.h" 9 | 10 | 11 | 12 | 13 | 14 | #define KEY_PIN 22 15 | const int key_pins[] = {27, 14, 12, 13}; 16 | #define KEY_UP 0 17 | #define KEY_DOWN 1 18 | #define KEY_M 2 19 | 20 | TaskHandle_t AlzaTask; 21 | AlzaET1Ng::ControlPanel AlzaControl(Serial2, KEY_PIN); 22 | httpd_handle_t httpServer; 23 | 24 | //U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); 25 | U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 5, /* dc=*/ 32, /* reset=*/ 33); 26 | 27 | esp_err_t get_height_handler(httpd_req_t *req) 28 | { 29 | httpd_resp_send(req, String(AlzaControl.getHeight()).c_str(), HTTPD_RESP_USE_STRLEN); 30 | return ESP_OK; 31 | } 32 | 33 | httpd_uri_t uri_get_height = { 34 | .uri = "/height", 35 | .method = HTTP_GET, 36 | .handler = get_height_handler, 37 | .user_ctx = NULL 38 | }; 39 | 40 | 41 | httpd_handle_t startWebServer(void) 42 | { 43 | httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 44 | httpd_handle_t server = NULL; 45 | 46 | if (httpd_start(&server, &config) == ESP_OK) { 47 | httpd_register_uri_handler(server, &uri_get_height); 48 | } 49 | return server; 50 | } 51 | 52 | void stop_webserver(httpd_handle_t server) 53 | { 54 | if (server) { 55 | httpd_stop(server); 56 | } 57 | } 58 | 59 | void AlzaMainLoop( void * parameter) { 60 | for(;;) { 61 | AlzaControl.handleLoop(); 62 | } 63 | } 64 | 65 | void ConnectToWiFi() 66 | { 67 | WiFi.mode(WIFI_STA); 68 | WiFi.begin(ssid, password); 69 | Serial.print("Connecting to "); Serial.println(ssid); 70 | 71 | uint8_t i = 0; 72 | while (WiFi.status() != WL_CONNECTED) 73 | { 74 | Serial.print('.'); 75 | delay(500); 76 | 77 | if ((++i % 16) == 0) 78 | { 79 | Serial.println(F(" still trying to connect")); 80 | } 81 | } 82 | 83 | Serial.print(F("Connected. My IP address is: ")); 84 | Serial.println(WiFi.localIP()); 85 | } 86 | 87 | void setup() 88 | { 89 | u8g2.begin(); 90 | 91 | 92 | Serial.begin(115200); 93 | Serial.write("Hello world"); 94 | for(int x=0; x<4; x++) 95 | { 96 | pinMode(key_pins[x], INPUT); 97 | } 98 | 99 | u8g2.clearBuffer(); 100 | u8g2.setFont(u8g2_font_ncenB08_tr); 101 | u8g2.drawStr(15,10,"Hello World!"); 102 | u8g2.sendBuffer(); 103 | 104 | xTaskCreatePinnedToCore( 105 | AlzaMainLoop, 106 | "AlzaTask", 107 | 10000, /* Stack size in words */ 108 | NULL, /* Task input parameter */ 109 | 0, /* Priority of the task */ 110 | &AlzaTask, 111 | 0); /* Core where the task should run */ 112 | 113 | ConnectToWiFi(); 114 | httpServer = startWebServer(); 115 | 116 | } 117 | 118 | void writeln(String ln) { 119 | u8g2.clearBuffer(); 120 | u8g2.setCursor(0,15); 121 | u8g2.print(ln); 122 | u8g2.sendBuffer(); 123 | } 124 | 125 | int up, down, memory; 126 | unsigned long last_refresh = 0; 127 | char displayData[] = {0, 0, 0, 0, 0}; 128 | 129 | 130 | 131 | void loop() 132 | { 133 | 134 | up = digitalRead(key_pins[KEY_UP]); 135 | down = digitalRead(key_pins[KEY_DOWN]); 136 | memory = digitalRead(key_pins[KEY_M]); 137 | 138 | if (up == HIGH) { 139 | AlzaControl.holdCommand(AlzaET1Ng::Commands::Up); 140 | } else if (down == HIGH) { 141 | AlzaControl.holdCommand(AlzaET1Ng::Commands::Down); 142 | } else if (memory == HIGH) { 143 | AlzaControl.holdCommand(AlzaET1Ng::Commands::M); 144 | } else { 145 | AlzaControl.holdCommand(AlzaET1Ng::Commands::Status); 146 | } 147 | 148 | u8g2.clearBuffer(); 149 | u8g2.setFont(u8g2_font_ncenB14_tr); 150 | AlzaControl.getBcdDisplayAsString(displayData); 151 | u8g2.drawStr(35, 40, displayData); 152 | u8g2.setFont(u8g2_font_helvR08_te); 153 | u8g2.drawStr(10, 10, WiFi.localIP().toString().c_str()); 154 | u8g2.sendBuffer(); 155 | } -------------------------------------------------------------------------------- /azure-pipelines-1.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - main 8 | 9 | 10 | pool: 11 | vmImage: ubuntu-latest 12 | 13 | steps: 14 | - script: echo Hello, world! 15 | displayName: 'Run a one-line script' 16 | 17 | - script: | 18 | echo Add other tasks to build, test, and deploy your project. 19 | echo See https://aka.ms/yaml 20 | - task: DotNetCoreCLI@2 21 | inputs: 22 | command: 'build' 23 | displayName: 'Run a multi-line script' 24 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - main 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | steps: 13 | - script: echo Hello, world! 14 | displayName: 'Run a one-line script' 15 | 16 | - script: | 17 | echo Add other tasks to build, test, and deploy your project. 18 | echo See https://aka.ms/yaml 19 | displayName: 'Run a multi-line script' 20 | -------------------------------------------------------------------------------- /docs/control_panel_manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/docs/control_panel_manual.pdf -------------------------------------------------------------------------------- /images/control_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/control_box.png -------------------------------------------------------------------------------- /images/control_box_pins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/control_box_pins.png -------------------------------------------------------------------------------- /images/control_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/control_panel.png -------------------------------------------------------------------------------- /images/control_panel_pinout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/control_panel_pinout.png -------------------------------------------------------------------------------- /images/rj45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/rj45.png -------------------------------------------------------------------------------- /images/status_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/status_flow.png -------------------------------------------------------------------------------- /images/status_flow_annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/status_flow_annotated.png -------------------------------------------------------------------------------- /images/up_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/up_command.png -------------------------------------------------------------------------------- /images/up_command_annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmrazik/alzaergo/e88ef6d8f8407e09b1c57f5394f42abd6f04d830/images/up_command_annotated.png -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | Board only: 2 | Seem to send nothing at all 3 | After pluggin in control 4 | A0 5 1 1 41 41 69 1 1 1 41 41 4F 65 5B 3F 10 B0 5A 6 5B 3F 10 B0 5 | 38 5A 0 0 0 10 10 5A 0 0 0 10 10 5A 6 5B 3F 10 B0 6 | 7 | Err 4 blinking for a while 79 3F 66 == E04 8 | 9 | 5A 6 5B 3F 10 B0 10 | 5A 79 3F 66 10 2E E04 11 | 5A 79 3F 66 10 2E 12 | 5A 79 3F 66 10 2E 13 | 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 5A 79 3F 66 10 2E 14 | 15 | 16 | 17 | Panel only: 18 | F8 FF C0 8 A5 0 0 1 1 19 | 2 F1 A5 0 0 1 1 20 | 18 A0 1A 1 5 5 4A 0 0 1 1 A5 0 0 1 1 21 | 22 | 23 | 24 | 79 3F 66 25 | 26 | 27 | 28 | Board: 29 | 5A 7D .. 3F 10 .. 30 | 31 | 32 | 62cm: DB .. .. A7 33 | 65cm: ED .. .. B9 34 | 35 | 36 | s: CF .. .. 9B 37 | l: CF .. .. 9B 38 | 39 | ---------------- 40 | 62cm to 63 cm 41 | 6th - 4th = 0x68 42 | 43 | 5A 7D DB 6 10 6E (62.x cm) 44 | .. 45 | 5A 7D DB 5B 10 C3 46 | .. 47 | 5A 7D DB 4F 10 B7 48 | .. 49 | 5A 7D DB 66 10 CE 50 | .. 51 | 5A 7D DB 6D 10 D5 52 | .. 53 | 5A 7D DB 7D 10 E5 54 | .. 55 | 5A 7D DB 7F 10 E7 56 | .. 57 | 5A 7D DB 6F 10 D7 58 | .. 59 | 5A 7D CF 3F 10 9B (many times; 63.0) 60 | ------------------- 61 | 62 | ----------------- 63 | 63 to 87cm 64 | 5A 7D CF 6 10 62 (63.x cm) 65 | 5A 7D CF 5B 10 B7 66 | 5A 7D CF 4F 10 AB 67 | 5A 7D CF 66 10 C2 68 | 5A 7D CF 6D 10 C9 69 | 5A 7D CF 7D 10 D9 70 | 5A 7D CF 7 10 63 71 | 5A 7D CF 7F 10 DB 72 | 5A 7D CF 7F 10 10 81 73 | 5A 7D ED 7 10 81 (many times/; probably at 64.x cm) 74 | 57 D5 F5 6D 10 (many times) 75 | ... probably noise 76 | 5A 7D FF 3F 10 CB 77 | 5A 7D FF 6 10 92 78 | 79 | 5A 7D FF 5B 10 E7 80 | 5A 7D FF 4F 10 DB 81 | 5A 7D FF 66 10 52 82 | D5 FD 66 10 52 83 | D5 FD 6D 10 49 84 | D5 FD 6D 10 49 85 | D5 FD 7D 10 9 86 | 87 | 5A 7D FF 7D 10 9 88 | 5A 7D FF 7 10 93 89 | 90 | 5A 7D FF 7F 10 B 91 | 5A 7D FF 6F 10 4B 92 | D5 FD 6F 10 4B 93 | 94 | 95 | 96 | end: 97 | 98 | 5A 7F DB 66 10 D0 99 | 100 | 5A 7F DB 6D 10 D7 101 | 102 | 5A 7F DB 7D 10 E7 103 | 104 | 5A 7F 87 6 10 1C (87.1cm) 105 | 106 | -------------------- 107 | 108 | 87.1cm to 90.xcm 109 | 5A 7F FF 7D 10 B 110 | 5A 7F EF 7 10 85 111 | 5A 7F EF 6F 10 ED 112 | 113 | 5A 6F BF 3F 10 7D (?90.0) 114 | 5A 6F BF 6 10 44 (?90.1) 115 | 5A 6F BF 5B 10 99 (90.2cm) 116 | 117 | 118 | 119 | --------- 120 | 90.2 to 128 121 | stops at 100, 111, 128cm 122 | 123 | 5A 6 6 7D 10 99 124 | 5A 6 5B 6 10 77 125 | 5A 6 5B 5B 10 CC 126 | 5A 6 5B 4F 10 C0 (128 cm) 127 | 128 | 129 | 7D -> 7 -> 7F -> 6F -> 6 130 | 131 | DB -> CF -> E6 -> ED -> FD -> 132 | 133 | 134 | ---------------------------- 135 | 5A 7D DB 3F 10 A7 62.0 cm 136 | 5A 7D CF 3F 10 9B 63.0 cm 137 | 5A 7D E6 3F 10 B2 64.0 cm 138 | 5A 7D ED 3F 10 B9 65.0 cm 139 | 5A 7D FD 3F 10 C9 66.0 cm 140 | 5A 7D 87 3F 10 53 67.0 cm 141 | 5A 7D FF 3F 10 CB 68.0 cm 142 | 5A 7D EF 3F 10 BB 69.0 cm 143 | 5A 07 BF 3F 10 15 70.0 cm 144 | 5A 07 86 3F 10 DC 71.0 cm 145 | 5A 07 DB 3F 10 31 72.0 cm 146 | 5A 07 CF 3F 10 25 73.0 cm 147 | 5A 07 E6 3F 10 3C 74.0 cm 148 | 5A 07 E6 5B 10 58 74.2 cm 149 | 5A 07 E6 6D 10 6A 74.5 cm 150 | 5A 07 E6 6F 10 6C 74.9 cm 151 | 5A 07 ED 3F 10 43 75.0 cm 152 | 5A 07 ED 4F 10 53 75.3 cm 153 | 5A 07 ED 6D 10 71 75.5 cm 154 | 5A 07 ED 6F 10 73 75.9 cm 155 | 5A 07 FD 3F 10 53 76.0 cm 156 | 5A 07 FD 06 10 1A 76.1 cm 157 | 5A 07 FD 5B 10 6F 76.2 cm 158 | 5A 07 FD 4F 10 63 76.3 cm 159 | 5A 07 FD 66 10 7A 76.4 cm 160 | 5A 07 FD 6D 10 81 76.5 cm 161 | 5A 07 FD 7D 10 91 76.6 cm 162 | 5A 07 FD 07 10 1B 76.7 cm 163 | 5A 07 FD 7F 10 93 76.8 cm 164 | 5A 07 FD 6F 10 83 76.9 cm 165 | 5A 07 87 3F 10 DD 77.0 cm 166 | 5A 07 EF 6F 10 75 79.9 cm 5A 07 EF 6F 10 .. 79.9 cm 167 | 5A 7F BF 3F 10 8D 80.0 cm 168 | 5A 7F DB 3F 10 A9 82.0 cm 169 | 5A 6F BF 3F 10 7D 90.0 cm 170 | 5A 6F EF 6F 10 DD 99.9 cm 171 | 5A 06 3F 3F 10 94 100 cm 172 | 5A 06 3F 5B 10 B0 102 cm 173 | 5A 06 06 3F 10 5B 110 cm 174 | 5A 06 5B 3F 10 B0 120 cm 175 | 176 | _ 177 | |_| 178 | |_| 179 | 180 | 181 | 182 | 183 | 0b01000100 184 | 0b01010011 185 | 0b00010111 186 | -------------------------------------------------------------------------------- /src/AlzaET1Ng.cpp: -------------------------------------------------------------------------------- 1 | #include "AlzaET1Ng.h" 2 | #include "Arduino.h" 3 | using namespace AlzaET1Ng; 4 | 5 | const char AlzaET1Ng::bcdDigitToString(int bcd_code) 6 | { 7 | bcd_code = bcd_code & 0x7f; // we don't care about the top-most bit 8 | switch (bcd_code) { 9 | case 0x3f: 10 | return '0'; 11 | case 0x06: 12 | return '1'; 13 | case 0x5b: 14 | return '2'; 15 | case 0x4f: 16 | return '3'; 17 | case 0x66: 18 | return '4'; 19 | case 0x6d: 20 | return '5'; 21 | case 0x7d: 22 | return '6'; 23 | case 0x07: 24 | return '7'; 25 | case 0x7f: 26 | return '8'; 27 | case 0x6f: 28 | return '9'; 29 | case 0x40: 30 | return '-'; 31 | case 0x79: 32 | return 'E'; 33 | case 0x50: 34 | return 'r'; 35 | case 0x31: 36 | return 'T'; 37 | case 0x76: 38 | return 'H'; 39 | case 0x5c: 40 | return 'o'; 41 | case 0x78: 42 | return 't'; 43 | case 0x00: 44 | return ' '; 45 | } 46 | return '?'; 47 | } 48 | 49 | 50 | ControlPanel::ControlPanel(HardwareSerial &hws, int key): 51 | serial(hws) { 52 | pinMode(key, OUTPUT); 53 | digitalWrite(key, LOW); 54 | keyPin = key; 55 | #ifdef ALZAET1NG_THREADSAFE 56 | mutex = xSemaphoreCreateMutex(); 57 | if (mutex == NULL) { 58 | // do something here 59 | } 60 | #endif 61 | 62 | //serial.begin(9600); 63 | serial.begin(9600, SERIAL_8N1, 16, 17); 64 | nextCommand = Commands::Status; 65 | } 66 | 67 | 68 | bool ControlPanel::isValidResponse(int response[]) { 69 | unsigned int checksum = 0; 70 | for (int i=2; i < RESPONSE_SIZE; i++) { 71 | checksum = (checksum + response[i]) & 0xff; 72 | } 73 | return checksum == response[RESPONSE_SIZE]; 74 | } 75 | 76 | 77 | void ControlPanel::sendCommand(Commands cmd) { 78 | if (waitForResponse) { 79 | return; 80 | } 81 | // Command is 5 bytes: 82 | // [command_header] 0x0 [command] 0x01 [checksum] 83 | // e.g. 0xa5, 0x00, 0x20, 0x01, 0x21 84 | serial.write((uint8_t) COMMAND_HEADER); 85 | serial.write((uint8_t) 0x0); 86 | serial.write((uint8_t) cmd); 87 | serial.write((uint8_t) 0x01); 88 | serial.write((uint8_t) cmd + 1); // this is potentially wrong but all the known commands are smaller than 254 so this will give the rigth result 89 | 90 | lastCommandExecution = millis(); 91 | waitForResponse = true; 92 | } 93 | 94 | 95 | int ControlPanel::bcdToHeight() { 96 | // height is unfortunately unitless depending on the configuration of the control box 97 | // (holding T button for 8 seconds switches between inches and cm). This method 98 | // just converts whatever is displayed to an integer in the given unit 99 | 100 | // TODO: fix this for inches. If the display shows 72.5, we return 725 from here which is milimeters 101 | // when the table operates in inches and the display shows 32.4 we return 324 which is a unit that does 102 | // not make much sense 103 | int new_height = 0; 104 | 105 | // validate the display; it might show an error or something similar 106 | int i = 0; 107 | while (displayStatusString[i]) { 108 | // if we have some non-digit character it is probably error or some other status message 109 | if (!((displayStatusString[i] >= '0' && displayStatusString[i] <= '9') || (displayStatusString[i] == '.'))) { 110 | return 0; 111 | } 112 | i = i + 1; 113 | } 114 | 115 | new_height = (displayStatusString[0] - '0'); 116 | new_height = new_height * 10 + (displayStatusString[1] - '0'); 117 | if (displayStatusString[2] == '.') { 118 | new_height = new_height * 10 + (displayStatusString[3] - '0'); 119 | } else { 120 | new_height = new_height * 10 + (displayStatusString[2] - '0'); 121 | new_height = new_height * 10; 122 | } 123 | 124 | return new_height; 125 | } 126 | 127 | 128 | void ControlPanel::updateRepresentations() { 129 | #ifdef ALZAET1NG_THREADSAFE 130 | xSemaphoreTake(mutex, portMAX_DELAY); 131 | #endif 132 | 133 | // raw (7 segment display) 134 | displayStatus[0] = responseBuffer[2]; 135 | displayStatus[1] = responseBuffer[3]; 136 | displayStatus[2] = responseBuffer[4]; 137 | 138 | // 7 segment display to ASCII 139 | int position = 0; 140 | displayStatusString[position++] = bcdDigitToString(displayStatus[0]); 141 | displayStatusString[position++] = bcdDigitToString(displayStatus[1]); 142 | 143 | // If the middle character is '-' and the first one is '5', we are in the settings 144 | // The 7 segment display shows '5' and 'S' as the same character but they are not 145 | // the same. Be a bit smarter here and change the '5' to 'S' 146 | if ((displayStatusString[1] == '-') && (displayStatusString[0] == '5')) { 147 | displayStatusString[0] = 'S'; 148 | } 149 | 150 | // if the middle digit has top most bit set to 1, it means we have decimal point 151 | if ((displayStatus[1] & 0b10000000) == 0b10000000) { 152 | displayStatusString[position++] = '.'; 153 | } 154 | displayStatusString[position++] = bcdDigitToString(displayStatus[2]); 155 | displayStatusString[position] = 0; 156 | 157 | int new_height = bcdToHeight(); 158 | if (new_height != 0) { 159 | height = new_height; 160 | } 161 | #ifdef ALZAET1NG_THREADSAFE 162 | xSemaphoreGive(mutex); 163 | #endif 164 | } 165 | 166 | void ControlPanel::handleIncomingResponse() { 167 | if (serial.available()) { 168 | int c = serial.read(); 169 | if (c == RESPONSE_HEADER) { 170 | responseBuffer[0] = 1; 171 | responseBuffer[1] = c; 172 | } else if ((responseBuffer[0] >= 1) && (responseBuffer[0] < RESPONSE_SIZE)) { 173 | responseBuffer[++responseBuffer[0]] = c; 174 | } else { 175 | waitForResponse = false; 176 | } 177 | if (responseBuffer[0] == RESPONSE_SIZE) { 178 | waitForResponse = false; 179 | if (isValidResponse(responseBuffer)) { 180 | updateRepresentations(); 181 | #ifdef ALZAET1NG_THREADSAFE 182 | // This is just an assumption that when we are running in the THREADSAFE mode 183 | // we run on a dedicated core/task. In that case we need to call delay every now and then 184 | // otherwise the watchdog timer will be triggered and will kill the task 185 | 186 | // TODO: try to figure out if there is a way to just feed the watchdog and not do a 1ms delay 187 | // ~0.5ms delay might be sill desirable so we do not send next command immediately after the receive. 188 | // The originial control panel seem to be sending a command every ~7ms (which is approcimately 189 | // 1 command transmittion time + something on top) 190 | delay(1); 191 | #endif 192 | } // else invalid response -- nothing we can do here; ignore and let the controller send the nextCommand 193 | } 194 | } 195 | } 196 | 197 | 198 | void ControlPanel::detectStaleHeight() { 199 | 200 | } 201 | 202 | void ControlPanel::handleLoop() { 203 | #ifdef DEBUG 204 | if ((millis() - lastUpdate) > 50) { 205 | lastUpdate = millis(); 206 | Serial.write(String(lastUpdate).c_str()); 207 | Serial.write("\r\n"); 208 | } 209 | #endif 210 | sendCommand(nextCommand); 211 | handleIncomingResponse(); 212 | detectStaleHeight(); 213 | evaluateTargetHeight(); 214 | 215 | 216 | // if we are in some weird state, still waiting for a response but nothing comes in, stop waiting for response 217 | if ((millis() - lastCommandExecution) > WAIT_FOR_RESPONSE_TIMEOUT) { 218 | waitForResponse = false; 219 | } 220 | } 221 | 222 | 223 | void ControlPanel::holdCommand(Commands cmd) { 224 | if (cmd == Commands::Status) { 225 | digitalWrite(keyPin, LOW); 226 | } else { 227 | digitalWrite(keyPin, HIGH); 228 | } 229 | nextCommand = cmd; 230 | } 231 | 232 | 233 | void ControlPanel::getBcdDisplay(int *data) { 234 | #ifdef ALZAET1NG_THREADSAFE 235 | xSemaphoreTake(mutex, portMAX_DELAY); 236 | #endif 237 | data[0] = displayStatus[0]; 238 | data[1] = displayStatus[1]; 239 | data[2] = displayStatus[2]; 240 | #ifdef ALZAET1NG_THREADSAFE 241 | xSemaphoreGive(mutex); 242 | #endif 243 | } 244 | 245 | 246 | void ControlPanel::getBcdDisplayAsString(char *data) { 247 | #ifdef ALZAET1NG_THREADSAFE 248 | xSemaphoreTake(mutex, portMAX_DELAY); 249 | #endif 250 | data[0] = displayStatusString[0]; 251 | data[1] = displayStatusString[1]; 252 | data[2] = displayStatusString[2]; 253 | data[3] = displayStatusString[3]; 254 | #ifdef ALZAET1NG_THREADSAFE 255 | xSemaphoreGive(mutex); 256 | #endif 257 | } 258 | 259 | 260 | int ControlPanel::getHeight() { 261 | #ifdef ALZAET1NG_THREADSAFE 262 | xSemaphoreTake(mutex, portMAX_DELAY); 263 | #endif 264 | return height; 265 | #ifdef ALZAET1NG_THREADSAFE 266 | xSemaphoreGive(mutex); 267 | #endif 268 | } 269 | 270 | 271 | void ControlPanel::evaluateTargetHeight() { 272 | if (targetHeight != 0) { 273 | if ((nextCommand == Commands::Up) && (height >= targetHeight)) { 274 | holdCommand(Commands::Status); 275 | } else 276 | if ((nextCommand == Commands::Down) && (height <= targetHeight)) { 277 | holdCommand(Commands::Status); 278 | } 279 | } 280 | } 281 | 282 | 283 | void ControlPanel::setHeight(int newHeight) { 284 | targetHeight = newHeight; 285 | if (newHeight > height) { 286 | holdCommand(Commands::Up); 287 | } else 288 | if (newHeight < height) { 289 | holdCommand(Commands::Down); 290 | } 291 | } 292 | 293 | Commands ControlPanel::getNextCommand() { 294 | return nextCommand; 295 | } 296 | 297 | bool ControlPanel::waitingForResponse() { 298 | return waitForResponse; 299 | } 300 | -------------------------------------------------------------------------------- /src/AlzaET1Ng.h: -------------------------------------------------------------------------------- 1 | #ifndef AlzaET1Ng_h 2 | #define AlzaET1Ng_h 3 | #include "Arduino.h" 4 | 5 | #ifdef ALZAET1NG_THREADSAFE 6 | #include "freertos/semphr.h" 7 | #endif 8 | 9 | 10 | 11 | namespace AlzaET1Ng 12 | { 13 | const char bcdDigitToString(int bcd_code); 14 | 15 | static const int COMMAND_SIZE = 5; 16 | static const int RESPONSE_SIZE = 6; 17 | static const int RESPONSE_HEADER = 0x5A; 18 | static const int COMMAND_HEADER = 0xA5; 19 | static const unsigned long WAIT_FOR_RESPONSE_TIMEOUT = 200; 20 | enum Commands { 21 | Status = 0x00, 22 | Up = 0x20, 23 | Down = 0x40, 24 | M = 0x01, 25 | M1 = 0x02, 26 | M2 = 0x04, 27 | M3 = 0x08, 28 | T = 0x10, 29 | Reset = 0x60 30 | }; 31 | 32 | class ControlPanel 33 | { 34 | private: 35 | int displayStatus[3] = {0, 0, 0}; 36 | char displayStatusString[5] = {0, 0, 0, 0, 0}; 37 | int height = 0; 38 | #ifdef ALZAET1NG_THREADSAFE 39 | SemaphoreHandle_t mutex; 40 | #endif 41 | 42 | HardwareSerial &serial; 43 | int keyPin; 44 | 45 | Commands nextCommand; 46 | int targetHeight = 0; 47 | int oldHeight = 0; 48 | bool waitForResponse = false; 49 | unsigned long lastCommandExecution = 0; 50 | int responseBuffer[RESPONSE_SIZE + 1] = {0, 0, 0, 0, 0, 0, 0}; 51 | 52 | #ifdef DEBUG 53 | unsigned long lastUpdated = 0; 54 | #endif 55 | 56 | bool isValidResponse(int response[]); 57 | void updateRepresentations(); 58 | int bcdToHeight(); 59 | void evaluateTargetHeight(); 60 | void detectStaleHeight(); 61 | 62 | public: 63 | // TODO: Add a constructor for NeoSWSerial/SoftwareSerial so this can be used with SW serial too 64 | ControlPanel(HardwareSerial &srl, int key); 65 | void sendCommand(Commands cmd); 66 | void setHeight(int newHeight); 67 | void handleLoop(); 68 | void holdCommand(Commands cmd); 69 | void getBcdDisplay(int *data); 70 | void getBcdDisplayAsString(char *data); 71 | int getHeight(); 72 | 73 | // public to make testing easier 74 | void handleIncomingResponse(); 75 | 76 | // mostly for testing 77 | Commands getNextCommand(); 78 | bool waitingForResponse(); 79 | }; 80 | }; 81 | #endif -------------------------------------------------------------------------------- /tests/AlzaEt1Ng_unittest.cpp: -------------------------------------------------------------------------------- 1 | #include "gtest/gtest.h" 2 | #include 3 | #include "gmock/gmock-matchers.h" 4 | #include "arduino-mock/Arduino.h" 5 | #include "arduino-mock/Serial.h" 6 | 7 | 8 | 9 | typedef Serial_ HardwareSerial; 10 | 11 | #include "../src/AlzaET1Ng.h" 12 | 13 | using ::testing::Return; 14 | #define KEY_PIN 22 15 | 16 | class BasicTestFixture : public ::testing::Test { 17 | protected: 18 | ArduinoMock *arduinoMock; 19 | SerialMock *serialMock; 20 | 21 | virtual void SetUp() { 22 | arduinoMock = arduinoMockInstance(); 23 | serialMock = serialMockInstance(); 24 | EXPECT_CALL(*arduinoMock, pinMode(KEY_PIN, OUTPUT)); 25 | EXPECT_CALL(*arduinoMock, digitalWrite(KEY_PIN, 0)); 26 | EXPECT_CALL(*serialMock, begin(9600)); 27 | } 28 | 29 | virtual void TearDown() { 30 | releaseSerialMock(); 31 | releaseArduinoMock(); 32 | } 33 | }; 34 | 35 | class TestFixtureWithSerialData : public BasicTestFixture { 36 | protected: 37 | void SetSerialMessage(const int *response, int size) { 38 | EXPECT_CALL(*serialMock, available()) 39 | .Times(size) 40 | .WillRepeatedly(Return(true)); 41 | 42 | EXPECT_CALL(*serialMock, read()) 43 | .WillOnce(Return(response[0])) 44 | .WillOnce(Return(response[1])) 45 | .WillOnce(Return(response[2])) 46 | .WillOnce(Return(response[3])) 47 | .WillOnce(Return(response[4])) 48 | .WillOnce(Return(response[5])); 49 | 50 | } 51 | }; 52 | 53 | TEST_F(BasicTestFixture, holdCommand) { 54 | EXPECT_CALL(*arduinoMock, digitalWrite(KEY_PIN, 1)); 55 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 56 | AlzaControl.holdCommand(AlzaET1Ng::Commands::Down); 57 | ASSERT_EQ(AlzaControl.getNextCommand(), AlzaET1Ng::Commands::Down); 58 | } 59 | 60 | TEST_F(BasicTestFixture, holdCommandStatus) { 61 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 62 | EXPECT_CALL(*arduinoMock, digitalWrite(KEY_PIN, 0)); 63 | AlzaControl.holdCommand(AlzaET1Ng::Commands::Status); 64 | ASSERT_EQ(AlzaControl.getNextCommand(), AlzaET1Ng::Commands::Status); 65 | } 66 | 67 | TEST_F(BasicTestFixture, waitsForResponseAfterStartup) { 68 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 69 | EXPECT_CALL(*serialMock, write(testing::An())).Times(AlzaET1Ng::COMMAND_SIZE); 70 | EXPECT_CALL(*arduinoMock, millis()).Times(2); 71 | EXPECT_CALL(*serialMock, available()).WillOnce(Return(false)); 72 | AlzaControl.handleLoop(); 73 | ASSERT_EQ(AlzaControl.waitingForResponse(), true); 74 | } 75 | 76 | 77 | TEST_F(BasicTestFixture, waitForResponseTimesOut) { 78 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 79 | EXPECT_CALL(*serialMock, write(testing::An())).Times(AlzaET1Ng::COMMAND_SIZE); 80 | EXPECT_CALL(*arduinoMock, millis()) 81 | .WillOnce(Return(0)) 82 | .WillOnce(Return(0)); 83 | EXPECT_CALL(*serialMock, available()).WillOnce(Return(false)); 84 | AlzaControl.handleLoop(); 85 | ASSERT_EQ(AlzaControl.waitingForResponse(), true); 86 | EXPECT_CALL(*arduinoMock, millis()).WillOnce(Return(AlzaET1Ng::WAIT_FOR_RESPONSE_TIMEOUT + 1)); 87 | EXPECT_CALL(*serialMock, available()).WillOnce(Return(false)); 88 | AlzaControl.handleLoop(); 89 | ASSERT_EQ(AlzaControl.waitingForResponse(), false); 90 | } 91 | 92 | 93 | TEST_F(TestFixtureWithSerialData, handleIncomingResponse) { 94 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 95 | // Corresponds to 76.5 96 | int message[] = {AlzaET1Ng::RESPONSE_HEADER, 0x7, 0xfd, 0x6d, 0x10, 0x81}; 97 | SetSerialMessage(message, AlzaET1Ng::RESPONSE_SIZE); 98 | for (int i=0; i < AlzaET1Ng::RESPONSE_SIZE; i++) { 99 | AlzaControl.handleIncomingResponse(); 100 | } 101 | int data[3]; 102 | AlzaControl.getBcdDisplay(data); 103 | ASSERT_EQ(data[0], 0x7); 104 | ASSERT_EQ(data[1], 0xfd); 105 | ASSERT_EQ(data[2], 0x6d); 106 | 107 | ASSERT_EQ(AlzaControl.getHeight(), 765); 108 | 109 | char str_data[4]; 110 | AlzaControl.getBcdDisplayAsString(str_data); 111 | ASSERT_EQ(str_data[0], '7'); 112 | ASSERT_EQ(str_data[1], '6'); 113 | ASSERT_EQ(str_data[2], '.'); 114 | ASSERT_EQ(str_data[3], '5'); 115 | } 116 | 117 | 118 | TEST_F(TestFixtureWithSerialData, handleIncomingResponseWithWrongChecksum) { 119 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 120 | // Corresponds to 76.5 121 | int message[] = {AlzaET1Ng::RESPONSE_HEADER, 0x7, 0xfd, 0x6d, 0x10, 0x82}; 122 | SetSerialMessage(message, AlzaET1Ng::RESPONSE_SIZE); 123 | for (int i=0; i < AlzaET1Ng::RESPONSE_SIZE; i++) { 124 | AlzaControl.handleIncomingResponse(); 125 | } 126 | int data[3]; 127 | AlzaControl.getBcdDisplay(data); 128 | ASSERT_EQ(data[0], 0x0); 129 | ASSERT_EQ(data[1], 0x0); 130 | ASSERT_EQ(data[2], 0x0); 131 | } 132 | 133 | 134 | TEST_F(TestFixtureWithSerialData, testSettingsString) { 135 | AlzaET1Ng::ControlPanel AlzaControl(Serial, KEY_PIN); 136 | // Corresponds to 76.5 137 | int message[] = {AlzaET1Ng::RESPONSE_HEADER, 0x6d, 0x40, 0x5b, 0x10, 0x18}; 138 | SetSerialMessage(message, AlzaET1Ng::RESPONSE_SIZE); 139 | for (int i=0; i < AlzaET1Ng::RESPONSE_SIZE; i++) { 140 | AlzaControl.handleIncomingResponse(); 141 | } 142 | 143 | ASSERT_EQ(AlzaControl.getHeight(), 0); 144 | 145 | char str_data[4]; 146 | AlzaControl.getBcdDisplayAsString(str_data); 147 | ASSERT_EQ(str_data[0], 'S'); 148 | ASSERT_EQ(str_data[1], '-'); 149 | ASSERT_EQ(str_data[2], '2'); 150 | } 151 | 152 | int main(int argc, char* argv[]) { 153 | ::testing::InitGoogleTest(&argc, argv); 154 | return RUN_ALL_TESTS(); 155 | } -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8) 2 | project(alzaergo-et-ng-tests) 3 | find_package(Threads REQUIRED) 4 | 5 | SET(CMAKE_CXX_FLAGS "-g -O0 --coverage -fprofile-arcs -ftest-coverage") 6 | 7 | add_subdirectory(arduino_mock) 8 | 9 | include_directories( 10 | ${ARDUINO_MOCK_INCLUDE_DIRS} 11 | ${ARDUINO_MOCK_LIBS_DIR}/_deps/googletest-src/googletest/include 12 | ${ARDUINO_MOCK_LIBS_DIR}/_deps/googletest-src/googlemock/include 13 | ) 14 | 15 | message("libs_dir: ${ARDUINO_MOCK_LIBS_DIR}") 16 | 17 | file(GLOB LIB_SRCS "../src/*.cpp") 18 | file(GLOB SRCS "*.cpp") 19 | add_executable(test-all ${SRCS} ${LIB_SRCS}) 20 | 21 | 22 | target_link_libraries(test-all 23 | ${ARDUINO_MOCK_LIBS_DIR}/lib/libgtest.a 24 | ${ARDUINO_MOCK_LIBS_DIR}/lib/libgmock.a 25 | ${ARDUINO_MOCK_LIBS_DIR}/dist/lib/libarduino_mock.a 26 | ${CMAKE_THREAD_LIBS_INIT} 27 | ) 28 | 29 | add_dependencies(test-all arduino_mock) 30 | 31 | enable_testing() 32 | add_test(TestAll test-all) 33 | -------------------------------------------------------------------------------- /tests/arduino_mock/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8) 2 | project(arduino_mock_builder C CXX) 3 | include(ExternalProject) 4 | 5 | ExternalProject_Add(arduino_mock 6 | GIT_REPOSITORY https://github.com/mmrazik/arduino-mock.git 7 | GIT_TAG hardware-serial 8 | PREFIX ${CMAKE_CURRENT_BINARY_DIR}/arduino_mock 9 | INSTALL_COMMAND "" 10 | ) 11 | 12 | ExternalProject_Get_Property(arduino_mock source_dir) 13 | set(ARDUINO_MOCK_INCLUDE_DIRS ${source_dir}/include PARENT_SCOPE) 14 | 15 | ExternalProject_Get_Property(arduino_mock binary_dir) 16 | set(ARDUINO_MOCK_LIBS_DIR ${binary_dir} PARENT_SCOPE) --------------------------------------------------------------------------------