diff --git a/.gitignore b/.gitignore index fa3d1d3..749a664 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,9 @@ # Grammax Parser.h -Parser_debug.txt \ No newline at end of file +Parser_debug.txt + +# Folders +ui +# terminal +terminal/node_modules diff --git a/README.md b/README.md index cec1d8b..eb4df46 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # ZeroLamp + ZeroBone's ESP32-based smart lamp project. + +## 📦 Dependencies / Arduino Libraries + +**ZeroLamp** requires the following Arduino libraries, compatible with **ESP32-S3**. +All libraries can be installed via the **Arduino Library Manager**. + +| Library | Purpose | Author | Install via Library Manager | +|-----------------|-----------------------------------------------|----------------------|-------------------------------------| +| FastLED | Control addressable RGB LEDs (WS2812B matrix) | Daniel Garcia et al. | Search `FastLED` in Library Manager | +| WiFi | Connect ESP32-S3 to Wi-Fi networks | Espressif Systems | Built-in for ESP32 in Arduino core | +| ESPmDNS | Device name discovery over network (mDNS) | Espressif Systems | Built-in for ESP32 in Arduino core | +| BluetoothSerial | Bluetooth communication (ESP32-S3) | Espressif Systems | Built-in for ESP32 in Arduino core | + +**Notes:** + +- `TOUCH_BUTTON_PIN` (TTP223) uses standard `digitalRead()`, no extra library required. +- Make sure your **Arduino IDE** is updated and ESP32-S3 board support is installed via **Boards Manager**. + +## 🚀 Quick Start + +1. Clone the repository +2. Open `zerolamp.ino` in Arduino IDE +3. Set `WLAN_SSID` and `WLAN_PASSWORD` in `wlan.h` +4. Upload to ESP32-S3 board +5. Press `TOUCH_BUTTON_PIN` to wake / sleep diff --git a/terminal/terminal.js b/terminal/terminal.js new file mode 100644 index 0000000..bda7bc0 --- /dev/null +++ b/terminal/terminal.js @@ -0,0 +1,65 @@ +// siakinnik - terminal.js +const WebSocket = require('ws'); +const readline = require('readline'); + +const SERVER_URL = 'ws://zerolamp.local:81'; +let clientAuthorized = false; + +// readline +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: '> ' +}); + +let ws; + +const connect = () => { + ws = new WebSocket(SERVER_URL); + ws.on('open', () => { + console.log('Connected. Waiting for code request...'); + }); + + ws.on('message', (data) => { + const msg = data.toString().trim(); + console.log('\n<<', msg); + + if (!clientAuthorized && msg.includes('Enter connection code:')) { + rl.question('Enter code: ', (code) => { + ws.send(code.trim()); + }); + } else if (!clientAuthorized && msg.includes('Authorized!')) { + clientAuthorized = true; + console.log('Authorization success!'); + rl.prompt(); + } else if (!clientAuthorized && msg.includes('Wrong code')) { + console.log('Wrong code, try again!'); + ws.close(); + setTimeout(connect, 1000); + } + }); + + ws.on('close', () => { + clientAuthorized = false; + console.log('\nConnection closed. Reconnecting in 3 seconds...'); + setTimeout(connect, 3000); + }); + + ws.on('error', (err) => { + console.error('Connection error:', err.message); + setTimeout(connect, 3000); + }); +}; + +rl.on('line', (line) => { + if (!clientAuthorized) { + console.log('Authorization first!'); + rl.prompt(); + return; + } + const command = line.trim().replace(/[\r\n]+$/, '').replace(/;$/, ''); + ws.send(command); + rl.prompt(); +}); + +connect(); \ No newline at end of file diff --git a/third_party/lib/LICENSES/LICENSE_ArduinoJWT.txt b/third_party/lib/LICENSES/LICENSE_ArduinoJWT.txt new file mode 100644 index 0000000..6ed2475 --- /dev/null +++ b/third_party/lib/LICENSES/LICENSE_ArduinoJWT.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Chris Moorhouse +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/zerolamp/JWT.cpp b/zerolamp/JWT.cpp new file mode 100644 index 0000000..72689bd --- /dev/null +++ b/zerolamp/JWT.cpp @@ -0,0 +1,166 @@ +/** + + Copyright (c) 2016, Interior Automation Ltd. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be + used to endorse or promote products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + **/ + +#include +#include "JWT.h" +#include "base64.hpp" +#include "sha256.h" + +// The standard JWT header already base64 encoded. Equates to {"alg": "HS256", "typ": "JWT"} +const PROGMEM char* jwtHeader = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + +JWT::JWT(String psk) { + _psk = psk; +} + +JWT::JWT(char* psk) { + _psk = String(psk); +} + +void JWT::setPSK(String psk) { + _psk = psk; +} +void JWT::setPSK(char* psk) { + _psk = String(psk); +} + +int JWT::getJWTLength(String& payload) { + return getJWTLength((char*)payload.c_str()); +} + +int JWT::getJWTLength(char* payload) { + return strlen(jwtHeader) + encode_base64_length(strlen(payload)) + encode_base64_length(32) + 2; +} + +int JWT::getJWTPayloadLength(String& jwt) { + return getJWTPayloadLength((char*)jwt.c_str()); +} + +int JWT::getJWTPayloadLength(char* jwt) { + char jwtCopy[strlen(jwt)]; + memcpy((char*)jwtCopy, jwt, strlen(jwt)); + // Get all three jwt parts + const char* sep = "."; + char* token; + token = strtok(jwtCopy, sep); + token = strtok(NULL, sep); + if(token == NULL) { + return -1; + } else { + return decode_base64_length((unsigned char*)token) + 1; + } +} + +String JWT::encodeJWT(String& payload) { + char jwt[getJWTLength(payload)]; + encodeJWT((char*)payload.c_str(), (char*)jwt); + return String(jwt); +} + +void JWT::encodeJWT(char* payload, char* jwt) { + unsigned char* ptr = (unsigned char*)jwt; + // Build the initial part of the jwt (header.payload) + memcpy(ptr, jwtHeader, strlen(jwtHeader)); + ptr += strlen(jwtHeader); + *ptr++ = '.'; + encode_base64((unsigned char*)payload, strlen(payload), ptr); + ptr += encode_base64_length(strlen(payload)); + // Get rid of any padding (trailing '=' added when base64 encoding) + while(*(ptr - 1) == '=') { + ptr--; + } + *(ptr) = 0; + // Build the signature + Sha256.initHmac((const unsigned char*)_psk.c_str(), _psk.length()); + Sha256.print(jwt); + // Add the signature to the jwt + *ptr++ = '.'; + encode_base64(Sha256.resultHmac(), 32, ptr); + ptr += encode_base64_length(32); + // Get rid of any padding and replace / and + + while(*(ptr - 1) == '=') { + ptr--; + } + *(ptr) = 0; +} + +bool JWT::decodeJWT(String& jwt, String& payload) { + int payloadLength = getJWTPayloadLength(jwt); + if(payloadLength > 0) { + char jsonPayload[payloadLength]; + if(decodeJWT((char*)jwt.c_str(), (char*)jsonPayload, payloadLength)) { + payload = String(jsonPayload); + return true; + } + } + return false; +} + +bool JWT::decodeJWT(char* jwt, char* payload, int payloadLength) { + // Get all three jwt parts + const char* sep = "."; + char* encodedHeader = strtok(jwt, sep); + char* encodedPayload = strtok(NULL, sep); + char* encodedSignature = strtok(NULL, sep); + + // Check all three jwt parts exist + if(encodedHeader == NULL || encodedPayload == NULL || encodedSignature == NULL) + { + payload = NULL; + return false; + } + + // Build the signature + Sha256.initHmac((const unsigned char*)_psk.c_str(), _psk.length()); + Sha256.print(encodedHeader); + Sha256.print("."); + Sha256.print(encodedPayload); + + // Encode the signature as base64 + unsigned char base64Signature[encode_base64_length(32)]; + encode_base64(Sha256.resultHmac(), 32, base64Signature); + unsigned char* ptr = &base64Signature[0] + encode_base64_length(32); + // Get rid of any padding and replace / and + + while(*(ptr - 1) == '=') { + ptr--; + } + *(ptr) = 0; + + // Do the signatures match? + if(strcmp((char*)encodedSignature, (char*)base64Signature) == 0) { + // Decode the payload + decode_base64((unsigned char*)encodedPayload, (unsigned char*)payload); + payload[payloadLength - 1] = 0; + return true; + } else { + payload = NULL; + return false; + } +} diff --git a/zerolamp/JWT.h b/zerolamp/JWT.h new file mode 100644 index 0000000..8fb14db --- /dev/null +++ b/zerolamp/JWT.h @@ -0,0 +1,64 @@ +/** + + Copyright (c) 2016, Interior Automation Ltd. + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be + used to endorse or promote products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + **/ + +#ifndef ARDUINO_JWT_H +#define ARDUINO_JWT_H + +#include + + + +class JWT { +private: + String _psk; + +public: + JWT(String psk); + JWT(char* psk); + + // Set a new psk for encoding and decoding JWTs + void setPSK(String psk); + void setPSK(char* psk); + + // Get the calculated length of a JWT + int getJWTLength(String& payload); + int getJWTLength(char* payload); + // Get the length of the decoded payload from a JWT + int getJWTPayloadLength(String& jwt); + int getJWTPayloadLength(char* jwt); + // Create a JSON Web Token + String encodeJWT(String& payload); + void encodeJWT(char* payload, char* jwt); + // Decode a JWT and retreive the payload + bool decodeJWT(String& jwt, String& payload); + bool decodeJWT(char* jwt, char* payload, int payloadLength); +}; + +#endif diff --git a/zerolamp/base64.hpp b/zerolamp/base64.hpp new file mode 100644 index 0000000..edf09fa --- /dev/null +++ b/zerolamp/base64.hpp @@ -0,0 +1,201 @@ +/** + * Based on original work by Densaugeo + * Original library located at https://github.com/Densaugeo/base64_arduino + */ + +/** + * Base64 encoding and decoding of strings. Uses '+' for 62, '\' for 63, '=' for padding + * This has been modified to use '-' for 62, '_' for 63 as per the JWT specification + */ + +#ifndef BASE64_H_INCLUDED +#define BASE64_H_INCLUDED + +/* binary_to_base64: + * Description: + * Converts a single byte from a binary value to the corresponding base64 character + * Parameters: + * v - Byte to convert + * Returns: + * ascii code of base64 character. If byte is >= 64, then there is not corresponding base64 character + * and 255 is returned + */ +unsigned char binary_to_base64(unsigned char v); + +/* base64_to_binary: + * Description: + * Converts a single byte from a base64 character to the corresponding binary value + * Parameters: + * c - Base64 character (as ascii code) + * Returns: + * 6-bit binary value + */ +unsigned char base64_to_binary(unsigned char v); + +/* encode_base64_length: + * Description: + * Calculates length of base64 string needed for a given number of binary bytes + * Parameters: + * input_length - Amount of binary data in bytes + * Returns: + * Number of base64 characters needed to encode input_length bytes of binary data + */ +unsigned int encode_base64_length(unsigned int input_length); + +/* decode_base64_length: + * Description: + * Calculates number of bytes of binary data in a base64 string + * Parameters: + * input - Base64-encoded null-terminated string + * Returns: + * Number of bytes of binary data in input + */ +unsigned int decode_base64_length(unsigned char input[]); + +/* encode_base64: + * Description: + * Converts an array of bytes to a base64 null-terminated string + * Parameters: + * input - Pointer to input data + * input_length - Number of bytes to read from input pointer + * output - Pointer to output string. Null terminator will be added automatically + * Returns: + * Length of encoded string in bytes (not including null terminator) + */ +unsigned int encode_base64(unsigned char input[], unsigned int input_length, unsigned char output[]); + +/* decode_base64: + * Description: + * Converts a base64 null-terminated string to an array of bytes + * Parameters: + * input - Pointer to input string + * output - Pointer to output array + * Returns: + * Number of bytes in the decoded binary + */ +unsigned int decode_base64(unsigned char input[], unsigned char output[]); + +unsigned char binary_to_base64(unsigned char v) { + // Capital letters - 'A' is ascii 65 and base64 0 + if(v < 26) return v + 'A'; + + // Lowercase letters - 'a' is ascii 97 and base64 26 + if(v < 52) return v + 71; + + // Digits - '0' is ascii 48 and base64 52 + if(v < 62) return v - 4; + + // '+' is ascii 43 and base64 62 + if(v == 62) return '-'; + + // '/' is ascii 47 and base64 63 + if(v == 63) return '_'; + + return 64; +} + +unsigned char base64_to_binary(unsigned char c) { + // Capital letters - 'A' is ascii 65 and base64 0 + if('A' <= c && c <= 'Z') return c - 'A'; + + // Lowercase letters - 'a' is ascii 97 and base64 26 + if('a' <= c && c <= 'z') return c - 71; + + // Digits - '0' is ascii 48 and base64 52 + if('0' <= c && c <= '9') return c + 4; + + // '+' is ascii 43 and base64 62 + if(c == '-') return 62; + + // '/' is ascii 47 and base64 63 + if(c == '_') return 63; + + return 255; +} + +unsigned int encode_base64_length(unsigned int input_length) { + return (input_length + 2)/3*4; +} + +unsigned int decode_base64_length(unsigned char input[]) { + unsigned char *start = input; + + while(base64_to_binary(input[0]) < 64) { + ++input; + } + + unsigned int input_length = input - start; + + unsigned int output_length = input_length/4*3; + + switch(input_length % 4) { + default: return output_length; + case 2: return output_length + 1; + case 3: return output_length + 2; + } +} + +unsigned int encode_base64(unsigned char input[], unsigned int input_length, unsigned char output[]) { + unsigned int full_sets = input_length/3; + + // While there are still full sets of 24 bits... + for(unsigned int i = 0; i < full_sets; ++i) { + output[0] = binary_to_base64( input[0] >> 2); + output[1] = binary_to_base64((input[0] & 0x03) << 4 | input[1] >> 4); + output[2] = binary_to_base64((input[1] & 0x0F) << 2 | input[2] >> 6); + output[3] = binary_to_base64( input[2] & 0x3F); + + input += 3; + output += 4; + } + + switch(input_length % 3) { + case 0: + output[0] = '\0'; + break; + case 1: + output[0] = binary_to_base64( input[0] >> 2); + output[1] = binary_to_base64((input[0] & 0x03) << 4); + output[2] = '='; + output[3] = '='; + output[4] = '\0'; + break; + case 2: + output[0] = binary_to_base64( input[0] >> 2); + output[1] = binary_to_base64((input[0] & 0x03) << 4 | input[1] >> 4); + output[2] = binary_to_base64((input[1] & 0x0F) << 2); + output[3] = '='; + output[4] = '\0'; + break; + } + + return encode_base64_length(input_length); +} + +unsigned int decode_base64(unsigned char input[], unsigned char output[]) { + unsigned int output_length = decode_base64_length(input); + + // While there are still full sets of 24 bits... + for(unsigned int i = 2; i < output_length; i += 3) { + output[0] = base64_to_binary(input[0]) << 2 | base64_to_binary(input[1]) >> 4; + output[1] = base64_to_binary(input[1]) << 4 | base64_to_binary(input[2]) >> 2; + output[2] = base64_to_binary(input[2]) << 6 | base64_to_binary(input[3]); + + input += 4; + output += 3; + } + + switch(output_length % 3) { + case 1: + output[0] = base64_to_binary(input[0]) << 2 | base64_to_binary(input[1]) >> 4; + break; + case 2: + output[0] = base64_to_binary(input[0]) << 2 | base64_to_binary(input[1]) >> 4; + output[1] = base64_to_binary(input[1]) << 4 | base64_to_binary(input[2]) >> 2; + break; + } + + return output_length; +} + +#endif // ifndef diff --git a/zerolamp/bluetooth.cpp b/zerolamp/bluetooth.cpp index eae70ce..4ad3329 100644 --- a/zerolamp/bluetooth.cpp +++ b/zerolamp/bluetooth.cpp @@ -1,70 +1,120 @@ -#include #include "bluetooth.h" +#include "JWT.h" // siakinnik - added +#include #include "program_controller.h" +#include +#include -#include +// extern const char* WLAN_SSID; +// extern const char* WLAN_PASSWORD; -#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED) -#error Bluetooth is not enabled! -#endif +// WebSocket +WebSocketsServer webSocket(81); -BluetoothSerial SerialBT; +String incoming_line; +String connectionCode; +bool clientAuthorized = false; -void bluetooth_init() { - - SerialBT.begin("ZeroLamp", false, true); - // SerialBT.deleteAllBondedDevices(); +// SerialWrapper +BLESerialWrapper wsSerial; - Serial.print("ESP32 bluetooth address: "); - Serial.println(SerialBT.getBtAddressString()); +void BLESerialWrapper::println(const String& msg) { + if (clientAuthorized) { + webSocket.broadcastTXT(msg + "\n"); + } +} +void BLESerialWrapper::printf(const char* fmt, ...) { + char buffer[256]; + va_list args; + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + println(String(buffer)); } -std::vector incoming_bytes; +// Websocet event +// ChatGPT parts!!! +void webSocketEvent(uint8_t client_num, WStype_t type, uint8_t* payload, size_t length) { + if (type == WStype_CONNECTED) { + Serial.printf("Client %u connected\n", client_num); + clientAuthorized = false; + incoming_line = ""; + + // siakinnik - added + connectionCode = String(random(1000, 9999)); + Serial.print("Connection code: "); + Serial.println(connectionCode); + String cmd1 = "fg 16"; + String cmd2 = "w |" + connectionCode + "|"; + program_controller_handle_command(std::string(cmd1.c_str())); + program_controller_handle_command(std::string(cmd2.c_str())); + + webSocket.sendTXT(client_num, "Enter connection code:"); + } else if (type == WStype_TEXT) { + String msg = ""; + for (size_t i = 0; i < length; i++) msg += (char)payload[i]; + msg.trim(); + + if (!clientAuthorized) { + if (msg == connectionCode) { + clientAuthorized = true; + webSocket.sendTXT(client_num, "Authorized! Send commands."); + + // siakinnik - added + // siakinnik TODO - put original fg size and mode + String cmd1 = "fg 6"; + String cmd2 = "fg clock"; + program_controller_handle_command(std::string(cmd1.c_str())); + program_controller_handle_command(std::string(cmd2.c_str())); + + } else { + webSocket.sendTXT(client_num, "Wrong code. Disconnecting..."); + webSocket.disconnect(client_num); + + // siakinnik - added + // siakinnik TODO - put original fg size and mode + String cmd1 = "fg 6"; + String cmd2 = "fg clock"; + program_controller_handle_command(std::string(cmd1.c_str())); + program_controller_handle_command(std::string(cmd2.c_str())); + } + return; + } -void bluetooth_tick() { + program_controller_handle_command(std::string(msg.c_str())); - if (!SerialBT.available()) { - return; + } else if (type == WStype_DISCONNECTED) { + clientAuthorized = false; + incoming_line = ""; } +} - int read_iterations = 0; - bool reached_end = false; - - do { - - uint8_t byte = SerialBT.read(); - read_iterations++; - - if (byte == '\r') { - continue; - } - - if (byte == '\n') { - reached_end = true; - break; +void bluetooth_init() { + if (WiFi.status() != WL_CONNECTED) { + Serial.println("Waiting for Wi-Fi connection..."); + while (WiFi.status() != WL_CONNECTED) { + delay(100); } + } - incoming_bytes.emplace_back(byte); - - } while (SerialBT.available() && read_iterations < MAX_READ_ITERATIONS_PER_TICK); - - if (reached_end) { - - std::string incoming_string(incoming_bytes.begin(), incoming_bytes.end()); - - incoming_bytes.clear(); - - program_controller_handle_command(std::move(incoming_string)); + // siakinnik - removed + // connectionCode = String(random(1000, 9999)); + // Serial.print("Connection code: "); + // Serial.println(connectionCode); - } + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} +void bluetooth_tick() { + webSocket.loop(); } -BluetoothSerial* bluetooth_serial() { - return &SerialBT; +BLESerialWrapper* bluetooth_serial() { + return &wsSerial; } bool bluetooth_device_connected() { - return SerialBT.hasClient(); + return clientAuthorized; } \ No newline at end of file diff --git a/zerolamp/bluetooth.h b/zerolamp/bluetooth.h index 9f60387..db4100b 100644 --- a/zerolamp/bluetooth.h +++ b/zerolamp/bluetooth.h @@ -1,13 +1,28 @@ #ifndef ZB_BLUETOOTH_H #define ZB_BLUETOOTH_H -#include +#include +#include +#include +#include -#define MAX_READ_ITERATIONS_PER_TICK 64 +struct BLESerialWrapper { + void println(const String& msg); + void printf(const char* fmt, ...); +}; + +// siakinnik - added +// struct WSClientInit { +// uint8_t client_num; +// unsigned long connectTime; +// bool authorized; +// }; + +const unsigned long INIT_TIMEOUT = 2000; void bluetooth_init(); void bluetooth_tick(); -BluetoothSerial* bluetooth_serial(); +BLESerialWrapper* bluetooth_serial(); bool bluetooth_device_connected(); -#endif \ No newline at end of file +#endif diff --git a/zerolamp/build_options.h b/zerolamp/build_options.h new file mode 100644 index 0000000..c967f27 --- /dev/null +++ b/zerolamp/build_options.h @@ -0,0 +1,32 @@ +// siakinnik - added +// -------------------- +// Build options +// -------------------- + +#ifndef BUILD_OPTIONS_H +#define BUILD_OPTIONS_H + +#define LEGACY_ZEROLAMP +// #define ZEROLAMP_S3V1 + +#ifdef ZEROLAMP_S3V1 +#define TOUCH_BUTTON_PIN 7 // deep-sleep button (TTP223) +#endif + +#define CURRENT_LIMIT 2000 // maximum matrix current in milliamps +#define NUM_LEDS 256 // 16x16 matrix + +#ifdef ZEROLAMP_S3V1 +#define DATA_PIN 5 // GPIO pin connected to WS2812B matrix +#endif + +#ifdef LEGACY_ZEROLAMP +#define DATA_PIN 19 // GPIO pin connected to WS2812B matrix +#endif + +#define SNAKE_MATRIX true +// #define ROW_MATRIX true + +const int MATRIX_WIDTH = 16; +const int MATRIX_HEIGHT = 16; +#endif \ No newline at end of file diff --git a/zerolamp/matrix.cpp b/zerolamp/matrix.cpp index 5abaa77..0e19129 100644 --- a/zerolamp/matrix.cpp +++ b/zerolamp/matrix.cpp @@ -5,23 +5,37 @@ CRGB leds[NUM_LEDS]; void matrix_init() { FastLED.addLeds(leds, NUM_LEDS); - FastLED.setBrightness(16); + // siakinnik - removed + // FastLED.setBrightness(16); + + FastLED.setBrightness(MATRIX_BRIGHTNES); + if (CURRENT_LIMIT > 0) { FastLED.setMaxPowerInVoltsAndMilliamps(5, CURRENT_LIMIT); } - FastLED.setCorrection(TypicalLEDStrip); - // optional: clear all led's to ensure that they don't continue representing the state after the firmware stopped or crashed + FastLED.setCorrection(TypicalLEDStrip); FastLED.clear(true); } +#ifdef ROW_MATRIX +inline int ledId(int y, int x) { + // Added rotation 90° + int newY = x; + int newX = MATRIX_WIDTH - 1 - y; + return newY * MATRIX_WIDTH + newX; +} +#endif + +#ifdef SNAKE_MATRIX inline int ledId(int y, int x) { if (x % 2 == 0) { // led's numbered from top to bottom return MATRIX_HEIGHT * x + y; } - return MATRIX_HEIGHT * x + MATRIX_HEIGHT - 1 - y; // mirror along the y axis + return MATRIX_HEIGHT * x + MATRIX_HEIGHT - 1 - y; // mirror along the y axis } +#endif void matrix_setLedColor(int y, int x, CRGB color) { assert(y >= 0); @@ -32,4 +46,4 @@ void matrix_setLedColor(int y, int x, CRGB color) { void matrix_clearLed(int y, int x) { matrix_setLedColor(y, x, CRGB::Black); -} \ No newline at end of file +} diff --git a/zerolamp/matrix.h b/zerolamp/matrix.h index 35ec6fe..abad833 100644 --- a/zerolamp/matrix.h +++ b/zerolamp/matrix.h @@ -2,13 +2,19 @@ #define ZB_MATRIX_H #include +#include "build_options.h" // siakinnik - added -#define CURRENT_LIMIT 2000 // maximum current in milliamps -#define NUM_LEDS 256 // 16x16 matrix -#define DATA_PIN 19 // GPIO pin connected to WS2812B matrix +// siakinnik - moved to build_options +// #define CURRENT_LIMIT 2000 // maximum current in milliamps +// #define NUM_LEDS 256 // 16x16 matrix +// #define DATA_PIN 5 // GPIO pin connected to WS2812B matrix +// #define SNAKE_MATRIX true +// // #define ROW_MATRIX true -const int MATRIX_WIDTH = 16; -const int MATRIX_HEIGHT = 16; +// const int MATRIX_WIDTH = 16; +// const int MATRIX_HEIGHT = 16; + +const int MATRIX_BRIGHTNES = 16; // siakinnik - added void matrix_init(); void matrix_setLedColor(int y, int x, CRGB color); diff --git a/zerolamp/mode_clock.cpp b/zerolamp/mode_clock.cpp index 187ce94..4faee0b 100644 --- a/zerolamp/mode_clock.cpp +++ b/zerolamp/mode_clock.cpp @@ -3,6 +3,7 @@ #include #include "font.h" #include "wlan.h" +#include void ClockMode::enter(int logical_width, int logical_height) { this->logical_width = logical_width; diff --git a/zerolamp/mode_info.cpp b/zerolamp/mode_info.cpp index 58a8bbe..2098282 100644 --- a/zerolamp/mode_info.cpp +++ b/zerolamp/mode_info.cpp @@ -2,25 +2,46 @@ #include "font.h" void InfoMode::enter(int logical_width, int logical_height) { - line1_start_x = logical_width; - line2_start_x = logical_width; + // siakinnik - deleted + // line1_start_x = logical_width; + // line2_start_x = logical_width; + + // siakinnik - added + int center_index = 0; + int center_x = logical_width / 2; + line1_start_x = center_x - (CENTER_VISIBLE * FONT_WIDTH) / 2 - center_index * FONT_WIDTH; line1_colors = new CRGB[text_line1.size()]; +#ifdef MONOTONE + CRGB baseColor = CHSV(0, 0xff, 0xff); // white, ended up red for some reason ._. for (int i = 0; i < text_line1.size(); i++) { - line1_colors[i] = CHSV(map(i, 0, text_line1.size() - 1, 0, 0xff), 0xff, 0xff); + line1_colors[i] = baseColor; } - line2_colors = new CRGB[text_line2.size()]; - for (int i = 0; i < text_line2.size(); i++) { - line2_colors[i] = CHSV(map(i, 0, text_line2.size() - 1, 0, 0xff), 0xff, 0xff); +#endif + +#ifndef MONOTONE + for (int i = 0; i < text_line1.size(); i++) { + line1_colors[i] = CHSV(map(i, 0, text_line1.size() - 1, 0, 0xff), 0xff, 0xff); } +#endif + + // siakinnik - deleted + // line2_colors = new CRGB[text_line2.size()]; + // for (int i = 0; i < text_line2.size(); i++) { + // line2_colors[i] = CHSV(map(i, 0, text_line2.size() - 1, 0, 0xff), 0xff, 0xff); + // } set_render_interval(500); } void InfoMode::leave() { - delete line1_colors; - delete line2_colors; + // siakinnik - deleted + // delete line1_colors; + // delete line2_colors; + + // siakinnik - added + delete[] line1_colors; } void InfoMode::render_frame(int offset_x, int offset_y, int viewport_width, int viewport_height) { @@ -29,35 +50,45 @@ void InfoMode::render_frame(int offset_x, int offset_y, int viewport_width, int int line1_real_width = draw_string_horizontally( offset_x + line1_start_x, - offset_y + 2, + offset_y + 5, text_line1.c_str(), offset_x, offset_x + viewport_width - 1, line1_colors, - 1 - ); + 1); - int line2_real_width = draw_string_horizontally( - offset_x + line2_start_x, - offset_y + 9, - text_line2.c_str(), - offset_x, - offset_x + viewport_width - 1, - line2_colors, - 1 - ); + // siakinnik - deleted + // int line2_real_width = draw_string_horizontally( + // offset_x + line2_start_x, + // offset_y + 9, + // text_line2.c_str(), + // offset_x, + // offset_x + viewport_width - 1, + // line2_colors, + // 1 + // ); line1_start_x--; - line2_start_x--; + // siakinnik - deleted + // line2_start_x--; - if (line1_start_x < -line1_real_width) { - line1_start_x = viewport_width; - } + // siakinnik - deleted + // if (line1_start_x < -line1_real_width) { + // line1_start_x = viewport_width; + // } + + // siakinnik - added + int center_x = viewport_width / 2; + int start_center_x = center_x - (CENTER_VISIBLE * FONT_WIDTH) / 2; - if (line2_start_x < -line2_real_width) { - line2_start_x = viewport_width; + if (line1_start_x < -line1_real_width) { + line1_start_x = start_center_x; } + // siakinnik - deleted + // if (line2_start_x < -line2_real_width) { + // line2_start_x = viewport_width; + // } } CommandHandleResult InfoMode::handle_command(std::string command) { diff --git a/zerolamp/mode_info.h b/zerolamp/mode_info.h index f4d58af..e1af828 100644 --- a/zerolamp/mode_info.h +++ b/zerolamp/mode_info.h @@ -4,6 +4,11 @@ #include "mode.h" #include +// siakinnik - added +#define MONOTONE +#define CENTER_VISIBLE 2 +#define FONT_WIDTH 3 + class InfoMode : public LampMode { private: diff --git a/zerolamp/sha256.cpp b/zerolamp/sha256.cpp new file mode 100644 index 0000000..f8b6b2e --- /dev/null +++ b/zerolamp/sha256.cpp @@ -0,0 +1,177 @@ +/** + * Based on original work by Cathedrow + * Original library located at https://github.com/Cathedrow/Cryptosuite + */ + +#include +#include "sha256.h" + +uint32_t sha256K[] PROGMEM = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 +}; + +#define BUFFER_SIZE 64 + +unsigned char sha256InitState[] PROGMEM = { + 0x67,0xe6,0x09,0x6a, // H0 + 0x85,0xae,0x67,0xbb, // H1 + 0x72,0xf3,0x6e,0x3c, // H2 + 0x3a,0xf5,0x4f,0xa5, // H3 + 0x7f,0x52,0x0e,0x51, // H4 + 0x8c,0x68,0x05,0x9b, // H5 + 0xab,0xd9,0x83,0x1f, // H6 + 0x19,0xcd,0xe0,0x5b // H7 +}; + +void Sha256Class::init(void) { + memcpy_P(state.b,sha256InitState,32); + byteCount = 0; + bufferOffset = 0; +} + +uint32_t Sha256Class::ror32(uint32_t number, unsigned char bits) { + return ((number << (32-bits)) | (number >> bits)); +} + +void Sha256Class::hashBlock() { + // Sha256 only for now + unsigned char i; + uint32_t a,b,c,d,e,f,g,h,t1,t2; + + a=state.w[0]; + b=state.w[1]; + c=state.w[2]; + d=state.w[3]; + e=state.w[4]; + f=state.w[5]; + g=state.w[6]; + h=state.w[7]; + + for (i=0; i<64; i++) { + if (i>=16) { + t1 = buffer.w[i&15] + buffer.w[(i-7)&15]; + t2 = buffer.w[(i-2)&15]; + t1 += ror32(t2,17) ^ ror32(t2,19) ^ (t2>>10); + t2 = buffer.w[(i-15)&15]; + t1 += ror32(t2,7) ^ ror32(t2,18) ^ (t2>>3); + buffer.w[i&15] = t1; + } + t1 = h; + t1 += ror32(e,6) ^ ror32(e,11) ^ ror32(e,25); // ?1(e) + t1 += g ^ (e & (g ^ f)); // Ch(e,f,g) + t1 += pgm_read_dword(sha256K+i); // Ki + t1 += buffer.w[i&15]; // Wi + t2 = ror32(a,2) ^ ror32(a,13) ^ ror32(a,22); // ?0(a) + t2 += ((b & c) | (a & (b | c))); // Maj(a,b,c) + h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + state.w[0] += a; + state.w[1] += b; + state.w[2] += c; + state.w[3] += d; + state.w[4] += e; + state.w[5] += f; + state.w[6] += g; + state.w[7] += h; +} + +void Sha256Class::addUncounted(unsigned char data) { + buffer.b[bufferOffset ^ 3] = data; + bufferOffset++; + if (bufferOffset == BUFFER_SIZE) { + hashBlock(); + bufferOffset = 0; + } +} + +size_t Sha256Class::write(unsigned char data) { + ++byteCount; + addUncounted(data); + return( 1 ); +} + +void Sha256Class::pad() { + // Implement SHA-256 padding (fips180-2 §5.1.1) + + // Pad with 0x80 followed by 0x00 until the end of the block + addUncounted(0x80); + while (bufferOffset != 56) addUncounted(0x00); + + // Append length in the last 8 bytes + addUncounted(0); // We're only using 32 bit lengths + addUncounted(0); // But SHA-1 supports 64 bit lengths + addUncounted(0); // So zero pad the top bits + addUncounted(byteCount >> 29); // Shifting to multiply by 8 + addUncounted(byteCount >> 21); // as SHA-1 supports bitstreams as well as + addUncounted(byteCount >> 13); // byte. + addUncounted(byteCount >> 5); + addUncounted(byteCount << 3); +} + + +unsigned char* Sha256Class::result(void) { + // Pad to complete the last block + pad(); + + // Swap byte order back + for (int i=0; i<8; i++) { + uint32_t a,b; + a=state.w[i]; + b=a<<24; + b|=(a<<8) & 0x00ff0000; + b|=(a>>8) & 0x0000ff00; + b|=a>>24; + state.w[i]=b; + } + + // Return pointer to hash (20 characters) + return state.b; +} + + +#define HMAC_IPAD 0x36 +#define HMAC_OPAD 0x5c + +unsigned char keyBuffer[BLOCK_LENGTH]; // K0 in FIPS-198a +unsigned char innerHash[HASH_LENGTH]; + +void Sha256Class::initHmac(const unsigned char* key, int keyLength) { + unsigned char i; + memset(keyBuffer,0,BLOCK_LENGTH); + if (keyLength > BLOCK_LENGTH) { + // Hash long keys + init(); + for (;keyLength--;) write(*key++); + memcpy(keyBuffer,result(),HASH_LENGTH); + } else { + // Block length keys are used as is + memcpy(keyBuffer,key,keyLength); + } + //for (i=0; i +#include "Print.h" + +#define HASH_LENGTH 32 +#define BLOCK_LENGTH 64 + +union _buffer { + unsigned char b[BLOCK_LENGTH]; + uint32_t w[BLOCK_LENGTH/4]; +}; +union _state { + unsigned char b[HASH_LENGTH]; + uint32_t w[HASH_LENGTH/4]; +}; + +class Sha256Class : public Print +{ + public: + void init(void); + void initHmac(const unsigned char* secret, int secretLength); + unsigned char* result(void); + unsigned char* resultHmac(void); + virtual size_t write(unsigned char); + using Print::write; + private: + void pad(); + void addUncounted(unsigned char data); + void hashBlock(); + uint32_t ror32(uint32_t number, unsigned char bits); + _buffer buffer; + unsigned char bufferOffset; + _state state; + uint32_t byteCount; + unsigned char keyBuffer[BLOCK_LENGTH]; + unsigned char innerHash[HASH_LENGTH]; +}; +extern Sha256Class Sha256; + +#endif diff --git a/zerolamp/wlan.cpp b/zerolamp/wlan.cpp index 72f93dc..b5b0fbb 100644 --- a/zerolamp/wlan.cpp +++ b/zerolamp/wlan.cpp @@ -1,17 +1,64 @@ #include "wlan.h" +// #include "time.h" #include #include +#include // NTP Server const char* ntpServer = "pool.ntp.org"; // Offset for your timezone in seconds (e.g., -5*60*60 for UTC-5) -const long gmtOffset_sec = 60*60; +const long gmtOffset_sec = 60 * 60; // Daylight saving time offset in seconds (usually 0 or 3600) const int daylightOffset_sec = 0; void wlan_init() { WiFi.mode(WIFI_STA); WiFi.begin(WLAN_SSID, WLAN_PASSWORD); + + Serial.print("Connecting to Wi-Fi"); + + unsigned long startMillis = millis(); + bool connected = false; + + while (millis() - startMillis < WIFI_CONNECT_TIMEOUT) { + if (WiFi.status() == WL_CONNECTED) { + connected = true; + break; + } + delay(500); + Serial.print("."); + } + + if (connected) { + Serial.println(); + Serial.print("Connected! IP: "); + Serial.println(WiFi.localIP()); + + // --- mDNS --- + if (MDNS.begin(MDNS_NAME)) { + Serial.println("mDNS responder started"); + } else { + Serial.println("Error starting mDNS"); + } + } else { + Serial.println(); + Serial.println("Wi-Fi connection failed. Starting AP mode..."); + + WiFi.mode(WIFI_AP); + WiFi.softAP(AP_SSID, AP_PASSWORD); + + Serial.print("AP mode started. Connect to Wi-Fi SSID: "); + Serial.println(AP_SSID); + + // mDNS + if (MDNS.begin(MDNS_NAME)) { + Serial.println("mDNS responder started in AP mode"); + } else { + Serial.println("Error starting mDNS in AP mode"); + } + Serial.print("AP IP: "); + Serial.println(WiFi.softAPIP()); + } // configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); } @@ -25,7 +72,7 @@ struct tm* get_current_time() { return ¤t_time_info; } - return nullptr; // error + return nullptr; // error } void _sync_time() { @@ -59,9 +106,8 @@ void _sync_time() { lastSuccessfulTimeSync = millis(); Serial.println(&timeInfo, "Time synchronized: %A, %B %d %Y %H:%M:%S"); - } void wlan_tick() { _sync_time(); -} \ No newline at end of file +} diff --git a/zerolamp/wlan.h b/zerolamp/wlan.h index 75d60ad..4fc7f49 100644 --- a/zerolamp/wlan.h +++ b/zerolamp/wlan.h @@ -1,16 +1,20 @@ #ifndef ZB_WLAN_H #define ZB_WLAN_H -#define WLAN_SSID "YODA" -#define WLAN_PASSWORD "2I9gF8FyJB27rf0ObvaVmrOGaJw5zrVh" +// #define WLAN_SSID "YODA" +// #define WLAN_PASSWORD "2I9gF8FyJB27rf0ObvaVmrOGaJw5zrVh" -// #define WLAN_SSID "NikitaNet_VPN_2.4" -// #define WLAN_PASSWORD "63430356" +#define WLAN_SSID "TP-Link_BF08" +#define WLAN_PASSWORD "16333161" #define TIME_SYNC_INTERVAL 30000 // how often to syncronize time, in milliseconds +#define MDNS_NAME "ZeroLamp" +#define AP_SSID "ZeroLamp-Setup" +#define AP_PASSWORD "12345678" +#define WIFI_CONNECT_TIMEOUT 30000 -struct tm* get_current_time(); void wlan_init(); void wlan_tick(); +struct tm* get_current_time(); #endif \ No newline at end of file diff --git a/zerolamp/zerolamp.ino b/zerolamp/zerolamp.ino index f40750a..7d0e465 100644 --- a/zerolamp/zerolamp.ino +++ b/zerolamp/zerolamp.ino @@ -2,12 +2,42 @@ #include "wlan.h" #include "bluetooth.h" #include "program_controller.h" +#include "build_options.h" // siakinnik - added #include +#include -void setup() { +// siakinnik - moved to build_options +// #ifdef ZEROLAMP_S3V1 +// #define TOUCH_BUTTON_PIN 7 // deep-sleep button (TTP223) +// #endif + +#ifdef ZEROLAMP_S3V1 +void go_to_sleep() { + Serial.println("Going to deep sleep..."); + + // Clear matrix + FastLED.clear(true); + delay(50); + + // Button wakeup + esp_sleep_enable_ext0_wakeup( + (gpio_num_t)TOUCH_BUTTON_PIN, + 1 // HIGH + ); + + Serial.flush(); + esp_deep_sleep_start(); +} +#endif +void setup() { Serial.begin(115200); + delay(200); + +#ifdef ZEROLAMP_S3V1 + pinMode(TOUCH_BUTTON_PIN, INPUT); +#endif Serial.println("Initializing ZeroLamp..."); @@ -15,22 +45,28 @@ void setup() { randomSeed(seed); random16_set_seed(~seed); + wlan_init(); bluetooth_init(); matrix_init(); - wlan_init(); program_controller_init(); - - Serial.println("ZeroLamp successfully initialized."); + Serial.println("ZeroLamp ready."); } void loop() { +#ifdef ZEROLAMP_S3V1 + // Button + if (digitalRead(TOUCH_BUTTON_PIN) == HIGH) { + delay(50); + if (digitalRead(TOUCH_BUTTON_PIN) == HIGH) { + delay(500); + go_to_sleep(); + } + } +#endif bluetooth_tick(); wlan_tick(); program_controller_tick(); - - // add 16-bit entropy from the slower random number generator to the faster one random16_add_entropy(random(0x10000)); - }