├── .gitignore ├── library.properties ├── .github └── workflows │ └── version.yml ├── library.json ├── scripts ├── copy.py ├── version.py └── keywords.py ├── platformio.ini ├── LICENSE ├── examples ├── control │ └── control.ino ├── camera │ └── camera.ino ├── navigation │ └── navigation.ino ├── health │ └── health.ino └── watch │ └── watch.ino ├── README.md ├── keywords.txt └── src ├── ChronosESP32.h └── ChronosESP32.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | .pio 4 | .vscode 5 | 6 | navigation.txt 7 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ChronosESP32 2 | version=1.9.0 3 | author=fbiego 4 | maintainer=fbiego 5 | sentence=Setup your ESP32 as a smartwatch and connect to Chronos app over BLE. 6 | paragraph=A wrapper library for ESP32 to facilitate easy setup of a smartwatch like project. Supports syncing of notifications from the phone. 7 | category=Communication 8 | url=https://github.com/fbiego/chronos-esp32 9 | architectures=* 10 | includes=ChronosESP32.h 11 | depends=ESP32Time,NimBLE-Arduino -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Validate Library Version 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main # or your default branch 7 | 8 | jobs: 9 | check-version: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Run version validation script 22 | run: python3 scripts/version.py 23 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChronosESP32", 3 | "version": "1.9.0", 4 | "keywords": "Arduino, ESP32, Time, BLE, Watch", 5 | "description": "A library for ESP32 to interface with Chronos app over BLE", 6 | "repository": 7 | { 8 | "type": "git", 9 | "url": "https://github.com/fbiego/chronos-esp32" 10 | }, 11 | "authors": 12 | [ 13 | { 14 | "name": "fbiego", 15 | "email": "fbiego.fb@gmail.com", 16 | "maintainer": true 17 | } 18 | ], 19 | "frameworks": "arduino", 20 | "platforms": "espressif32", 21 | "dependencies": { 22 | "h2zero/NimBLE-Arduino": "^2.1.0", 23 | "fbiego/ESP32Time": "^2.0.6" 24 | }, 25 | "headers": "ChronosESP32.h" 26 | } 27 | -------------------------------------------------------------------------------- /scripts/copy.py: -------------------------------------------------------------------------------- 1 | 2 | import shutil 3 | import os 4 | 5 | Import("env") 6 | 7 | def copy_files_recursive(src_dir, dest_dir): 8 | """Recursively copies files and directories from the source to the destination. 9 | 10 | Args: 11 | src_dir: The source directory. 12 | dest_dir: The destination directory. 13 | """ 14 | print(f"Updating source files to {dest_dir}") 15 | 16 | for item in os.listdir(src_dir): 17 | src_item = os.path.join(src_dir, item) 18 | dst_item = os.path.join(dest_dir, item) 19 | 20 | if os.path.isdir(src_item): 21 | shutil.copytree(src_item, dst_item, dirs_exist_ok=True) 22 | else: 23 | shutil.copy2(src_item, dst_item) 24 | 25 | try: 26 | pioenv = env._dict['PIOENV'] 27 | print(f"PlatformIO environment: {pioenv}") 28 | except (KeyError, AttributeError): 29 | print("PlatformIO environment not found in the SCons Environment.") 30 | pioenv = "devkit" 31 | 32 | sep = os.sep 33 | copy_files_recursive(f"src{sep}", f".pio{sep}libdeps{sep}{pioenv}{sep}src{sep}") 34 | 35 | -------------------------------------------------------------------------------- /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 | default_envs = devkit 14 | 15 | ; Uncomment only one to test 16 | src_dir = examples/watch 17 | ; src_dir = examples/camera 18 | ; src_dir = examples/control 19 | ; src_dir = examples/navigation 20 | ; src_dir = examples/health 21 | 22 | 23 | [env] 24 | platform = espressif32 25 | framework = arduino 26 | extra_scripts = post:scripts/copy.py 27 | monitor_speed = 115200 28 | 29 | lib_deps = 30 | ; use src folder as library 31 | file://./src 32 | ; external library dependencies 33 | fbiego/ESP32Time@^2.0.6 34 | h2zero/NimBLE-Arduino@^2.1.0 35 | 36 | [env:esp32dev] 37 | board = esp32dev 38 | 39 | [env:devkit] 40 | board = esp32doit-devkit-v1 41 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Felix Biego 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 | -------------------------------------------------------------------------------- /scripts/version.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import sys 4 | from pathlib import Path 5 | 6 | def extract_version_from_properties(file_path): 7 | with open(file_path, "r") as f: 8 | for line in f: 9 | if line.strip().startswith("version="): 10 | return line.strip().split("=", 1)[1] 11 | raise ValueError("Version not found in library.properties") 12 | 13 | def extract_version_from_json(file_path): 14 | with open(file_path, "r") as f: 15 | data = json.load(f) 16 | return data.get("version") 17 | 18 | def extract_version_from_header(file_path): 19 | content = Path(file_path).read_text() 20 | major = re.search(r"#define\s+CHRONOSESP_VERSION_MAJOR\s+(\d+)", content) 21 | minor = re.search(r"#define\s+CHRONOSESP_VERSION_MINOR\s+(\d+)", content) 22 | patch = re.search(r"#define\s+CHRONOSESP_VERSION_PATCH\s+(\d+)", content) 23 | if not (major and minor and patch): 24 | raise ValueError("Version macros not found in header") 25 | return f"{major.group(1)}.{minor.group(1)}.{patch.group(1)}" 26 | 27 | def main(): 28 | props_ver = extract_version_from_properties("library.properties") 29 | json_ver = extract_version_from_json("library.json") 30 | header_ver = extract_version_from_header("src/ChronosESP32.h") 31 | 32 | print(f"📦 library.properties version: {props_ver}") 33 | print(f"📝 library.json version: {json_ver}") 34 | print(f"🔣 Header macro version: {header_ver}") 35 | 36 | if props_ver != json_ver or props_ver != header_ver: 37 | print("❌ Version mismatch detected!") 38 | sys.exit(1) 39 | 40 | print("✅ All version numbers match!") 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /scripts/keywords.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | def extract_keywords(header_file_path, library_name="ChronosESP32", output_file="keywords.txt"): 5 | with open(header_file_path, "r") as file: 6 | lines = file.readlines() 7 | 8 | output_lines = [f"{library_name}\tKEYWORD1\n"] 9 | content_up_to_private = [] 10 | 11 | private_markers = [ 12 | re.compile(r'^\s*private:', re.I), 13 | re.compile(r'^\s*/\*\s*private', re.I), 14 | re.compile(r'^\s*#ifdef\s+INTERNAL_API', re.I), 15 | re.compile(r'^\s*#ifndef\s+PUBLIC_API', re.I), 16 | ] 17 | 18 | for line in lines: 19 | if any(marker.search(line) for marker in private_markers): 20 | break 21 | content_up_to_private.append(line) 22 | 23 | public_section = ''.join(content_up_to_private) 24 | public_section = re.sub(r"//.*?$|/\*.*?\*/", "", public_section, flags=re.DOTALL | re.MULTILINE) 25 | 26 | # Ordered function name extraction 27 | func_pattern = re.compile( 28 | r"^[ \t]*((?!static)(?!inline)[a-zA-Z_][\w\s\*\(\)]*?)\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^;]*\)\s*;", 29 | re.MULTILINE 30 | ) 31 | seen = set() 32 | functions_in_order = [] 33 | for match in func_pattern.finditer(public_section): 34 | func_name = match.group(2) 35 | if func_name not in seen: 36 | seen.add(func_name) 37 | functions_in_order.append(func_name) 38 | 39 | for func in functions_in_order: 40 | output_lines.append(f"{func}\tKEYWORD2") 41 | 42 | output_lines.append("") 43 | 44 | # Full content for structs/enums 45 | full_content = ''.join(lines) 46 | full_content = re.sub(r"//.*?$|/\*.*?\*/", "", full_content, flags=re.DOTALL | re.MULTILINE) 47 | 48 | struct_enum_pattern = re.compile(r"\b(struct|enum)\s+([a-zA-Z_]\w*)\b") 49 | struct_enum_names = [] 50 | seen_structs = set() 51 | for match in struct_enum_pattern.finditer(full_content): 52 | name = match.group(2) 53 | if name not in seen_structs: 54 | seen_structs.add(name) 55 | struct_enum_names.append(name) 56 | 57 | for name in struct_enum_names: 58 | output_lines.append(f"{name}\tLITERAL1") 59 | 60 | output_lines.append("") 61 | 62 | # Enum values 63 | enum_blocks = re.findall(r"enum\s+[a-zA-Z_]\w*\s*{([^}]+)}", full_content, re.DOTALL) 64 | for block in enum_blocks: 65 | enum_values = re.findall(r"\b([A-Z_][A-Za-z0-9_]*)\b", block) 66 | for val in enum_values: 67 | output_lines.append(f"{val}\tLITERAL1") 68 | output_lines.append("") 69 | 70 | with open(output_file, "w") as out_file: 71 | out_file.write("\n".join(output_lines)) 72 | 73 | print(f"[✓] Extracted keywords saved to: {output_file}") 74 | 75 | # Example usage 76 | if __name__ == "__main__": 77 | if len(sys.argv) < 1: 78 | print("Usage: python extract_keywords.py ") 79 | else: 80 | extract_keywords(sys.argv[1]) 81 | -------------------------------------------------------------------------------- /examples/control/control.ino: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #include 34 | 35 | #define BUTTON_PIN 0 36 | #define LED_PIN 2 37 | 38 | ChronosESP32 watch; 39 | // ChronosESP32 watch("Chronos Watch"); // set the bluetooth name 40 | 41 | volatile bool buttonPressed = false; 42 | 43 | void IRAM_ATTR buttonISR() 44 | { 45 | buttonPressed = true; 46 | } 47 | 48 | void connectionCallback(bool state) 49 | { 50 | Serial.print("Connection state: "); 51 | Serial.println(state ? "Connected" : "Disconnected"); 52 | } 53 | 54 | void setup() 55 | { 56 | Serial.begin(115200); 57 | pinMode(BUTTON_PIN, INPUT_PULLUP); 58 | pinMode(LED_PIN, OUTPUT); 59 | 60 | watch.setConnectionCallback(connectionCallback); 61 | watch.begin(); 62 | 63 | Serial.println(watch.getAddress()); // mac address, call after begin() 64 | 65 | watch.setBattery(80); // set the battery level, will be synced to the app 66 | 67 | attachInterrupt(BUTTON_PIN, buttonISR, FALLING); 68 | } 69 | 70 | void loop() 71 | { 72 | watch.loop(); // handles internal routine functions 73 | 74 | digitalWrite(LED_PIN, watch.isConnected()); // use a led to show the connection status 75 | 76 | // String time = watch.getHourC() + watch.getTime(":%M ") + watch.getAmPmC(); 77 | // Serial.println(time); 78 | // delay(5000); 79 | 80 | if (buttonPressed) 81 | { 82 | buttonPressed = false; 83 | // music control, 84 | watch.musicControl(MUSIC_TOGGLE); // MUSIC_PLAY, MUSIC_PAUSE, MUSIC_PREVIOUS, MUSIC_NEXT, MUSIC_TOGGLE, VOLUME_UP, VOLUME_DOWN, VOLUME_MUTE 85 | Serial.println("Sent music command"); 86 | 87 | // watch.setVolume(50); // set the music volume [0-100] (experimental) 88 | 89 | // find phone 90 | // watch.findPhone(true); // true -> ring the phone, false -> stop the ringing, the ringing will also be stopped automatically after 30 seconds; 91 | } 92 | } -------------------------------------------------------------------------------- /examples/camera/camera.ino: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #include 34 | 35 | #define BUTTON_PIN 0 36 | #define LED_PIN 2 37 | 38 | ChronosESP32 watch; 39 | // ChronosESP32 watch("Chronos Watch"); // set the bluetooth name 40 | 41 | volatile bool buttonPressed = false; 42 | 43 | void IRAM_ATTR buttonISR() 44 | { 45 | buttonPressed = true; 46 | } 47 | 48 | void connectionCallback(bool state) 49 | { 50 | Serial.print("Connection state: "); 51 | Serial.println(state ? "Connected" : "Disconnected"); 52 | } 53 | 54 | void configCallback(Config config, uint32_t a, uint32_t b) 55 | { 56 | switch (config) 57 | { 58 | case CF_CAMERA: 59 | // state is saved internally 60 | // bool camera = watch.isCameraReady(); // to access outside the callback 61 | Serial.print("Camera: "); 62 | Serial.println(b ? "Active" : "Inactive"); 63 | break; 64 | } 65 | } 66 | 67 | void setup() 68 | { 69 | Serial.begin(115200); 70 | pinMode(BUTTON_PIN, INPUT_PULLUP); 71 | pinMode(LED_PIN, OUTPUT); 72 | 73 | watch.setConnectionCallback(connectionCallback); 74 | watch.setConfigurationCallback(configCallback); 75 | 76 | watch.begin(); 77 | 78 | Serial.println(watch.getAddress()); // mac address, call after begin() 79 | 80 | watch.setBattery(80); // set the battery level, will be synced to the app 81 | 82 | attachInterrupt(BUTTON_PIN, buttonISR, FALLING); 83 | } 84 | 85 | void loop() 86 | { 87 | watch.loop(); // handles internal routine functions 88 | 89 | digitalWrite(LED_PIN, watch.isCameraReady()); // use a led to show the camera status 90 | 91 | if (buttonPressed) 92 | { 93 | buttonPressed = false; 94 | // capture photo 95 | if (watch.capturePhoto()) 96 | { 97 | Serial.println("Sent capture photo command"); 98 | } 99 | else 100 | { 101 | Serial.println("Camera not ready"); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chronos-esp32 2 | A wrapper library for ESP32 to facilitate easy setup of a smartwatch like project. Supports syncing of notifications from the phone. 3 | Setup your ESP32 as a smartwatch and connect to Chronos app over BLE. 4 | 5 | [![arduino-library-badge](https://www.ardu-badge.com/badge/ChronosESP32.svg?)](https://www.arduinolibraries.info/libraries/chronos-esp32) 6 | [![PlatformIO Registry](https://badges.registry.platformio.org/packages/fbiego/library/ChronosESP32.svg)](https://registry.platformio.org/libraries/fbiego/ChronosESP32) 7 | 8 | 9 | ## Features 10 | 11 | - [x] Time (Auto sync via BLE) 12 | - [x] Notifications (Receive notifications from connected phone) 13 | - [x] Weather (Receive weather info) 14 | - [x] Controls (Music, Find Phone, Camera) 15 | - [x] Phone Battery (Level, Charging state) (Chronos app v3.5.1+) 16 | - [x] Navigation (Chronos app v3.7.5+) 17 | - [x] Contacts & QR codes 18 | - [ ] Alarms 19 | 20 | ## Companion App 21 | 22 | Download Chronos 23 | 24 | [`Chronos`](https://chronos.ke/app?id=esp32) 25 | 26 | ## Functions 27 | 28 | ``` 29 | // library 30 | ChronosESP32(); 31 | ChronosESP32(String name, ChronosScreen screen = CS_240x240_128_CTF); // set the BLE name 32 | void begin(); // initializes BLE server 33 | void stop(bool clearAll = true); // stop the BLE server 34 | void loop(); // handles routine functions 35 | bool isRunning(); // check whether BLE server is inited and running 36 | void setName(String name); // set the BLE name (call before begin) 37 | void setScreen(ChronosScreen screen); // set the screen config (call before begin) 38 | void setChunkedTransfer(bool chunked); 39 | bool isSubscribed(); 40 | 41 | // watch 42 | bool isConnected(); 43 | void set24Hour(bool mode); 44 | bool is24Hour(); 45 | String getAddress(); 46 | void setBattery(uint8_t level, bool charging = false); 47 | bool isCameraReady(); 48 | void syncRequest(); 49 | 50 | // notifications 51 | int getNotificationCount(); 52 | Notification getNotificationAt(int index); 53 | void clearNotifications(); 54 | 55 | // weather 56 | int getWeatherCount(); 57 | String getWeatherCity(); 58 | String getWeatherTime(); 59 | Weather getWeatherAt(int index); 60 | HourlyForecast getForecastHour(int hour); 61 | WeatherLocation getWeatherLocation(); 62 | 63 | // extras 64 | RemoteTouch getTouch(); 65 | String getQrAt(int index); 66 | void setQr(int index, String qr); 67 | 68 | // alarms 69 | Alarm getAlarm(int index); 70 | void setAlarm(int index, Alarm alarm); 71 | bool isAlarmActive(int index); 72 | bool isAlarmActive(Alarm alarm); 73 | bool isAnyAlarmActive(); 74 | 75 | // control 76 | void sendCommand(uint8_t *command, size_t length); 77 | void musicControl(Control command); 78 | void setVolume(uint8_t level); 79 | bool capturePhoto(); 80 | void findPhone(bool state); 81 | 82 | // phone battery status 83 | void setNotifyBattery(bool state); 84 | bool isPhoneCharging(); 85 | uint8_t getPhoneBattery(); 86 | 87 | // app info 88 | int getAppCode(); 89 | String getAppVersion(); 90 | 91 | // navigation 92 | Navigation getNavigation(); 93 | 94 | // contacts 95 | void setContact(int index, Contact contact); 96 | Contact getContact(int index); 97 | int getContactCount(); 98 | Contact getSoSContact(); 99 | void setSOSContactIndex(int index); 100 | int getSOSContactIndex(); 101 | 102 | // helper functions for ESP32Time 103 | int getHourC(); // return hour based on 24-hour variable (0-12 or 0-23) 104 | String getHourZ(); // return zero padded hour string based on 24-hour variable (00-12 or 00-23) 105 | String getAmPmC(bool caps = true); // return (no caps)am/pm or (caps)AM/PM for 12 hour mode or none for 24 hour mode 106 | 107 | // callbacks 108 | void setConnectionCallback(void (*callback)(bool)); 109 | void setNotificationCallback(void (*callback)(Notification)); 110 | void setRingerCallback(void (*callback)(String, bool)); 111 | void setConfigurationCallback(void (*callback)(Config, uint32_t, uint32_t)); 112 | void setDataCallback(void (*callback)(uint8_t *, int)); 113 | void setRawDataCallback(void (*callback)(uint8_t *, int)); 114 | ``` 115 | 116 | ## PlatformIO 117 | 118 | Open the project folder in VS Code with PlatformIO installed to directly run the example sketches. This makes it easier to develop and test features 119 | 120 | ## Dependencies 121 | - [`ESP32Time`](https://github.com/fbiego/ESP32Time) 122 | - [`NimBLE-Arduino`](https://github.com/h2zero/NimBLE-Arduino) 123 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ChronosESP32 KEYWORD1 2 | 3 | begin KEYWORD2 4 | stop KEYWORD2 5 | loop KEYWORD2 6 | isRunning KEYWORD2 7 | setName KEYWORD2 8 | setScreen KEYWORD2 9 | setChunkedTransfer KEYWORD2 10 | isSubscribed KEYWORD2 11 | isConnected KEYWORD2 12 | set24Hour KEYWORD2 13 | is24Hour KEYWORD2 14 | getAddress KEYWORD2 15 | setBattery KEYWORD2 16 | isCameraReady KEYWORD2 17 | syncRequest KEYWORD2 18 | getNotificationCount KEYWORD2 19 | getNotificationAt KEYWORD2 20 | clearNotifications KEYWORD2 21 | getWeatherCount KEYWORD2 22 | getWeatherCity KEYWORD2 23 | getWeatherTime KEYWORD2 24 | getWeatherAt KEYWORD2 25 | getForecastHour KEYWORD2 26 | getWeatherLocation KEYWORD2 27 | getTouch KEYWORD2 28 | getQrAt KEYWORD2 29 | setQr KEYWORD2 30 | getAlarm KEYWORD2 31 | setAlarm KEYWORD2 32 | isAlarmActive KEYWORD2 33 | isAnyAlarmActive KEYWORD2 34 | sendCommand KEYWORD2 35 | musicControl KEYWORD2 36 | setVolume KEYWORD2 37 | capturePhoto KEYWORD2 38 | findPhone KEYWORD2 39 | setNotifyBattery KEYWORD2 40 | isPhoneCharging KEYWORD2 41 | getPhoneBattery KEYWORD2 42 | getAppCode KEYWORD2 43 | getAppVersion KEYWORD2 44 | getNavigation KEYWORD2 45 | setContact KEYWORD2 46 | getContact KEYWORD2 47 | getContactCount KEYWORD2 48 | getSoSContact KEYWORD2 49 | setSOSContactIndex KEYWORD2 50 | getSOSContactIndex KEYWORD2 51 | sendRealtimeSteps KEYWORD2 52 | sendRealtimeHeartRate KEYWORD2 53 | sendRealtimeBloodPressure KEYWORD2 54 | sendRealtimeBloodOxygen KEYWORD2 55 | sendRealtimeHealthData KEYWORD2 56 | sendStepsRecord KEYWORD2 57 | sendHeartRateRecord KEYWORD2 58 | sendBloodPressureRecord KEYWORD2 59 | sendBloodOxygenRecord KEYWORD2 60 | sendSleepRecord KEYWORD2 61 | sendTemperatureRecord KEYWORD2 62 | getHourC KEYWORD2 63 | getHourZ KEYWORD2 64 | getAmPmC KEYWORD2 65 | setConnectionCallback KEYWORD2 66 | setNotificationCallback KEYWORD2 67 | setRingerCallback KEYWORD2 68 | setConfigurationCallback KEYWORD2 69 | setDataCallback KEYWORD2 70 | setRawDataCallback KEYWORD2 71 | setHealthRequestCallback KEYWORD2 72 | 73 | Control LITERAL1 74 | SleepType LITERAL1 75 | Notification LITERAL1 76 | Weather LITERAL1 77 | WeatherLocation LITERAL1 78 | HourlyForecast LITERAL1 79 | ChronosTimer LITERAL1 80 | ChronosData LITERAL1 81 | Alarm LITERAL1 82 | Setting LITERAL1 83 | RemoteTouch LITERAL1 84 | Navigation LITERAL1 85 | Contact LITERAL1 86 | DateTime LITERAL1 87 | Config LITERAL1 88 | HealthRequest LITERAL1 89 | ChronosScreen LITERAL1 90 | 91 | MUSIC_PLAY LITERAL1 92 | MUSIC_PAUSE LITERAL1 93 | MUSIC_PREVIOUS LITERAL1 94 | MUSIC_NEXT LITERAL1 95 | MUSIC_TOGGLE LITERAL1 96 | VOLUME_UP LITERAL1 97 | VOLUME_DOWN LITERAL1 98 | VOLUME_MUTE LITERAL1 99 | 100 | SLEEP_AWAKE LITERAL1 101 | SLEEP_LIGHT LITERAL1 102 | SLEEP_DEEP LITERAL1 103 | 104 | CF_TIME LITERAL1 105 | CF_RTW LITERAL1 106 | CF_HR24 LITERAL1 107 | CF_LANG LITERAL1 108 | CF_RST LITERAL1 109 | CF_CLR LITERAL1 110 | CF_HOURLY LITERAL1 111 | CF_FIND LITERAL1 112 | CF_USER LITERAL1 113 | CF_ALARM LITERAL1 114 | CF_FONT LITERAL1 115 | CF_SED LITERAL1 116 | CF_SLEEP LITERAL1 117 | CF_QUIET LITERAL1 118 | CF_WATER LITERAL1 119 | CF_WEATHER LITERAL1 120 | CF_CAMERA LITERAL1 121 | CF_PBAT LITERAL1 122 | CF_APP LITERAL1 123 | CF_QR LITERAL1 124 | CF_NAV_DATA LITERAL1 125 | CF_NAV_ICON LITERAL1 126 | CF_CONTACT LITERAL1 127 | CF_SYNCED LITERAL1 128 | 129 | HR_STEPS_RECORDS LITERAL1 130 | HR_SLEEP_RECORDS LITERAL1 131 | HR_HEART_RATE_MEASURE LITERAL1 132 | HR_BLOOD_OXYGEN_MEASURE LITERAL1 133 | HR_BLOOD_PRESSURE_MEASURE LITERAL1 134 | HR_MEASURE_ALL LITERAL1 135 | 136 | CS_0x0_000_CFF LITERAL1 137 | CS_240x240_130_STF LITERAL1 138 | CS_240x240_130_STT LITERAL1 139 | CS_80x160_096_RTF LITERAL1 140 | CS_80x160_096_RTT LITERAL1 141 | CS_135x240_114_RTF LITERAL1 142 | CS_135x240_114_RTT LITERAL1 143 | CS_240x240_128_CTF LITERAL1 144 | CS_240x240_128_CTT LITERAL1 145 | CS_240x288_157_RTF LITERAL1 146 | CS_240x288_157_RTT LITERAL1 147 | CS_240x283_172_RTF LITERAL1 148 | CS_240x283_172_RTT LITERAL1 149 | CS_360x360_130_CTF LITERAL1 150 | CS_360x360_130_CTT LITERAL1 151 | CS_320x380_177_RTF LITERAL1 152 | CS_320x380_177_RTT LITERAL1 153 | CS_320x385_175_RTF LITERAL1 154 | CS_320x385_175_RTT LITERAL1 155 | CS_320x360_160_RTF LITERAL1 156 | CS_320x360_160_RTT LITERAL1 157 | CS_240x296_191_RTF LITERAL1 158 | CS_240x296_191_RTT LITERAL1 159 | CS_412x412_145_CTF LITERAL1 160 | CS_412x412_145_CTT LITERAL1 161 | CS_410x494_200_RTF LITERAL1 162 | CS_410x494_200_RTT LITERAL1 163 | CS_466x466_143_CTF LITERAL1 164 | CS_466x466_143_CTT LITERAL1 165 | CF_WATCHY_200x200 LITERAL1 166 | CF_ESP32_240x240 LITERAL1 167 | CF_ESP32_466x466 LITERAL1 168 | CF_WEBSCREEN_536x240 LITERAL1 169 | CF_WAVESHARE_410x502 LITERAL1 170 | CF_ZSWATCH_240x240 LITERAL1 171 | -------------------------------------------------------------------------------- /examples/navigation/navigation.ino: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #include 34 | 35 | ChronosESP32 watch("Chronos Nav"); // set the bluetooth name 36 | 37 | bool change = false; 38 | uint32_t nav_crc = 0xFFFFFFFF; 39 | 40 | void connectionCallback(bool state) 41 | { 42 | Serial.print("Connection state: "); 43 | Serial.println(state ? "Connected" : "Disconnected"); 44 | } 45 | 46 | void notificationCallback(Notification notification) 47 | { 48 | Serial.print("Notification received at "); 49 | Serial.println(notification.time); 50 | Serial.print("From: "); 51 | Serial.print(notification.app); 52 | Serial.print("\tIcon: "); 53 | Serial.println(notification.icon); 54 | Serial.println(notification.title); 55 | Serial.println(notification.message); 56 | } 57 | 58 | void configCallback(Config config, uint32_t a, uint32_t b) 59 | { 60 | switch (config) 61 | { 62 | case CF_NAV_DATA: 63 | Serial.print("Navigation state: "); 64 | Serial.println(a ? "Active" : "Inactive"); 65 | change = true; 66 | if (a) 67 | { 68 | Navigation nav = watch.getNavigation(); 69 | Serial.println(nav.directions); 70 | Serial.println(nav.eta); 71 | Serial.println(nav.duration); 72 | Serial.println(nav.distance); 73 | Serial.println(nav.title); 74 | Serial.println(nav.speed); 75 | } 76 | break; 77 | case CF_NAV_ICON: 78 | Serial.print("Navigation Icon data, position: "); 79 | Serial.println(a); 80 | Serial.print("Icon CRC: "); 81 | Serial.printf("0x%04X\n", b); 82 | if (a == 2){ 83 | Navigation nav = watch.getNavigation(); 84 | if (nav_crc != nav.iconCRC) 85 | { 86 | nav_crc = nav.iconCRC; 87 | 88 | for (int y = 0; y < 50; y++) { Serial.print("-"); } // draw top border 89 | Serial.println(); 90 | 91 | for (int y = 0; y < 48; y++) 92 | { 93 | Serial.print("|"); // draw left border 94 | for (int x = 0; x < 48; x++) 95 | { 96 | int byte_index = (y * 48 + x) / 8; 97 | int bit_pos = 7 - (x % 8); 98 | bool px_on = (nav.icon[byte_index] >> bit_pos) & 0x01; 99 | // example to draw a pixel on a TFT display 100 | // tft.drawPixel(x, y, px_on ? TFT_WHITE : TFT_BLACK); 101 | Serial.print(px_on ? "X" : " "); 102 | } 103 | Serial.println("|"); // draw right border 104 | } 105 | for (int y = 0; y < 50; y++) { Serial.print("-"); } // draw bottom border 106 | Serial.println(); 107 | } 108 | } 109 | break; 110 | } 111 | } 112 | 113 | void setup() 114 | { 115 | Serial.begin(115200); 116 | 117 | // set the callbacks before calling begin funtion 118 | watch.setConnectionCallback(connectionCallback); 119 | watch.setNotificationCallback(notificationCallback); 120 | watch.setConfigurationCallback(configCallback); 121 | 122 | watch.begin(); // initializes the BLE 123 | // make sure the ESP32 is not paired with your phone in the bluetooth settings 124 | // go to Chronos app > Watches tab > Watches button > Pair New Devices > Search > Select your board 125 | // you only need to do it once. To disconnect, click on the rotating icon (Top Right) 126 | 127 | Serial.println(watch.getAddress()); // mac address, call after begin() 128 | 129 | watch.setBattery(80); // set the battery level, will be synced to the app 130 | } 131 | 132 | void loop() 133 | { 134 | watch.loop(); // handles internal routine functions 135 | 136 | // if (change){ 137 | // change = false; 138 | 139 | // Navigation nav = watch.getNavigation(); 140 | // if (nav.active){ 141 | // Serial.println(nav.directions); 142 | // Serial.println(nav.eta); 143 | // Serial.println(nav.duration); 144 | // Serial.println(nav.distance); 145 | // Serial.println(nav.title); 146 | // Serial.println(nav.speed); 147 | // } 148 | // } 149 | } -------------------------------------------------------------------------------- /examples/health/health.ino: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2025 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #include 34 | 35 | ChronosESP32 watch("Chronos Health"); // set the bluetooth name 36 | 37 | bool send_health = false; // flag to send health data 38 | bool send_sleep = false; // flag to send sleep data 39 | 40 | HealthRequest measureType; 41 | bool measure_run = false; 42 | 43 | void connectionCallback(bool state) 44 | { 45 | Serial.print("Connection state: "); 46 | Serial.println(state ? "Connected" : "Disconnected"); 47 | } 48 | 49 | void healthRequestCallback(HealthRequest request, bool state) 50 | { 51 | 52 | switch (request) 53 | { 54 | case HR_STEPS_RECORDS: 55 | { 56 | Serial.println("Steps records request"); 57 | send_health = true; 58 | } 59 | break; 60 | case HR_SLEEP_RECORDS: 61 | Serial.println("Sleep records request"); 62 | send_sleep = true; 63 | break; 64 | case HR_MEASURE_ALL: 65 | case HR_HEART_RATE_MEASURE: 66 | case HR_BLOOD_OXYGEN_MEASURE: 67 | case HR_BLOOD_PRESSURE_MEASURE: 68 | measureType = request; 69 | measure_run = state; 70 | Serial.print("Health realtime request "); 71 | Serial.print(measureType); 72 | Serial.print("\t"); 73 | Serial.println(state ? "Start" : "Stop"); 74 | break; 75 | } 76 | } 77 | 78 | void setup() 79 | { 80 | Serial.begin(115200); 81 | 82 | // set the callbacks before calling begin funtion 83 | watch.setConnectionCallback(connectionCallback); 84 | watch.setHealthRequestCallback(healthRequestCallback); 85 | 86 | watch.begin(); // initializes the BLE 87 | // make sure the ESP32 is not paired with your phone in the bluetooth settings 88 | // go to Chronos app > Watches tab > Watches button > Pair New Devices > Search > Select your board 89 | // you only need to do it once. To disconnect, click on the rotating icon (Top Right) 90 | 91 | Serial.println(watch.getAddress()); // mac address, call after begin() 92 | 93 | watch.setBattery(80); // set the battery level, will be synced to the app 94 | 95 | watch.set24Hour(true); // the 24 hour mode will be overwritten when the command is received from the app 96 | } 97 | 98 | void loop() 99 | { 100 | watch.loop(); // handles internal routine functions 101 | 102 | String time = watch.getHourC() + watch.getTime(":%M ") + watch.getAmPmC(); 103 | Serial.println(time); 104 | delay(5000); 105 | 106 | // update steps and calories when values changes 107 | // watch.sendRealtimeSteps(steps, calories); // send realtime steps & calories 108 | 109 | if (send_health) 110 | { 111 | // send health data 112 | uint32_t steps = random(1000, 3000); 113 | uint32_t calories = random(500, 800); 114 | watch.sendRealtimeSteps(steps, calories); // send realtime steps & calories 115 | Serial.printf("Steps: %d, Calories: %d\n", steps, calories); 116 | 117 | // steps and calories are grouped by hour. They are also cumulative throughout the day. 118 | // ie if steps at 10am is 100 and 11am is 200, (the 11am record will be previous + current 100 + 200 = 300) 119 | watch.sendStepsRecord(204, 23, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear(), 72, 98, 120, 82); // send steps records 120 | watch.sendStepsRecord(646, 45, 14, watch.getDay(), watch.getMonth() + 1, watch.getYear(), 73, 99, 126, 78); // send steps records 121 | watch.sendStepsRecord(2345, 69, 14, watch.getDay(), watch.getMonth() + 1, watch.getYear(), 76, 96, 110, 70); // send steps records 122 | watch.sendStepsRecord(5654, 124, 15, watch.getDay(), watch.getMonth() + 1, watch.getYear(), 75, 97, 114, 76); // send steps records 123 | 124 | // heart rate records 125 | watch.sendHeartRateRecord(78, 30, 11, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 126 | watch.sendHeartRateRecord(82, 5, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 127 | watch.sendHeartRateRecord(86, 20, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 128 | 129 | // blood pressure records 130 | watch.sendBloodPressureRecord(112, 76, 30, 11, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 131 | watch.sendBloodPressureRecord(120, 84, 5, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 132 | watch.sendBloodPressureRecord(118, 79, 20, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 133 | 134 | // blood oxygen records 135 | watch.sendBloodOxygenRecord(96, 30, 11, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 136 | watch.sendBloodOxygenRecord(98, 5, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 137 | watch.sendBloodOxygenRecord(97, 20, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 138 | 139 | // temperature records 140 | watch.sendTemperatureRecord(35.6, 30, 11, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 141 | watch.sendTemperatureRecord(37.2, 5, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 142 | watch.sendTemperatureRecord(37.6, 20, 12, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 143 | 144 | send_health = false; 145 | } 146 | 147 | if (send_sleep) 148 | { 149 | // sleep records sample 150 | watch.sendSleepRecord(90, SLEEP_LIGHT, 0, 21, watch.getDay() - 1, watch.getMonth() + 1, watch.getYear()); 151 | watch.sendSleepRecord(330, SLEEP_DEEP, 30, 22, watch.getDay() - 1, watch.getMonth() + 1, watch.getYear()); 152 | watch.sendSleepRecord(60, SLEEP_LIGHT, 0, 4, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 153 | watch.sendSleepRecord(120, SLEEP_DEEP, 0, 5, watch.getDay(), watch.getMonth() + 1, watch.getYear()); 154 | 155 | send_sleep = false; 156 | } 157 | 158 | if (measure_run) 159 | { 160 | // simulate running measurements, ideally the values would be retrieved from a sensor 161 | uint8_t hr = random(72, 84); 162 | uint8_t sp = random(95, 100); 163 | uint8_t bpH = random(110, 130); 164 | uint8_t bpL = random(75, 95); 165 | 166 | switch (measureType) 167 | { 168 | case HR_MEASURE_ALL: 169 | watch.sendRealtimeHealthData(hr, sp, bpH, bpL); 170 | break; 171 | case HR_HEART_RATE_MEASURE: 172 | watch.sendRealtimeHeartRate(hr); 173 | break; 174 | case HR_BLOOD_OXYGEN_MEASURE: 175 | watch.sendRealtimeBloodOxygen(sp); 176 | break; 177 | case HR_BLOOD_PRESSURE_MEASURE: 178 | watch.sendRealtimeBloodPressure(bpH, bpL); 179 | break; 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /examples/watch/watch.ino: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #include 34 | 35 | #define LED_PIN 2 36 | 37 | ChronosESP32 watch; 38 | // ChronosESP32 watch("Chronos Watch"); // set the bluetooth name 39 | // ChronosESP32 watch("Chronos Watch", CS_360x360_130_CTF); // set the bluetooth name and screen configuration 40 | 41 | void connectionCallback(bool state) 42 | { 43 | Serial.print("Connection state: "); 44 | Serial.println(state ? "Connected" : "Disconnected"); 45 | // bool connected = watch.isConnected(); 46 | } 47 | 48 | void notificationCallback(Notification notification) 49 | { 50 | Serial.print("Notification received at "); 51 | Serial.println(notification.time); 52 | Serial.print("From: "); 53 | Serial.print(notification.app); 54 | Serial.print("\tIcon: "); 55 | Serial.println(notification.icon); 56 | Serial.println(notification.title); 57 | Serial.println(notification.message); 58 | // see loop on how to access notifications 59 | } 60 | 61 | void ringerCallback(String caller, bool state) 62 | { 63 | if (state) 64 | { 65 | Serial.print("Ringer: Incoming call from "); 66 | Serial.println(caller); 67 | } 68 | else 69 | { 70 | Serial.println("Ringer dismissed"); 71 | } 72 | } 73 | 74 | void configCallback(Config config, uint32_t a, uint32_t b) 75 | { 76 | switch (config) 77 | { 78 | case CF_TIME: 79 | // time is saved internally 80 | // command with no parameters 81 | Serial.println("The time has been set"); 82 | Serial.println(watch.getTimeDate()); 83 | break; 84 | case CF_RTW: 85 | // state not saved internally 86 | Serial.print("Raise to wake: "); 87 | Serial.println(b ? "ON" : "OFF"); 88 | break; 89 | case CF_RST: 90 | // command with no parameters 91 | Serial.println("Reset request"); 92 | break; 93 | case CF_FIND: 94 | // command with no parameters 95 | Serial.println("Find request"); 96 | break; 97 | case CF_FONT: 98 | // state not saved internally 99 | Serial.print("Font settings: Color "); 100 | Serial.printf("0x%06X", a); 101 | Serial.print("\tStyle: "); 102 | Serial.print((b >> 16) & 0xFFFF); 103 | Serial.print("\tPosition: "); 104 | Serial.println(b & 0xFFFF); 105 | break; 106 | case CF_ALARM: 107 | // alarms are saved on the alarms 108 | // see loop for how to access alarms 109 | Serial.print("Alarm: Index "); 110 | Serial.print(a); 111 | Serial.print("\tTime: "); 112 | Serial.print(uint8_t(b >> 24)); 113 | Serial.print(":"); 114 | Serial.print(uint8_t(b >> 16)); 115 | Serial.print("\tRepeat: "); 116 | Serial.print(uint8_t(b >> 8)); 117 | Serial.print("\tEnabled: "); 118 | Serial.println((uint8_t(b)) ? "ON" : "OFF"); 119 | break; 120 | case CF_QUIET: 121 | // state not saved internally 122 | Serial.print("Quiet hours: "); 123 | Serial.print(a ? "ON" : "OFF"); 124 | Serial.print("\tTime: "); 125 | Serial.print(uint8_t(b >> 24)); 126 | Serial.print(":"); 127 | Serial.print(uint8_t(b >> 16)); 128 | Serial.print("\tto\t"); 129 | Serial.print(uint8_t(b >> 8)); 130 | Serial.print(":"); 131 | Serial.println((uint8_t(b))); 132 | break; 133 | case CF_SLEEP: 134 | // state not saved internally 135 | Serial.print("Sleep time: "); 136 | Serial.print(a ? "ON" : "OFF"); 137 | Serial.print("\tTime: "); 138 | Serial.print(uint8_t(b >> 24)); 139 | Serial.print(":"); 140 | Serial.print(uint8_t(b >> 16)); 141 | Serial.print("\tto\t"); 142 | Serial.print(uint8_t(b >> 8)); 143 | Serial.print(":"); 144 | Serial.println((uint8_t(b))); 145 | break; 146 | case CF_SED: 147 | // state not saved internally 148 | Serial.print("Sedentary time: "); 149 | Serial.print((a & 0xFF) ? "ON" : "OFF"); 150 | Serial.print("\tInterval: "); 151 | Serial.print(((a >> 16) & 0xFFFF)); 152 | Serial.print("\tTime: "); 153 | Serial.print(uint8_t(b >> 24)); 154 | Serial.print(":"); 155 | Serial.print(uint8_t(b >> 16)); 156 | Serial.print("\tto\t"); 157 | Serial.print(uint8_t(b >> 8)); 158 | Serial.print(":"); 159 | Serial.println((uint8_t(b))); 160 | break; 161 | case CF_WATER: 162 | // state not saved internally 163 | Serial.print("Drink water reminder: "); 164 | Serial.print((a & 0xFF) ? "ON" : "OFF"); 165 | Serial.print("\tInterval: "); 166 | Serial.print(((a >> 16) & 0xFFFF)); 167 | Serial.print("\tTime: "); 168 | Serial.print(uint8_t(b >> 24)); 169 | Serial.print(":"); 170 | Serial.print(uint8_t(b >> 16)); 171 | Serial.print("\tto\t"); 172 | Serial.print(uint8_t(b >> 8)); 173 | Serial.print(":"); 174 | Serial.println((uint8_t(b))); 175 | break; 176 | case CF_USER: 177 | // state not saved internally 178 | Serial.print("User info: Age: "); 179 | Serial.print(uint8_t(a >> 24)); 180 | Serial.print("\tHeight: "); 181 | Serial.print(uint8_t(a >> 16)); 182 | Serial.print("\tWeight: "); 183 | Serial.print(uint8_t(a >> 8)); 184 | Serial.print("\tStep length: "); 185 | Serial.print(uint8_t(a)); 186 | Serial.print("\tUnits: "); 187 | Serial.print(uint8_t(b >> 24) ? "Metric" : "Imperial"); 188 | Serial.print("\tTarget steps: "); 189 | Serial.print(uint8_t(b >> 16) * 1000); 190 | Serial.print("\tTemp: "); 191 | Serial.println(uint8_t(b >> 8) ? "°F" : "°C"); 192 | break; 193 | case CF_HOURLY: 194 | // state not saved internally 195 | Serial.print("Hourly measurement: "); 196 | Serial.println(b ? "ON" : "OFF"); 197 | break; 198 | case CF_HR24: 199 | // state is saved internally 200 | // bool hr24 = watch.is24Hour(); // to access outside the callback 201 | Serial.print("24 hour mode: "); 202 | Serial.println(b ? "ON" : "OFF"); 203 | break; 204 | case CF_CAMERA: 205 | // state is saved internally 206 | // bool camera = watch.isCameraReady(); // to access outside the callback 207 | Serial.print("Camera: "); 208 | Serial.println(b ? "Active" : "Inactive"); 209 | break; 210 | case CF_LANG: 211 | // state not saved internally 212 | Serial.print("Language: "); 213 | Serial.println(b); 214 | break; 215 | case CF_PBAT: 216 | // state is saved internally 217 | Serial.print("Phone battery: "); 218 | Serial.println(a == 1 ? "Charging" : "Not Charging"); // bool state = watch.isPhoneCharging(); 219 | Serial.print("Level: "); 220 | Serial.print(b); // uint8_t level = watch.getPhoneBattery(); 221 | Serial.println("%"); 222 | break; 223 | case CF_APP: 224 | // state is saved internally 225 | Serial.print("Chronos App; Code: "); 226 | Serial.print(a); // int code = watch.getAppCode(); 227 | Serial.print(" Version: "); 228 | Serial.println(watch.getAppVersion()); 229 | break; 230 | case CF_QR: 231 | // qr links 232 | if (a == 0){ 233 | // individual qr links (b is the index) 234 | Serial.print("QR code: "); 235 | Serial.println(watch.getQrAt(b)); 236 | } 237 | if (a == 1) 238 | { 239 | // end of qr links transmission 240 | Serial.print("QR Links received. Count: "); 241 | Serial.println(b); 242 | } 243 | break; 244 | case CF_WEATHER: 245 | // weather is saved 246 | Serial.println("Weather received"); 247 | if (a) 248 | { 249 | // if a == 1, high & low temperature values might not yet be updated 250 | if (a == 2) 251 | { 252 | int n = watch.getWeatherCount(); 253 | String updateTime = watch.getWeatherTime(); 254 | Serial.print("Weather Count: "); 255 | Serial.print(n); 256 | Serial.print("\tUpdated at: "); 257 | Serial.println(updateTime); 258 | 259 | for (int i = 0; i < n; i++) 260 | { 261 | // iterate through weather forecast, index 0 is today, 1 tomorrow...etc 262 | Weather w = watch.getWeatherAt(i); 263 | Serial.print("Day:"); // day of the week (0 - 6) 264 | Serial.print(w.day); 265 | Serial.print("\tIcon:"); 266 | Serial.print(w.icon); 267 | Serial.print("\t"); 268 | Serial.print(w.temp); 269 | Serial.print("°C"); 270 | Serial.print("\tHigh:"); 271 | Serial.print(w.high); 272 | Serial.print("°C"); 273 | Serial.print("\tLow:"); 274 | Serial.print(w.low); 275 | Serial.println("°C"); 276 | if (i == 0) 277 | { 278 | Serial.print("Pressure: "); 279 | Serial.print(w.pressure); 280 | Serial.print("\tUV: "); 281 | Serial.println(w.uv); 282 | } 283 | } 284 | } 285 | } 286 | if (b) 287 | { 288 | Serial.print("City name: "); 289 | String city = watch.getWeatherCity(); // 290 | Serial.println(city); 291 | 292 | WeatherLocation loc = watch.getWeatherLocation(); 293 | Serial.print("City: "); 294 | Serial.println(loc.city); 295 | Serial.print("Region: "); 296 | Serial.println(loc.region); 297 | Serial.print("Country: "); 298 | Serial.println(loc.country); 299 | Serial.print("Coordinates -> Lat: "); 300 | Serial.print(loc.latitude, 6); 301 | Serial.print("\tLon: "); 302 | Serial.println(loc.longitude, 6); 303 | } 304 | Serial.println(); 305 | break; 306 | case CF_CONTACT: 307 | if (a == 0){ 308 | Serial.println("Receiving contacts"); 309 | Serial.print("SOS index: "); 310 | Serial.print(uint8_t(b >> 8)); 311 | Serial.print("\tSize: "); 312 | Serial.println(uint8_t(b)); 313 | } 314 | if (a == 1){ 315 | Serial.println("Received all contacts"); 316 | int n = uint8_t(b); // contacts size -> watch.getContactCount(); 317 | int s = uint8_t(b >> 8); // sos contact index -> watch.getSOSContactIndex(); 318 | for (int i = 0; i < n; i++) 319 | { 320 | Contact cn = watch.getContact(i); 321 | Serial.print("Name: "); 322 | Serial.print(cn.name); 323 | Serial.print(s == i ? " [SOS]" : ""); 324 | Serial.print("\tNumber: "); 325 | Serial.println(cn.number); 326 | } 327 | } 328 | break; 329 | } 330 | } 331 | 332 | void dataCallback(uint8_t *data, int length) 333 | { 334 | Serial.println("Received Data"); 335 | for (int i = 0; i < length; i++) 336 | { 337 | Serial.printf("%02X ", data[i]); 338 | } 339 | Serial.println(); 340 | } 341 | 342 | void setup() 343 | { 344 | Serial.begin(115200); 345 | 346 | pinMode(LED_PIN, OUTPUT); 347 | 348 | // set the callbacks before calling begin funtion 349 | watch.setConnectionCallback(connectionCallback); 350 | watch.setNotificationCallback(notificationCallback); 351 | watch.setRingerCallback(ringerCallback); 352 | watch.setConfigurationCallback(configCallback); 353 | watch.setDataCallback(dataCallback); 354 | 355 | watch.begin(); // initializes the BLE 356 | // make sure the ESP32 is not paired with your phone in the bluetooth settings 357 | // go to Chronos app > Watches tab > Watches button > Pair New Devices > Search > Select your board 358 | // you only need to do it once. To disconnect, click on the rotating icon (Top Right) 359 | 360 | Serial.println(watch.getAddress()); // mac address, call after begin() 361 | 362 | watch.setBattery(80); // set the battery level, will be synced to the app 363 | 364 | // watch.clearNotifications(); // clear the default notification (Chronos app install text) 365 | 366 | watch.set24Hour(true); // the 24 hour mode will be overwritten when the command is received from the app 367 | // this modifies the return of the functions below 368 | watch.getAmPmC(true); // 12 hour mode true->(am/pm), false->(AM/PM), if 24 hour mode returns empty string ("") 369 | watch.getHourC(); // (0-12), (0-23) 370 | watch.getHourZ(); // zero padded hour (00-12), (00-23) 371 | watch.is24Hour(); // resturns whether in 24 hour mode 372 | // watch.setNotifyBattery(false); // whether to enable or disable receiving phone battery status (enabled by default) 373 | } 374 | 375 | void loop() 376 | { 377 | watch.loop(); // handles internal routine functions 378 | 379 | // watch.setBattery(85, true); // set the battery level and charging state 380 | 381 | String time = watch.getHourC() + watch.getTime(":%M ") + watch.getAmPmC(); 382 | Serial.println(time); 383 | delay(500); 384 | 385 | /* 386 | // access available notifications 387 | int count = watch.getNotificationCount(); 388 | Serial.print("Count: "); 389 | Serial.println(count); 390 | for (int i = 0; i < count; i++) 391 | { 392 | // iterate through available notifications, index 0 is the latest received notification 393 | Notification n = watch.getNotificationAt(i); 394 | Serial.print("Notification received at "); 395 | Serial.println(n.time); 396 | Serial.print("From: "); 397 | Serial.print(n.app); 398 | Serial.print("\t Icon->"); 399 | Serial.println(n.icon); 400 | Serial.println(n.message); 401 | } 402 | watch.clearNotifications(); // watch.getNotificationCount() will return zero, 403 | // notifications in the buffer are still accessible using watch.getNotificationAt(i) 404 | */ 405 | 406 | /* 407 | // read the alarms, 8 available 408 | // the alarms are only stored as received from the app 409 | for (int j = 0; j < 8; j++){ 410 | Alarm a = watch.getAlarm(j); 411 | Serial.print("Alarm: "); 412 | Serial.print(j + 1); 413 | Serial.print("\tTime: "); 414 | Serial.print(a.hour); 415 | Serial.print(":"); 416 | Serial.print(a.minute); 417 | Serial.print("\tState: "); 418 | Serial.println(a.enabled ? "Enabled": "Disabled"); 419 | } 420 | 421 | // you need to save alarms after receiving them from the app and restore them during setup 422 | // otherwise they will be lost when the esp32 is restarted 423 | Alarm a1; 424 | a1.hour = 7; 425 | a1.minute = 30; 426 | a1.repeat = 0b0111110; // repeat from Monday to Friday 427 | a1.enabled = true; 428 | watch.setAlarm(0, a1); // save alarm at index 0 429 | 430 | watch.isAlarmActive(0); // check if alarm at index 0 is active 431 | watch.isAlarmActive(watch.getAlarm(0)); // check if a specific alarm is active 432 | watch.isAnyAlarmActive(); // check if any alarm is active 433 | */ 434 | 435 | digitalWrite(LED_PIN, watch.isAnyAlarmActive()); 436 | 437 | /* // access weather forecast details 438 | int n = watch.getWeatherCount(); 439 | String updateTime = watch.getWeatherTime(); 440 | Serial.print("Weather Count: "); 441 | Serial.print(n); 442 | Serial.print("\tUpdated at: "); 443 | Serial.println(updateTime); 444 | 445 | for (int i = 0; i < n; i++) 446 | { 447 | // iterate through weather forecast, index 0 is today, 1 tomorrow...etc 448 | Weather w = watch.getWeatherAt(i); 449 | Serial.print("Day:"); // day of the week (0 - 6) 450 | Serial.print(w.day); 451 | Serial.print("\tIcon: "); 452 | Serial.print(w.icon); 453 | Serial.print("\t"); 454 | Serial.print(w.temp); 455 | Serial.print("°C"); 456 | Serial.print("\tHigh: "); 457 | Serial.print(w.high); 458 | Serial.print("°C"); 459 | Serial.print("\tLow: "); 460 | Serial.print(w.low); 461 | Serial.println("°C"); 462 | } 463 | */ 464 | } -------------------------------------------------------------------------------- /src/ChronosESP32.h: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #ifndef CHRONOSESP32_H 34 | #define CHRONOSESP32_H 35 | 36 | #include 37 | #include 38 | #include 39 | 40 | #define CHRONOSESP_VERSION_MAJOR 1 41 | #define CHRONOSESP_VERSION_MINOR 9 42 | #define CHRONOSESP_VERSION_PATCH 0 43 | 44 | #define CHRONOSESP_VERSION F(CHRONOSESP_VERSION_MAJOR "." CHRONOSESP_VERSION_MINOR "." CHRONOSESP_VERSION_PATCH) 45 | 46 | #define NOTIF_SIZE 10 47 | #define WEATHER_SIZE 7 48 | #define ALARM_SIZE 8 49 | #define DATA_SIZE 512 50 | #define FORECAST_SIZE 24 51 | #define QR_SIZE 9 52 | #define ICON_SIZE 48 53 | #define ICON_DATA_SIZE (ICON_SIZE * ICON_SIZE) / 8 54 | #define CONTACTS_SIZE 255 55 | 56 | #define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" 57 | #define CHARACTERISTIC_UUID_RX "6e400002-b5a3-f393-e0a9-e50e24dcca9e" 58 | #define CHARACTERISTIC_UUID_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e" 59 | 60 | enum Control 61 | { 62 | MUSIC_PLAY = 0x9D00, 63 | MUSIC_PAUSE = 0x9D01, 64 | MUSIC_PREVIOUS = 0x9D02, 65 | MUSIC_NEXT = 0x9D03, 66 | MUSIC_TOGGLE = 0x9900, 67 | 68 | VOLUME_UP = 0x99A1, 69 | VOLUME_DOWN = 0x99A2, 70 | VOLUME_MUTE = 0x99A3, 71 | }; 72 | 73 | enum SleepType 74 | { 75 | SLEEP_AWAKE = 0, 76 | SLEEP_LIGHT = 1, 77 | SLEEP_DEEP = 2, 78 | }; 79 | 80 | struct Notification 81 | { 82 | int icon; 83 | String app; 84 | String time; 85 | String title; 86 | String message; 87 | }; 88 | 89 | struct Weather 90 | { 91 | int icon; 92 | int day; 93 | int temp; 94 | int high; 95 | int low; 96 | int pressure; 97 | int uv; 98 | }; 99 | 100 | struct WeatherLocation 101 | { 102 | String city; 103 | String region; 104 | String country; 105 | float latitude; 106 | float longitude; 107 | }; 108 | 109 | struct HourlyForecast 110 | { 111 | int day; // day of forecast 112 | int hour; // hour of the forecast 113 | int icon; 114 | int temp; // 115 | int uv; // uv index 116 | int humidity; // % 117 | int wind; // wind speed km/h 118 | }; 119 | 120 | struct ChronosTimer 121 | { 122 | unsigned long time; 123 | long duration = 5000; 124 | bool active; 125 | }; 126 | 127 | struct ChronosData 128 | { 129 | int length; 130 | uint8_t data[DATA_SIZE]; 131 | }; 132 | 133 | struct Alarm 134 | { 135 | uint8_t hour; 136 | uint8_t minute; 137 | uint8_t repeat; 138 | bool enabled; 139 | }; 140 | 141 | struct Setting 142 | { 143 | uint8_t hour; 144 | uint8_t minute; 145 | uint8_t repeat; 146 | bool enabled; 147 | }; 148 | 149 | struct RemoteTouch 150 | { 151 | bool state; 152 | uint32_t x; 153 | uint32_t y; 154 | }; 155 | 156 | struct Navigation 157 | { 158 | bool active = false; // whether running or not 159 | bool isNavigation = false; // navigation or general info 160 | bool hasIcon = false; // icon present in the navigation data 161 | String distance; // distance to destination 162 | String duration; // time to destination 163 | String eta; // estimated time of arrival (time,date) 164 | String title; // distance to next point or title 165 | String directions; // place info ie current street name/ instructions 166 | String speed; // speed (available via OsmAnd app) 167 | uint8_t icon[ICON_DATA_SIZE]; // navigation icon 48x48 (1bpp) 168 | uint32_t iconCRC; // to identify whether the icon has changed 169 | }; 170 | 171 | struct Contact 172 | { 173 | String name; 174 | String number; 175 | }; 176 | 177 | struct DateTime 178 | { 179 | uint8_t second; 180 | uint8_t minute; 181 | uint8_t hour; 182 | uint8_t day; 183 | uint8_t month; 184 | uint32_t year; 185 | }; 186 | 187 | enum Config 188 | { 189 | CF_TIME = 0, // time - (a 0 = before, 1 = after setting) 190 | CF_RTW, // raise to wake - 191 | CF_HR24, // 24 hour mode - 192 | CF_LANG, // watch language - 193 | CF_RST, // watch reset - 194 | CF_CLR, // watch clear data 195 | CF_HOURLY, // hour measurement - 196 | CF_FIND, // find watch - 197 | CF_USER, // user details (age)(height)(weight)(step length)(target)(units[]) 198 | CF_ALARM, // alarm (index)(hour) (minute) (enabled) (repeat) - 199 | CF_FONT, // font settings (color[3])(b1+b2) - 200 | CF_SED, // sedentary (hour)(minute)(hour)(minute)(interval)(enabled) - 201 | CF_SLEEP, // sleep time (hour)(minute)(hour)(minute)(enabled) - 202 | CF_QUIET, // quiet hours (hour)(minute)(hour)(minute)(enabled) - 203 | CF_WATER, // water reminder (hour)(minute)(hour)(minute)(interval)(enabled)- 204 | CF_WEATHER, // weather config (a Weekly) (b City Name) - 205 | CF_CAMERA, // camera config, (ready state) 206 | CF_PBAT, // phone battery ([a] isPhoneCharing, [b] phoneBatteryLevel) 207 | CF_APP, // app version info 208 | CF_QR, // qr codes received 209 | CF_NAV_DATA, // navigation data received 210 | CF_NAV_ICON, // navigation icon received 211 | CF_CONTACT, // contacts data received 212 | CF_SYNCED, // data sync completed (esp32 can go to sleep if needed) 213 | }; 214 | 215 | enum HealthRequest 216 | { 217 | HR_STEPS_RECORDS = 0, // app is requesting step records 218 | HR_SLEEP_RECORDS, // app is requesting sleep records 219 | 220 | HR_HEART_RATE_MEASURE, // app has started heart rate measurement 221 | HR_BLOOD_OXYGEN_MEASURE, // app has started blood oxygen measurement 222 | HR_BLOOD_PRESSURE_MEASURE, // app has started blood pressure measurement 223 | HR_MEASURE_ALL, // app has started all health measurements 224 | }; 225 | 226 | /* 227 | The screen configurations below is only used for identification on the Chronos app. 228 | Under the watch tab, when you click on watch info you can see the detected screen configuration. 229 | The primary purpose of this configuration is to aid in loading watchfaces on supported watches with the correct resolution. 230 | ChronosESP32 library is implementing this for future development 231 | */ 232 | enum ChronosScreen 233 | { 234 | // Resolution(240x240), Size in inches(1.3), Type(0 - Round [C], 1 - Square [S], 2 - Rectangular [R]) 235 | CS_0x0_000_CFF = 0, // default no config 236 | CS_240x240_130_STF = 1, // 240x240, 1.3 inches, Square, True, False 237 | CS_240x240_130_STT = 2, // 240x240, 1.3 inches, Square, True, True 238 | CS_80x160_096_RTF = 3, // 80x160, 0.96 inches, Rectangular, True, False 239 | CS_80x160_096_RTT = 4, // 80x160, 0.96 inches, Rectangular, True, True 240 | CS_135x240_114_RTF = 5, // 135x240, 1.14 inches, Rectangular, True, False 241 | CS_135x240_114_RTT = 6, // 135x240, 1.14 inches, Rectangular, True, True 242 | CS_240x240_128_CTF = 7, // 240x240, 1.28 inches, Round, True, False 243 | CS_240x240_128_CTT = 8, // 240x240, 1.28 inches, Round, True, True 244 | CS_240x288_157_RTF = 9, // 240x288, 1.57 inches, Rectangular, True, False 245 | CS_240x288_157_RTT = 10, // 240x288, 1.57 inches, Rectangular, True, True 246 | CS_240x283_172_RTF = 11, // 240x283, 1.72 inches, Rectangular, True, False 247 | CS_240x283_172_RTT = 12, // 240x283, 1.72 inches, Rectangular, True, True 248 | CS_360x360_130_CTF = 13, // 360x360, 1.3 inches, Round, True, False 249 | CS_360x360_130_CTT = 14, // 360x360, 1.3 inches, Round, True, True 250 | CS_320x380_177_RTF = 15, // 320x380, 1.77 inches, Rectangular, True, False 251 | CS_320x380_177_RTT = 16, // 320x380, 1.77 inches, Rectangular, True, True 252 | CS_320x385_175_RTF = 17, // 320x385, 1.75 inches, Rectangular, True, False 253 | CS_320x385_175_RTT = 18, // 320x385, 1.75 inches, Rectangular, True, True 254 | CS_320x360_160_RTF = 19, // 320x360, 1.6 inches, Rectangular, True, False 255 | CS_320x360_160_RTT = 20, // 320x360, 1.6 inches, Rectangular, True, True 256 | CS_240x296_191_RTF = 21, // 240x296, 1.91 inches, Rectangular, True, False 257 | CS_240x296_191_RTT = 22, // 240x296, 1.91 inches, Rectangular, True, True 258 | CS_412x412_145_CTF = 23, // 412x412, 1.45 inches, Round, True, False 259 | CS_412x412_145_CTT = 24, // 412x412, 1.45 inches, Round, True, True 260 | CS_410x494_200_RTF = 25, // 410x494, 2.0 inches, Rectangular, True, False 261 | CS_410x494_200_RTT = 32, // 410x494, 2.0 inches, Rectangular, True, True 262 | CS_466x466_143_CTF = 33, // 466x466, 1.43 inches, Round, True, False 263 | CS_466x466_143_CTT = 34, // 466x466, 1.43 inches, Round, True, True 264 | 265 | // extended configurations for open source projects, used as an identifier for Chronos App 266 | CF_WATCHY_200x200 = 0x80, // Watchy 200x200 (E_PAPER) 267 | CF_ESP32_240x240 = 0x81, // Generic ESP32 240x240 268 | CF_ESP32_466x466 = 0x82, // Generic ESP32 466x466 (AMOLED) 269 | CF_WEBSCREEN_536x240 = 0x83, // Webscreen 536x240 (AMOLED) 270 | CF_WAVESHARE_410x502 = 0x84, // Waveshare 410x502 (AMOLED) 271 | CF_ZSWATCH_240x240 = 0x85, // ZSWatch 240x240 272 | CF_VIEWE_28_240x320 = 0x86, // Viewe ESP32 240x320 273 | }; 274 | 275 | class ChronosESP32 : public BLEServerCallbacks, public BLECharacteristicCallbacks, public ESP32Time 276 | { 277 | 278 | public: 279 | // library 280 | ChronosESP32(); 281 | ChronosESP32(String name, ChronosScreen screen = CF_ESP32_240x240); // set the BLE name 282 | void begin(); // initializes BLE server 283 | void stop(bool clearAll = true); // stop the BLE server 284 | void loop(); // handles routine functions 285 | bool isRunning(); // check whether BLE server is inited and running 286 | void setName(String name); // set the BLE name (call before begin) 287 | void setScreen(ChronosScreen screen); // set the screen config (call before begin) 288 | void setChunkedTransfer(bool chunked); 289 | bool isSubscribed(); 290 | 291 | // watch 292 | bool isConnected(); 293 | void set24Hour(bool mode); 294 | bool is24Hour(); 295 | String getAddress(); 296 | void setBattery(uint8_t level, bool charging = false); 297 | bool isCameraReady(); 298 | void syncRequest(); 299 | 300 | // notifications 301 | int getNotificationCount(); 302 | Notification getNotificationAt(int index); 303 | void clearNotifications(); 304 | 305 | // weather 306 | int getWeatherCount(); 307 | String getWeatherCity(); 308 | String getWeatherTime(); 309 | Weather getWeatherAt(int index); 310 | HourlyForecast getForecastHour(int hour); 311 | WeatherLocation getWeatherLocation(); 312 | 313 | // extras 314 | RemoteTouch getTouch(); 315 | String getQrAt(int index); 316 | void setQr(int index, String qr); 317 | 318 | // TODO (settings) 319 | // isQuietActive 320 | // isSleepActive 321 | 322 | // alarms 323 | Alarm getAlarm(int index); 324 | void setAlarm(int index, Alarm alarm); 325 | bool isAlarmActive(int index); 326 | bool isAlarmActive(Alarm alarm); 327 | bool isAnyAlarmActive(); 328 | // TODO (alarms) 329 | // alarm active callback 330 | // getActiveAlarms 331 | 332 | // control 333 | void sendCommand(uint8_t *command, size_t length, bool force_chunked = false); 334 | void musicControl(Control command); 335 | void setVolume(uint8_t level); 336 | bool capturePhoto(); 337 | void findPhone(bool state); 338 | 339 | // phone battery status 340 | void setNotifyBattery(bool state); 341 | bool isPhoneCharging(); 342 | uint8_t getPhoneBattery(); 343 | 344 | // app info 345 | int getAppCode(); 346 | String getAppVersion(); 347 | 348 | // navigation 349 | Navigation getNavigation(); 350 | 351 | // contacts 352 | void setContact(int index, Contact contact); 353 | Contact getContact(int index); 354 | int getContactCount(); 355 | Contact getSoSContact(); 356 | void setSOSContactIndex(int index); 357 | int getSOSContactIndex(); 358 | 359 | // health data 360 | void sendRealtimeSteps(uint32_t steps, uint32_t calories); 361 | void sendRealtimeHeartRate(uint8_t heartRate); 362 | void sendRealtimeBloodPressure(uint8_t systolic, uint8_t diastolic); 363 | void sendRealtimeBloodOxygen(uint8_t bloodOxygen); 364 | void sendRealtimeHealthData(uint8_t heartRate, uint8_t bloodOxygen, uint8_t systolic, uint8_t diastolic); 365 | 366 | void sendStepsRecord(uint32_t steps, uint32_t calories, uint8_t hour, uint8_t day, uint8_t month, uint32_t year, uint8_t heartRate = 0, uint8_t bloodOxygen = 0, uint8_t systolic = 0, uint8_t diastolic = 0); 367 | void sendHeartRateRecord(uint8_t heartRate, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year); 368 | void sendBloodPressureRecord(uint8_t systolic, uint8_t diastolic, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year); 369 | void sendBloodOxygenRecord(uint8_t bloodOxygen, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year); 370 | void sendSleepRecord(uint16_t sleepTime, SleepType type, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year); 371 | void sendTemperatureRecord(float temperature, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year); 372 | 373 | void sendStepsRecord(uint32_t steps, uint32_t calories, DateTime dateTime, uint8_t heartRate = 0, uint8_t bloodOxygen = 0, uint8_t systolic = 0, uint8_t diastolic = 0); 374 | void sendHeartRateRecord(uint8_t heartRate, DateTime dateTime); 375 | void sendBloodPressureRecord(uint8_t systolic, uint8_t diastolic, DateTime dateTime); 376 | void sendBloodOxygenRecord(uint8_t bloodOxygen, DateTime dateTime); 377 | void sendTemperatureRecord(float temperature, DateTime dateTime); 378 | void sendSleepRecord(uint16_t sleepTime, SleepType type, DateTime dateTime); 379 | 380 | // helper functions for ESP32Time 381 | int getHourC(); // return hour based on 24-hour variable (0-12 or 0-23) 382 | String getHourZ(); // return zero padded hour string based on 24-hour variable (00-12 or 00-23) 383 | String getAmPmC(bool caps = true); // return (no caps)am/pm or (caps)AM/PM for 12 hour mode or none for 24 hour mode 384 | 385 | // callbacks 386 | void setConnectionCallback(void (*callback)(bool)); 387 | void setNotificationCallback(void (*callback)(Notification)); 388 | void setRingerCallback(void (*callback)(String, bool)); 389 | void setConfigurationCallback(void (*callback)(Config, uint32_t, uint32_t)); 390 | void setDataCallback(void (*callback)(uint8_t *, int)); 391 | void setRawDataCallback(void (*callback)(uint8_t *, int)); 392 | void setHealthRequestCallback(void (*callback)(HealthRequest, bool)); 393 | 394 | private: 395 | String _watchName = "Chronos ESP32"; 396 | String _address; 397 | bool _inited; 398 | bool _subscribed; 399 | uint8_t _batteryLevel; 400 | bool _isCharging; 401 | bool _connected; 402 | bool _batteryChanged; 403 | bool _hour24; 404 | bool _cameraReady; 405 | 406 | uint8_t _phoneBatteryLevel = 0; 407 | bool _phoneCharging; 408 | bool _notifyPhone = true; 409 | bool _sendESP; 410 | bool _chunked; 411 | 412 | Notification _notifications[NOTIF_SIZE]; 413 | int _notificationIndex; 414 | 415 | Weather _weather[WEATHER_SIZE]; 416 | String _weatherCity; 417 | String _weatherTime; 418 | int _weatherSize; 419 | WeatherLocation _weatherLocation; 420 | 421 | HourlyForecast _hourlyForecast[FORECAST_SIZE]; 422 | 423 | RemoteTouch _touch; 424 | 425 | int _appCode; 426 | String _appVersion; 427 | 428 | Alarm _alarms[ALARM_SIZE]; 429 | 430 | String _qrLinks[QR_SIZE]; 431 | 432 | Contact _contacts[CONTACTS_SIZE]; 433 | int _sosContact; 434 | int _contactSize; 435 | 436 | ChronosTimer _infoTimer; 437 | ChronosTimer _findTimer; 438 | 439 | ChronosData _incomingData; 440 | ChronosData _outgoingData; 441 | 442 | ChronosScreen _screenConf = CS_240x240_128_CTF; 443 | 444 | Navigation _navigation; 445 | 446 | void (*connectionChangeCallback)(bool) = nullptr; 447 | void (*notificationReceivedCallback)(Notification) = nullptr; 448 | void (*ringerAlertCallback)(String, bool) = nullptr; 449 | void (*configurationReceivedCallback)(Config, uint32_t, uint32_t) = nullptr; 450 | void (*dataReceivedCallback)(uint8_t *, int) = nullptr; 451 | void (*rawDataReceivedCallback)(uint8_t *, int) = nullptr; 452 | void (*healthRequestCallback)(HealthRequest, bool) = nullptr; 453 | 454 | void sendInfo(); 455 | void sendBattery(); 456 | void sendESP(); 457 | 458 | void splitTitle(const String &input, String &title, String &message, int icon); 459 | 460 | String appName(int id); 461 | String flashMode(FlashMode_t mode); 462 | 463 | // from BLEServerCallbacks 464 | virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override; 465 | virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override; 466 | 467 | // from BLECharacteristicCallbacks 468 | virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override; 469 | virtual void onSubscribe(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo, uint16_t subValue) override; 470 | 471 | void dataReceived(); 472 | 473 | static BLECharacteristic *pCharacteristicTX; 474 | static BLECharacteristic *pCharacteristicRX; 475 | }; 476 | 477 | #endif 478 | -------------------------------------------------------------------------------- /src/ChronosESP32.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Felix Biego 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | ______________ _____ 25 | ___ __/___ /_ ___(_)_____ _______ _______ 26 | __ /_ __ __ \__ / _ _ \__ __ `/_ __ \ 27 | _ __/ _ /_/ /_ / / __/_ /_/ / / /_/ / 28 | /_/ /_.___/ /_/ \___/ _\__, / \____/ 29 | /____/ 30 | 31 | */ 32 | 33 | #include 34 | #include "ChronosESP32.h" 35 | 36 | BLECharacteristic *ChronosESP32::pCharacteristicTX; 37 | BLECharacteristic *ChronosESP32::pCharacteristicRX; 38 | 39 | /*! 40 | @brief Constructor for ChronosESP32 41 | */ 42 | ChronosESP32::ChronosESP32() 43 | { 44 | _connected = false; 45 | _cameraReady = false; 46 | _batteryChanged = true; 47 | _qrLinks[0] = "https://chronos.ke/"; 48 | 49 | _notifications[0].icon = 0xC0; 50 | _notifications[0].time = "Now"; 51 | _notifications[0].app = "Chronos"; 52 | _notifications[0].message = "Download from Google Play to sync time and receive notifications"; 53 | 54 | _infoTimer.duration = 3 * 1000; // 3 seconds for info timer 55 | _findTimer.duration = 30 * 1000; // 30 seconds for find phone 56 | } 57 | 58 | /*! 59 | @brief Constructor for ChronosESP32 60 | @param name 61 | Bluetooth name 62 | @param screen 63 | screen config 64 | */ 65 | ChronosESP32::ChronosESP32(String name, ChronosScreen screen) 66 | { 67 | _watchName = name; 68 | _screenConf = screen; 69 | ChronosESP32(); 70 | } 71 | 72 | /*! 73 | @brief set bluetooth name (call before begin function) 74 | @param name 75 | Bluetooth name 76 | */ 77 | void ChronosESP32::setName(String name) 78 | { 79 | _watchName = name; 80 | } 81 | 82 | /*! 83 | @brief set screen config (call before begin function) 84 | @param screen 85 | screen config 86 | */ 87 | void ChronosESP32::setScreen(ChronosScreen screen) 88 | { 89 | _screenConf = screen; 90 | } 91 | 92 | /*! 93 | @brief initializes bluetooth LE server 94 | */ 95 | void ChronosESP32::begin() 96 | { 97 | BLEDevice::init(_watchName.c_str()); 98 | BLEServer *pServer = BLEDevice::createServer(); 99 | BLEDevice::setMTU(517); 100 | pServer->setCallbacks(this, false); 101 | 102 | BLEService *pService = pServer->createService(SERVICE_UUID); 103 | pCharacteristicTX = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, NIMBLE_PROPERTY::NOTIFY); 104 | pCharacteristicRX = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR); 105 | pCharacteristicRX->setCallbacks(this); 106 | pCharacteristicTX->setCallbacks(this); 107 | pService->start(); 108 | 109 | BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); 110 | pAdvertising->addServiceUUID(SERVICE_UUID); 111 | pAdvertising->enableScanResponse(true); 112 | pAdvertising->setPreferredParams(0x06, 0x12); // functions that help with iPhone connections issue 113 | pAdvertising->setName(_watchName.c_str()); 114 | pAdvertising->start(); 115 | 116 | _address = BLEDevice::getAddress().toString().c_str(); 117 | 118 | _inited = true; 119 | } 120 | 121 | /*! 122 | @brief stops bluetooth LE server 123 | */ 124 | void ChronosESP32::stop(bool clearAll) 125 | { 126 | BLEDevice::deinit(clearAll); 127 | _inited = false; 128 | } 129 | 130 | /*! 131 | @brief check whether bluetooth LE server is initialized and running 132 | */ 133 | bool ChronosESP32::isRunning() 134 | { 135 | return _inited; 136 | } 137 | 138 | /*! 139 | @brief handles routine functions 140 | */ 141 | void ChronosESP32::loop() 142 | { 143 | if (!_inited) 144 | { 145 | // begin not called. do nothing 146 | return; 147 | } 148 | 149 | if (_connected) 150 | { 151 | if (_infoTimer.active) 152 | { 153 | if (_infoTimer.time + _infoTimer.duration < millis()) 154 | { 155 | // timer end 156 | _infoTimer.active = false; 157 | 158 | sendInfo(); 159 | sendBattery(); 160 | setNotifyBattery(_notifyPhone); 161 | } 162 | } 163 | if (_findTimer.active) 164 | { 165 | if (_findTimer.time + _findTimer.duration < millis()) 166 | { 167 | // timer end 168 | _findTimer.active = false; 169 | findPhone(false); // auto cancel the command 170 | } 171 | } 172 | if (_batteryChanged) 173 | { 174 | _batteryChanged = false; 175 | sendBattery(); 176 | } 177 | if (_sendESP) 178 | { 179 | _sendESP = false; 180 | sendESP(); 181 | } 182 | } 183 | } 184 | 185 | /*! 186 | @brief set whether to split transferred bytes 187 | @param chunked 188 | enable or disable state 189 | */ 190 | void ChronosESP32::setChunkedTransfer(bool chunked) 191 | { 192 | _chunked = chunked; 193 | } 194 | 195 | /*! 196 | @brief check whether the device is connected 197 | */ 198 | bool ChronosESP32::isConnected() 199 | { 200 | return _connected; 201 | } 202 | 203 | /*! 204 | @brief check whether the device is subcribed to ble notifications 205 | */ 206 | bool ChronosESP32::isSubscribed() 207 | { 208 | return _subscribed; 209 | } 210 | 211 | /*! 212 | @brief set the clock to 24 hour mode 213 | @param mode 214 | enable or disable state 215 | */ 216 | void ChronosESP32::set24Hour(bool mode) 217 | { 218 | _hour24 = mode; 219 | } 220 | 221 | /*! 222 | @brief return the 24 hour mode 223 | */ 224 | bool ChronosESP32::is24Hour() 225 | { 226 | return _hour24; 227 | } 228 | 229 | /*! 230 | @brief return the mac address 231 | */ 232 | String ChronosESP32::getAddress() 233 | { 234 | return _address; 235 | } 236 | 237 | /*! 238 | @brief set the battery level 239 | @param level 240 | battery level 241 | @param charging 242 | charging state 243 | */ 244 | void ChronosESP32::setBattery(uint8_t level, bool charging) 245 | { 246 | if (_batteryLevel != level || _isCharging != charging) 247 | { 248 | _batteryChanged = true; 249 | _batteryLevel = level; 250 | _isCharging = charging; 251 | } 252 | } 253 | 254 | /*! 255 | @brief return camera status 256 | */ 257 | bool ChronosESP32::isCameraReady() 258 | { 259 | return _cameraReady; 260 | } 261 | 262 | /*! 263 | @brief return the number of notifications in the buffer 264 | */ 265 | int ChronosESP32::getNotificationCount() 266 | { 267 | if (_notificationIndex + 1 >= NOTIF_SIZE) 268 | { 269 | return NOTIF_SIZE; // the buffer is full 270 | } 271 | else 272 | { 273 | return _notificationIndex + 1; // the buffer is not full, 274 | } 275 | } 276 | 277 | /*! 278 | @brief return the notification at the buffer index 279 | @param index 280 | position of the notification to be returned, at 0 is the latest received 281 | */ 282 | Notification ChronosESP32::getNotificationAt(int index) 283 | { 284 | int latestIndex = (_notificationIndex - index + NOTIF_SIZE) % NOTIF_SIZE; 285 | return _notifications[latestIndex]; 286 | } 287 | 288 | /*! 289 | @brief clear the notificaitons 290 | */ 291 | void ChronosESP32::clearNotifications() 292 | { 293 | // here we just set the index to -1, existing data at the buffer will be overwritten 294 | // getNotificationCount() will return 0 but getNotificationAt() will return previous existing data 295 | _notificationIndex = -1; 296 | } 297 | 298 | /*! 299 | @brief set the contact at the index 300 | @param index 301 | position to set the contact 302 | @param contact 303 | the contact to be set 304 | */ 305 | void ChronosESP32::setContact(int index, Contact contact) 306 | { 307 | _contacts[index % CONTACTS_SIZE] = contact; 308 | } 309 | 310 | /*! 311 | @brief return the contact at the index 312 | @param index 313 | position of the contact to be returned 314 | */ 315 | Contact ChronosESP32::getContact(int index) 316 | { 317 | return _contacts[index % CONTACTS_SIZE]; 318 | } 319 | 320 | /*! 321 | @brief return the contact size 322 | */ 323 | int ChronosESP32::getContactCount() 324 | { 325 | return _contactSize; 326 | } 327 | 328 | /*! 329 | @brief return the sos contact 330 | */ 331 | Contact ChronosESP32::getSoSContact() 332 | { 333 | return _contacts[_sosContact % CONTACTS_SIZE]; 334 | } 335 | 336 | /*! 337 | @brief set the sos contact index 338 | */ 339 | void ChronosESP32::setSOSContactIndex(int index) 340 | { 341 | _sosContact = index; 342 | } 343 | 344 | /*! 345 | @brief return sos contact index 346 | */ 347 | int ChronosESP32::getSOSContactIndex() 348 | { 349 | return _sosContact; 350 | } 351 | 352 | /*! 353 | @brief return the weather count 354 | */ 355 | int ChronosESP32::getWeatherCount() 356 | { 357 | return _weatherSize; 358 | } 359 | 360 | /*! 361 | @brief return the weather city name 362 | */ 363 | String ChronosESP32::getWeatherCity() 364 | { 365 | return _weatherCity; 366 | } 367 | 368 | /*! 369 | @brief return the weather update time, HH:MM format 370 | */ 371 | String ChronosESP32::getWeatherTime() 372 | { 373 | return _weatherTime; 374 | } 375 | 376 | /*! 377 | @brief return the weather at the buffer index 378 | @param index 379 | position of the weather to be returned 380 | */ 381 | Weather ChronosESP32::getWeatherAt(int index) 382 | { 383 | return _weather[index % WEATHER_SIZE]; 384 | } 385 | 386 | /*! 387 | @brief return the weatherforecast for the hour 388 | @param hour 389 | position of the weather to be returned 390 | */ 391 | HourlyForecast ChronosESP32::getForecastHour(int hour) 392 | { 393 | return _hourlyForecast[hour % FORECAST_SIZE]; 394 | } 395 | 396 | /*! 397 | @brief return the weather location data 398 | */ 399 | WeatherLocation ChronosESP32::getWeatherLocation() 400 | { 401 | return _weatherLocation; 402 | } 403 | 404 | /*! 405 | @brief get the alarm at the index 406 | @param index 407 | position of the alarm to be returned 408 | */ 409 | Alarm ChronosESP32::getAlarm(int index) 410 | { 411 | return _alarms[index % ALARM_SIZE]; 412 | } 413 | 414 | /*! 415 | @brief set the alarm at the index 416 | @param index 417 | position of the alarm to be set 418 | @param alarm 419 | the alarm object 420 | */ 421 | void ChronosESP32::setAlarm(int index, Alarm alarm) 422 | { 423 | _alarms[index % ALARM_SIZE] = alarm; 424 | } 425 | 426 | /*! 427 | @brief check whether the alarm at the index is active 428 | @param index 429 | position of the alarm to be checked 430 | */ 431 | bool ChronosESP32::isAlarmActive(int index) 432 | { 433 | return isAlarmActive(_alarms[index % ALARM_SIZE]); 434 | } 435 | 436 | /*! 437 | @brief check whether the alarm is active 438 | @param alarm 439 | the alarm object to be checked 440 | */ 441 | bool ChronosESP32::isAlarmActive(Alarm alarm) 442 | { 443 | if (!alarm.enabled) 444 | return false; 445 | 446 | if (alarm.hour != this->getHour(true) || alarm.minute != this->getMinute()) 447 | return false; 448 | 449 | if (alarm.repeat == 0x80 || alarm.repeat == 0x7F) 450 | return true; 451 | 452 | int day = this->getDayofWeek(); // 0=Sun, ... 6=Sat 453 | 454 | if (day == 0) // Sunday → bit 6 455 | return (alarm.repeat & (1 << 6)) != 0; 456 | else // Mon–Sat → bits 0–5 457 | return (alarm.repeat & (1 << (day - 1))) != 0; 458 | } 459 | 460 | /*! 461 | @brief check whether any alarm is active 462 | */ 463 | bool ChronosESP32::isAnyAlarmActive() 464 | { 465 | for (int i = 0; i < ALARM_SIZE; i++) 466 | { 467 | if (isAlarmActive(_alarms[i])) 468 | return true; 469 | } 470 | return false; 471 | } 472 | 473 | /*! 474 | @brief send a command to the app 475 | @param command 476 | command data 477 | @param length 478 | command length 479 | @param force_chunked 480 | override internal chunked 481 | */ 482 | void ChronosESP32::sendCommand(uint8_t *command, size_t length, bool force_chunked) 483 | { 484 | if (!_inited) 485 | { 486 | // begin not called. do nothing 487 | return; 488 | } 489 | 490 | if ((length <= 20 || !_chunked) && !force_chunked) 491 | { 492 | // Send the entire command if it fits in one packet 493 | pCharacteristicTX->setValue(command, length); 494 | pCharacteristicTX->notify(); 495 | vTaskDelay(200 / portTICK_PERIOD_MS); 496 | } 497 | else 498 | { 499 | // Send the first 20 bytes as is (no header) 500 | pCharacteristicTX->setValue(command, 20); 501 | pCharacteristicTX->notify(); 502 | vTaskDelay(200 / portTICK_PERIOD_MS); 503 | 504 | // Send the remaining bytes with a header 505 | const size_t maxPayloadSize = 19; // Payload size excluding header 506 | uint8_t chunk[20]; // Buffer for chunks with header 507 | size_t offset = 20; // Start after the first 20 bytes 508 | uint8_t sequenceNumber = 0; // Sequence number for headers 509 | 510 | while (offset < length) 511 | { 512 | // Add the header (sequence number) 513 | chunk[0] = sequenceNumber++; 514 | 515 | // Calculate how many bytes to send in this chunk 516 | size_t bytesToSend = min(maxPayloadSize, length - offset); 517 | 518 | // Copy data to chunk, leaving space for the header 519 | memcpy(chunk + 1, command + offset, bytesToSend); 520 | 521 | // Send the chunk 522 | pCharacteristicTX->setValue(chunk, bytesToSend + 1); 523 | pCharacteristicTX->notify(); 524 | vTaskDelay(200 / portTICK_PERIOD_MS); 525 | 526 | // Update offset 527 | offset += bytesToSend; 528 | } 529 | } 530 | } 531 | 532 | /*! 533 | @brief send a music control command to the app 534 | @param command 535 | music action 536 | */ 537 | void ChronosESP32::musicControl(Control command) 538 | { 539 | uint8_t musicCmd[] = {0xAB, 0x00, 0x04, 0xFF, (uint8_t)(command >> 8), 0x80, (uint8_t)(command)}; 540 | sendCommand(musicCmd, 7); 541 | } 542 | 543 | /*! 544 | @brief send a command to set the volume level 545 | @param level 546 | volume level (0 - 100) 547 | */ 548 | void ChronosESP32::setVolume(uint8_t level) 549 | { 550 | uint8_t volumeCmd[] = {0xAB, 0x00, 0x05, 0xFF, 0x99, 0x80, 0xA0, level}; 551 | sendCommand(volumeCmd, 8); 552 | } 553 | 554 | /*! 555 | @brief send capture photo command to the app 556 | */ 557 | bool ChronosESP32::capturePhoto() 558 | { 559 | if (_cameraReady) 560 | { 561 | uint8_t captureCmd[] = {0xAB, 0x00, 0x04, 0xFF, 0x79, 0x80, 0x01}; 562 | sendCommand(captureCmd, 7); 563 | } 564 | return _cameraReady; 565 | } 566 | 567 | /*! 568 | @brief send a command to find the phone 569 | @param state 570 | enable or disable state 571 | */ 572 | void ChronosESP32::findPhone(bool state) 573 | { 574 | _findTimer.active = state; 575 | if (state) 576 | { 577 | _findTimer.time = millis(); 578 | } 579 | uint8_t c = state ? 0x01 : 0x00; 580 | uint8_t findCmd[] = {0xAB, 0x00, 0x04, 0xFF, 0x7D, 0x80, c}; 581 | sendCommand(findCmd, 7); 582 | } 583 | 584 | /*! 585 | @brief get the hour based on hour 24 mode 586 | */ 587 | int ChronosESP32::getHourC() 588 | { 589 | return this->getHour(_hour24); 590 | } 591 | 592 | /*! 593 | @brief get the zero padded hour based on hour 24 mode 594 | */ 595 | String ChronosESP32::getHourZ() 596 | { 597 | return this->getTime(_hour24 ? "%H" : "%I"); 598 | } 599 | 600 | /*! 601 | @brief get the am pm label 602 | @param caps 603 | capital letters mode 604 | */ 605 | String ChronosESP32::getAmPmC(bool caps) 606 | { 607 | return _hour24 ? "" : this->getAmPm(!caps); // on esp32time it's getAmPm(bool lowercase); 608 | } 609 | 610 | /*! 611 | @brief get remote touch data 612 | */ 613 | RemoteTouch ChronosESP32::getTouch() 614 | { 615 | return _touch; 616 | } 617 | 618 | /*! 619 | @brief get the qr link at the index 620 | @param index 621 | position of the qr link to be returned 622 | */ 623 | String ChronosESP32::getQrAt(int index) 624 | { 625 | return _qrLinks[index % QR_SIZE]; 626 | } 627 | 628 | void ChronosESP32::setQr(int index, String qr) 629 | { 630 | _qrLinks[index % QR_SIZE] = qr; 631 | } 632 | 633 | /*! 634 | @brief set the connection callback 635 | @param callback 636 | callback function 637 | */ 638 | void ChronosESP32::setConnectionCallback(void (*callback)(bool)) 639 | { 640 | connectionChangeCallback = callback; 641 | } 642 | 643 | /*! 644 | @brief set the notification callback 645 | @param callback 646 | callback function 647 | */ 648 | void ChronosESP32::setNotificationCallback(void (*callback)(Notification)) 649 | { 650 | notificationReceivedCallback = callback; 651 | } 652 | 653 | /*! 654 | @brief set the ringer callback 655 | @param callback 656 | callback function 657 | */ 658 | void ChronosESP32::setRingerCallback(void (*callback)(String, bool)) 659 | { 660 | ringerAlertCallback = callback; 661 | } 662 | 663 | /*! 664 | @brief set the configuration callback 665 | @param callback 666 | callback function 667 | */ 668 | void ChronosESP32::setConfigurationCallback(void (*callback)(Config, uint32_t, uint32_t)) 669 | { 670 | configurationReceivedCallback = callback; 671 | } 672 | 673 | /*! 674 | @brief set the data callback, assembled data packets matching the specified format (should start with 0xAB or 0xEA) 675 | @param callback 676 | callback function 677 | */ 678 | void ChronosESP32::setDataCallback(void (*callback)(uint8_t *, int)) 679 | { 680 | dataReceivedCallback = callback; 681 | } 682 | 683 | /*! 684 | @brief set the raw data callback, all incoming data via ble 685 | @param callback 686 | callback function 687 | */ 688 | void ChronosESP32::setRawDataCallback(void (*callback)(uint8_t *, int)) 689 | { 690 | rawDataReceivedCallback = callback; 691 | } 692 | 693 | /*! 694 | @brief set the health request callback 695 | @param callback 696 | callback function 697 | */ 698 | void ChronosESP32::setHealthRequestCallback(void (*callback)(HealthRequest, bool)) 699 | { 700 | healthRequestCallback = callback; 701 | } 702 | 703 | /*! 704 | @brief send the info properties to the app 705 | */ 706 | void ChronosESP32::sendInfo() 707 | { 708 | uint8_t infoCmd[] = {0xab, 0x00, 0x11, 0xff, 0x92, 0xc0, CHRONOSESP_VERSION_MAJOR, (CHRONOSESP_VERSION_MINOR * 10 + CHRONOSESP_VERSION_PATCH), 0x00, 0xfb, 0x1e, 0x40, 0xc0, 0x0e, 0x32, 0x28, 0x00, 0xe2, _screenConf, 0x80}; 709 | sendCommand(infoCmd, 20); 710 | } 711 | 712 | /*! 713 | @brief send the esp properties to the app 714 | */ 715 | void ChronosESP32::sendESP() 716 | { 717 | String espInfo; 718 | espInfo += "ChronosESP32 v" + String(CHRONOSESP_VERSION_MAJOR) + "." + String(CHRONOSESP_VERSION_MINOR) + "." + String(CHRONOSESP_VERSION_PATCH); 719 | espInfo += "\n" + String(ESP.getChipModel()); 720 | espInfo += " @" + String(ESP.getCpuFreqMHz()) + "Mhz"; 721 | espInfo += " Cores:" + String(ESP.getChipCores()); 722 | espInfo += " rev" + String(ESP.getChipRevision()); 723 | 724 | espInfo += "\nRAM: " + String((ESP.getHeapSize() / 1024.0), 0) + "kB"; 725 | espInfo += " + PSRAM: " + String((ESP.getPsramSize() / (1024.0 * 1024.0)), 0) + "MB"; 726 | 727 | espInfo += "\nFlash: " + String((ESP.getFlashChipSize() / (1024.0 * 1024.0)), 0) + "MB"; 728 | espInfo += " @" + String((ESP.getFlashChipSpeed() / 1000000.0), 0) + "Mhz"; 729 | espInfo += " " + flashMode(ESP.getFlashChipMode()); 730 | 731 | espInfo += "\nSDK: " + String(ESP.getSdkVersion()); 732 | espInfo += "\nSketch: " + String((ESP.getSketchSize() / (1024.0)), 0) + "kB"; 733 | 734 | if (espInfo.length() > 505) 735 | { 736 | espInfo = espInfo.substring(0, 505); 737 | } 738 | 739 | uint16_t len = espInfo.length(); 740 | _outgoingData.data[0] = 0xAB; 741 | _outgoingData.data[1] = highByte(len + 3); 742 | _outgoingData.data[2] = lowByte(len + 3); 743 | _outgoingData.data[3] = 0xFE; 744 | _outgoingData.data[4] = 0x92; 745 | _outgoingData.data[5] = 0x80; 746 | espInfo.toCharArray((char *)_outgoingData.data + 6, 506); 747 | sendCommand((uint8_t *)_outgoingData.data, 6 + len, true); 748 | } 749 | 750 | /*! 751 | @brief get flash mode string 752 | @param mode 753 | flash mode type 754 | */ 755 | String ChronosESP32::flashMode(FlashMode_t mode) 756 | { 757 | switch (mode) 758 | { 759 | case FM_QIO: 760 | return "QIO"; 761 | case FM_QOUT: 762 | return "QOUT"; 763 | case FM_DIO: 764 | return "DIO"; 765 | case FM_DOUT: 766 | return "DOUT"; 767 | case FM_FAST_READ: 768 | return "FAST_READ"; 769 | case FM_SLOW_READ: 770 | return "SLOW_READ"; 771 | default: 772 | return "UNKNOWN"; 773 | } 774 | } 775 | 776 | /*! 777 | @brief send the battery level 778 | */ 779 | void ChronosESP32::sendBattery() 780 | { 781 | uint8_t c = _isCharging ? 0x01 : 0x00; 782 | uint8_t batCmd[] = {0xAB, 0x00, 0x05, 0xFF, 0x91, 0x80, c, _batteryLevel}; 783 | sendCommand(batCmd, 8); 784 | } 785 | 786 | /*! 787 | @brief request the app to sync settings 788 | */ 789 | void ChronosESP32::syncRequest() 790 | { 791 | uint8_t syncCmd[] = {0xAB, 0x00, 0x03, 0xFE, 0x23, 0x80}; 792 | sendCommand(syncCmd, 6); 793 | } 794 | 795 | /*! 796 | @brief request the battery level of the phone 797 | */ 798 | void ChronosESP32::setNotifyBattery(bool state) 799 | { 800 | _notifyPhone = state; 801 | uint8_t s = state ? 0x01 : 0x00; 802 | uint8_t batRq[] = {0xAB, 0x00, 0x04, 0xFE, 0x91, 0x80, s}; // custom command AB..FE 803 | sendCommand(batRq, 7); 804 | } 805 | 806 | /*! 807 | @brief send the realtime steps & calories, this will be shown on the app homepage and notification if enabled 808 | @param steps 809 | current steps value 810 | @param calories 811 | current calories value 812 | @note the data sent will not be saved in the app 813 | */ 814 | void ChronosESP32::sendRealtimeSteps(uint32_t steps, uint32_t calories) 815 | { 816 | uint8_t stepsCmd[] = { 817 | 0xAB, 0x00, 0x0E, 0xFF, 0x51, 0x08, 818 | (uint8_t)(steps >> 16), (uint8_t)(steps >> 8), (uint8_t)(steps), 819 | (uint8_t)(calories >> 16), (uint8_t)(calories >> 8), (uint8_t)(calories), 820 | 0x00, 0x00, 0x00, 0x00, 0x00}; 821 | sendCommand(stepsCmd, 17); 822 | } 823 | /*! 824 | @brief send the realtime heart rate, this should be sent after the request from the app 825 | @param heartRate 826 | current heart rate value 827 | @note the data sent will be saved with the current date and time 828 | */ 829 | void ChronosESP32::sendRealtimeHeartRate(uint8_t heartRate) 830 | { 831 | // AB 00 05 FF 31 0A 49 1B 832 | uint8_t heartCmd[] = { 833 | 0xAB, 0x00, 0x05, 0xFF, 0x31, 0x0A, heartRate, 0x1B}; 834 | sendCommand(heartCmd, 8); 835 | } 836 | 837 | /*! 838 | @brief send the realtime blood pressure, this should be sent after the request from the app 839 | @param systolic 840 | systolic blood pressure value 841 | @param diastolic 842 | diastolic blood pressure value 843 | @note the data sent will be saved with the current date and time 844 | */ 845 | void ChronosESP32::sendRealtimeBloodPressure(uint8_t systolic, uint8_t diastolic) 846 | { 847 | // AB 00 05 FF 31 22 71 4C 848 | uint8_t pressureCmd[] = { 849 | 0xAB, 0x00, 0x05, 0xFF, 0x31, 0x22, systolic, diastolic}; 850 | sendCommand(pressureCmd, 8); 851 | } 852 | 853 | /*! 854 | @brief send the realtime blood oxygen, this should be sent after the request from the app 855 | @param bloodOxygen 856 | current blood oxygen value 857 | @note the data sent will be saved with the current date and time 858 | */ 859 | void ChronosESP32::sendRealtimeBloodOxygen(uint8_t bloodOxygen) 860 | { 861 | // AB 00 05 FF 31 12 62 30 862 | uint8_t oxygenCmd[] = { 863 | 0xAB, 0x00, 0x05, 0xFF, 0x31, 0x12, bloodOxygen, 0x30}; 864 | sendCommand(oxygenCmd, 8); 865 | } 866 | 867 | /*! 868 | @brief send the realtime health data, this should be sent after the request from the app 869 | @param heartRate 870 | current heart rate value 871 | @param bloodOxygen 872 | current blood oxygen value 873 | @param systolic 874 | systolic blood pressure value 875 | @param diastolic 876 | diastolic blood pressure value 877 | @note the data sent will be saved with the current date and time 878 | */ 879 | void ChronosESP32::sendRealtimeHealthData(uint8_t heartRate, uint8_t bloodOxygen, uint8_t systolic, uint8_t diastolic) 880 | { 881 | // AB 00 07 FF 32 80 44 61 72 4B 882 | uint8_t healthCmd[] = { 883 | 0xAB, 0x00, 0x07, 0xFF, 0x32, 0x80, heartRate, bloodOxygen, systolic, diastolic}; 884 | sendCommand(healthCmd, 10); 885 | } 886 | 887 | /*! 888 | @brief send a single steps record, the steps is typically grouped by the hour 889 | @param steps 890 | steps value 891 | @param calories 892 | calories value 893 | @param hour 894 | hour of the record 895 | @param day 896 | day of the record 897 | @param month 898 | month of the record 899 | @param year 900 | year of the record 901 | @param heartRate 902 | heart rate value if available (optional) 903 | @param bloodOxygen 904 | blood oxygen value if available (optional) 905 | @param systolic 906 | systolic blood pressure value if available (optional) 907 | @param diastolic 908 | diastolic blood pressure value if available (optional) 909 | */ 910 | void ChronosESP32::sendStepsRecord(uint32_t steps, uint32_t calories, uint8_t hour, uint8_t day, uint8_t month, uint32_t year, uint8_t heartRate, uint8_t bloodOxygen, uint8_t systolic, uint8_t diastolic) 911 | { 912 | uint8_t stepsCmd[] = { 913 | 0xAB, 0x00, 0x16, 0xFF, 0x51, 0x20, 914 | (uint8_t)(year - 2000), month, day, hour, 915 | (uint8_t)(steps >> 16), (uint8_t)(steps >> 8), (uint8_t)(steps), 916 | (uint8_t)(calories >> 16), (uint8_t)(calories >> 8), (uint8_t)(calories), 917 | heartRate, bloodOxygen, systolic, diastolic, 918 | 0x00, 0x00, 0x00, 0x00, 0x00}; 919 | sendCommand(stepsCmd, 25); 920 | } 921 | 922 | /*! 923 | @brief send a single heart rate record 924 | @param heartRate 925 | heart rate value 926 | @param minute 927 | minute of the record 928 | @param hour 929 | hour of the record 930 | @param day 931 | day of the record 932 | @param month 933 | month of the record 934 | @param year 935 | year of the record 936 | */ 937 | void ChronosESP32::sendHeartRateRecord(uint8_t heartRate, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year) 938 | { 939 | uint8_t heartCmd[] = { 940 | 0xAB, 0x00, 0x0A, 0xFF, 0x51, 0x11, 941 | (uint8_t)(year - 2000), month, day, hour, minute, 942 | heartRate, 0x00}; 943 | sendCommand(heartCmd, 13); 944 | } 945 | 946 | /*! 947 | @brief send a single blood pressure record 948 | @param systolic 949 | systolic blood pressure value 950 | @param diastolic 951 | diastolic blood pressure value 952 | @param minute 953 | minute of the record 954 | @param hour 955 | hour of the record 956 | @param day 957 | day of the record 958 | @param month 959 | month of the record 960 | @param year 961 | year of the record 962 | */ 963 | void ChronosESP32::sendBloodPressureRecord(uint8_t systolic, uint8_t diastolic, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year) 964 | { 965 | uint8_t pressureCmd[] = { 966 | 0xAB, 0x00, 0x0A, 0xFF, 0x51, 0x14, 967 | (uint8_t)(year - 2000), month, day, hour, minute, 968 | systolic, diastolic}; 969 | sendCommand(pressureCmd, 13); 970 | } 971 | 972 | /*! 973 | @brief send a single blood oxygen record 974 | @param bloodOxygen 975 | blood oxygen value 976 | @param minute 977 | minute of the record 978 | @param hour 979 | hour of the record 980 | @param day 981 | day of the record 982 | @param month 983 | month of the record 984 | @param year 985 | year of the record 986 | */ 987 | void ChronosESP32::sendBloodOxygenRecord(uint8_t bloodOxygen, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year) 988 | { 989 | uint8_t oxygenCmd[] = { 990 | 0xAB, 0x00, 0x0A, 0xFF, 0x51, 0x12, 991 | (uint8_t)(year - 2000), month, day, hour, minute, 992 | bloodOxygen, 0x00}; 993 | sendCommand(oxygenCmd, 13); 994 | } 995 | 996 | /*! 997 | @brief send a single sleep record 998 | @param sleepTime 999 | sleep time in minutes 1000 | @param type 1001 | sleep type (0 - light, 1 - deep) 1002 | @param minute 1003 | minute of the record 1004 | @param hour 1005 | hour of the record 1006 | @param day 1007 | day of the record 1008 | @param month 1009 | month of the record 1010 | @param year 1011 | year of the record 1012 | */ 1013 | void ChronosESP32::sendSleepRecord(uint16_t sleepTime, SleepType type, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year) 1014 | { 1015 | uint8_t sleepCmd[] = { 1016 | 0xAB, 0x00, 0x0B, 0xFF, 0x52, 0x80, 1017 | (uint8_t)(year - 2000), month, day, hour, minute, 1018 | (uint8_t)(type), highByte(sleepTime), lowByte(sleepTime)}; 1019 | sendCommand(sleepCmd, 14); 1020 | } 1021 | 1022 | /*! 1023 | @brief send a single temperature record 1024 | @param temperature 1025 | temperature value 1026 | @param minute 1027 | minute of the record 1028 | @param hour 1029 | hour of the record 1030 | @param day 1031 | day of the record 1032 | @param month 1033 | month of the record 1034 | @param year 1035 | year of the record 1036 | */ 1037 | void ChronosESP32::sendTemperatureRecord(float temperature, uint8_t minute, uint8_t hour, uint8_t day, uint8_t month, uint32_t year) 1038 | { 1039 | uint8_t tempCmd[] = { 1040 | 0xAB, 0x00, 0x0A, 0xFF, 0x51, 0x13, 1041 | (uint8_t)(year - 2000), month, day, hour, minute, 1042 | (uint8_t)(temperature), (uint8_t)((uint16_t)(temperature * 100.0) % 100)}; 1043 | sendCommand(tempCmd, 13); 1044 | } 1045 | 1046 | /*! 1047 | @brief send a single steps record, the steps is typically grouped by the hour 1048 | @param steps 1049 | steps value 1050 | @param calories 1051 | calories value 1052 | @param dateTime 1053 | date and time of the record 1054 | @param heartRate 1055 | heart rate value if available (optional) 1056 | @param bloodOxygen 1057 | blood oxygen value if available (optional) 1058 | @param systolic 1059 | systolic blood pressure value if available (optional) 1060 | @param diastolic 1061 | diastolic blood pressure value if available (optional) 1062 | */ 1063 | void ChronosESP32::sendStepsRecord(uint32_t steps, uint32_t calories, DateTime dateTime, uint8_t heartRate, uint8_t bloodOxygen, uint8_t systolic, uint8_t diastolic) 1064 | { 1065 | sendStepsRecord(steps, calories, dateTime.hour, dateTime.day, dateTime.month, dateTime.year, heartRate, bloodOxygen, systolic, diastolic); 1066 | } 1067 | 1068 | /*! 1069 | @brief send a single heart rate record 1070 | @param heartRate 1071 | heart rate value 1072 | @param dateTime 1073 | date and time of the record 1074 | */ 1075 | void ChronosESP32::sendHeartRateRecord(uint8_t heartRate, DateTime dateTime) 1076 | { 1077 | sendHeartRateRecord(heartRate, dateTime.minute, dateTime.hour, dateTime.day, dateTime.month, dateTime.year); 1078 | } 1079 | 1080 | /*! 1081 | @brief send a single blood pressure record 1082 | @param systolic 1083 | systolic blood pressure value 1084 | @param diastolic 1085 | diastolic blood pressure value 1086 | @param dateTime 1087 | date and time of the record 1088 | */ 1089 | void ChronosESP32::sendBloodPressureRecord(uint8_t systolic, uint8_t diastolic, DateTime dateTime) 1090 | { 1091 | sendBloodPressureRecord(systolic, diastolic, dateTime.minute, dateTime.hour, dateTime.day, dateTime.month, dateTime.year); 1092 | } 1093 | 1094 | /*! 1095 | @brief send a single blood oxygen record 1096 | @param bloodOxygen 1097 | blood oxygen value 1098 | @param dateTime 1099 | date and time of the record 1100 | */ 1101 | void ChronosESP32::sendBloodOxygenRecord(uint8_t bloodOxygen, DateTime dateTime) 1102 | { 1103 | sendBloodOxygenRecord(bloodOxygen, dateTime.minute, dateTime.hour, dateTime.day, dateTime.month, dateTime.year); 1104 | } 1105 | 1106 | /*! 1107 | @brief send a single temperature record 1108 | @param temperature 1109 | temperature value 1110 | @param dateTime 1111 | date and time of the record 1112 | */ 1113 | void ChronosESP32::sendTemperatureRecord(float temperature, DateTime dateTime) 1114 | { 1115 | sendTemperatureRecord(temperature, dateTime.minute, dateTime.hour, dateTime.day, dateTime.month, dateTime.year); 1116 | } 1117 | 1118 | /*! 1119 | @brief send a single sleep record 1120 | @param sleepTime 1121 | sleep time in minutes 1122 | @param type 1123 | sleep type (0 - light, 1 - deep) 1124 | @param dateTime 1125 | date and time of the record 1126 | */ 1127 | void ChronosESP32::sendSleepRecord(uint16_t sleepTime, SleepType type, DateTime dateTime) 1128 | { 1129 | sendSleepRecord(sleepTime, type, dateTime.minute, dateTime.hour, dateTime.day, dateTime.month, dateTime.year); 1130 | } 1131 | 1132 | /*! 1133 | @brief charging status of the phone 1134 | */ 1135 | bool ChronosESP32::isPhoneCharging() 1136 | { 1137 | return _phoneCharging; 1138 | } 1139 | 1140 | /*! 1141 | @brief battery level of the phone 1142 | */ 1143 | uint8_t ChronosESP32::getPhoneBattery() 1144 | { 1145 | return _phoneBatteryLevel; 1146 | } 1147 | 1148 | /*! 1149 | @brief app version code 1150 | */ 1151 | int ChronosESP32::getAppCode() 1152 | { 1153 | return _appCode; 1154 | } 1155 | /*! 1156 | @brief app version name 1157 | */ 1158 | 1159 | String ChronosESP32::getAppVersion() 1160 | { 1161 | return _appVersion; 1162 | } 1163 | 1164 | /*! 1165 | @brief get navigation data 1166 | */ 1167 | Navigation ChronosESP32::getNavigation() 1168 | { 1169 | return _navigation; 1170 | } 1171 | 1172 | /*! 1173 | @brief get the app name from the notification id 1174 | @param id 1175 | identifier of the app icon 1176 | */ 1177 | String ChronosESP32::appName(int id) 1178 | { 1179 | switch (id) 1180 | { 1181 | case 0x03: 1182 | return "Message"; 1183 | case 0x04: 1184 | return "Mail"; 1185 | case 0x07: 1186 | return "Tencent"; 1187 | case 0x08: 1188 | return "Skype"; 1189 | case 0x09: 1190 | return "Wechat"; 1191 | case 0x0A: 1192 | return "WhatsApp"; 1193 | case 0x0B: 1194 | return "Gmail"; 1195 | case 0x0E: 1196 | return "Line"; 1197 | case 0x0F: 1198 | return "Twitter"; 1199 | case 0x10: 1200 | return "Facebook"; 1201 | case 0x11: 1202 | return "Messenger"; 1203 | case 0x12: 1204 | return "Instagram"; 1205 | case 0x13: 1206 | return "Weibo"; 1207 | case 0x14: 1208 | return "KakaoTalk"; 1209 | case 0x16: 1210 | return "Viber"; 1211 | case 0x17: 1212 | return "Vkontakte"; 1213 | case 0x18: 1214 | return "Telegram"; 1215 | case 0x1B: 1216 | return "DingTalk"; 1217 | case 0x20: 1218 | return "WhatsApp Business"; 1219 | case 0x22: 1220 | return "WearFit Pro"; 1221 | case 0xC0: 1222 | return "Chronos"; 1223 | default: 1224 | return "Message"; 1225 | } 1226 | } 1227 | 1228 | /*! 1229 | @brief onConnect from BLEServerCallbacks 1230 | @param pServer 1231 | BLE server object 1232 | @param connInfo 1233 | connection information 1234 | */ 1235 | void ChronosESP32::onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) 1236 | { 1237 | _connected = true; 1238 | if (connectionChangeCallback != nullptr) 1239 | { 1240 | connectionChangeCallback(true); 1241 | } 1242 | } 1243 | 1244 | /*! 1245 | @brief onDisconnect from BLEServerCallbacks 1246 | @param pServer 1247 | BLE server object 1248 | @param connInfo 1249 | connection information 1250 | @param reason 1251 | disconnect reason 1252 | */ 1253 | void ChronosESP32::onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) 1254 | { 1255 | _connected = false; 1256 | _cameraReady = false; 1257 | BLEDevice::startAdvertising(); 1258 | _touch.state = false; // release touch 1259 | 1260 | if (_navigation.active) 1261 | { 1262 | _navigation.active = false; 1263 | if (configurationReceivedCallback != nullptr) 1264 | { 1265 | configurationReceivedCallback(CF_NAV_DATA, _navigation.active ? 1 : 0, 0); 1266 | } 1267 | } 1268 | 1269 | if (connectionChangeCallback != nullptr) 1270 | { 1271 | connectionChangeCallback(false); 1272 | } 1273 | } 1274 | 1275 | /*! 1276 | @brief onSubscribe to BLECharacteristicCallbacks 1277 | @param pCharacteristic 1278 | the BLECharacteristic object 1279 | @param connInfo 1280 | connection information 1281 | @param subValue 1282 | subcribe value 1283 | */ 1284 | void ChronosESP32::onSubscribe(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo, uint16_t subValue) 1285 | { 1286 | if (pCharacteristic == pCharacteristicTX) 1287 | { 1288 | _subscribed = subValue == 1; 1289 | 1290 | if (_subscribed) 1291 | { 1292 | _infoTimer.time = millis(); 1293 | _infoTimer.active = true; 1294 | } 1295 | } 1296 | } 1297 | 1298 | /*! 1299 | @brief onWrite from BLECharacteristicCallbacks 1300 | @param pCharacteristic 1301 | the BLECharacteristic object 1302 | @param connInfo 1303 | connection information 1304 | */ 1305 | void ChronosESP32::onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) 1306 | { 1307 | std::string pData = pCharacteristic->getValue(); 1308 | int len = pData.length(); 1309 | if (len > 0) 1310 | { 1311 | if (rawDataReceivedCallback != nullptr) 1312 | { 1313 | rawDataReceivedCallback((uint8_t *)pData.data(), len); 1314 | } 1315 | 1316 | if ((pData[0] == 0xAB || pData[0] == 0xEA) && (pData[3] == 0xFE || pData[3] == 0xFF)) 1317 | { 1318 | // start of data, assign length from packet 1319 | _incomingData.length = pData[1] * 256 + pData[2] + 3; 1320 | // copy data to incomingBuffer 1321 | for (int i = 0; i < len; i++) 1322 | { 1323 | _incomingData.data[i] = pData[i]; 1324 | } 1325 | 1326 | if (_incomingData.length <= len) 1327 | { 1328 | // complete packet assembled 1329 | dataReceived(); 1330 | } 1331 | else 1332 | { 1333 | // data is still being assembled 1334 | // Serial.println("Incomplete"); 1335 | } 1336 | } 1337 | else 1338 | { 1339 | int j = 20 + (pData[0] * 19); // data packet position 1340 | // copy data to incomingBuffer 1341 | for (int i = 0; i < len; i++) 1342 | { 1343 | _incomingData.data[j + i] = pData[i + 1]; 1344 | } 1345 | 1346 | if (_incomingData.length <= len + j - 1) 1347 | { 1348 | // complete packet assembled 1349 | dataReceived(); 1350 | } 1351 | else 1352 | { 1353 | // data is still being assembled 1354 | // Serial.println("Incomplete"); 1355 | } 1356 | } 1357 | 1358 | if (pData[0] == 0xB0) 1359 | { 1360 | // bin watchface chunk info 1361 | } 1362 | 1363 | if (pData[0] == 0xAF) 1364 | { 1365 | // bin watchface chunk data 1366 | } 1367 | } 1368 | } 1369 | 1370 | void ChronosESP32::splitTitle(const String &input, String &title, String &message, int icon) 1371 | { 1372 | int index = input.indexOf(':'); // Find the first occurrence of ':' 1373 | int newlineIndex = input.indexOf('\n'); // Find the first occurrence of '\n' 1374 | 1375 | if (index != -1 && index < 30 && (newlineIndex == -1 || newlineIndex > index)) 1376 | { 1377 | // Split only if ':' is before index 30 and there's no '\n' before it 1378 | title = input.substring(0, index); 1379 | message = input.substring(index + 1); 1380 | } 1381 | else 1382 | { 1383 | title = appName(icon); // No valid ':' before index 30, or '\n' appears before ':' 1384 | message = input; // Keep the full string in message 1385 | } 1386 | } 1387 | 1388 | /*! 1389 | @brief dataReceived function, called after data packets have been assembled 1390 | */ 1391 | void ChronosESP32::dataReceived() 1392 | { 1393 | int len = _incomingData.length; 1394 | 1395 | if (dataReceivedCallback != nullptr) 1396 | { 1397 | dataReceivedCallback(_incomingData.data, _incomingData.length); 1398 | } 1399 | if (_incomingData.data[0] == 0xAB) 1400 | { 1401 | switch (_incomingData.data[4]) 1402 | { 1403 | 1404 | case 0x20: 1405 | if (_incomingData.data[3] == 0xFE) 1406 | { 1407 | if (configurationReceivedCallback != nullptr) 1408 | { 1409 | configurationReceivedCallback(CF_SYNCED, 0, 0); 1410 | } 1411 | } 1412 | break; 1413 | case 0x23: 1414 | if (configurationReceivedCallback != nullptr) 1415 | { 1416 | configurationReceivedCallback(CF_RST, 0, 0); 1417 | } 1418 | break; 1419 | case 0x31: 1420 | switch (_incomingData.data[5]) 1421 | { 1422 | case 0x0A: 1423 | if (healthRequestCallback != nullptr) 1424 | { 1425 | healthRequestCallback(HR_HEART_RATE_MEASURE, _incomingData.data[6]); 1426 | } 1427 | break; 1428 | case 0x12: 1429 | if (healthRequestCallback != nullptr) 1430 | { 1431 | healthRequestCallback(HR_BLOOD_OXYGEN_MEASURE, _incomingData.data[6]); 1432 | } 1433 | break; 1434 | case 0x22: 1435 | if (healthRequestCallback != nullptr) 1436 | { 1437 | healthRequestCallback(HR_BLOOD_PRESSURE_MEASURE, _incomingData.data[6]); 1438 | } 1439 | break; 1440 | } 1441 | break; 1442 | case 0x32: 1443 | if (healthRequestCallback != nullptr) 1444 | { 1445 | healthRequestCallback(HR_MEASURE_ALL, _incomingData.data[6]); 1446 | } 1447 | break; 1448 | case 0x51: 1449 | switch (_incomingData.data[5]) 1450 | { 1451 | case 0x80: 1452 | if (healthRequestCallback != nullptr) 1453 | { 1454 | healthRequestCallback(HR_STEPS_RECORDS, true); 1455 | } 1456 | break; 1457 | } 1458 | 1459 | break; 1460 | case 0x52: 1461 | switch (_incomingData.data[5]) 1462 | { 1463 | case 0x80: 1464 | if (healthRequestCallback != nullptr) 1465 | { 1466 | healthRequestCallback(HR_SLEEP_RECORDS, true); 1467 | } 1468 | break; 1469 | } 1470 | 1471 | break; 1472 | case 0x53: 1473 | if (configurationReceivedCallback != nullptr) 1474 | { 1475 | uint8_t hour = _incomingData.data[7]; 1476 | uint8_t minute = _incomingData.data[8]; 1477 | uint8_t hour2 = _incomingData.data[9]; 1478 | uint8_t minute2 = _incomingData.data[10]; 1479 | bool enabled = _incomingData.data[6]; 1480 | uint32_t interval = ((uint32_t)_incomingData.data[11] << 16) | (uint16_t)_incomingData.data[6]; 1481 | uint32_t wtr = ((uint32_t)hour << 24) | ((uint32_t)minute << 16) | ((uint32_t)hour2 << 8) | ((uint32_t)minute2); 1482 | configurationReceivedCallback(CF_WATER, interval, wtr); 1483 | } 1484 | break; 1485 | 1486 | case 0x71: 1487 | if (configurationReceivedCallback != nullptr) 1488 | { 1489 | configurationReceivedCallback(CF_FIND, 0, 0); 1490 | } 1491 | break; 1492 | 1493 | case 0x72: 1494 | { 1495 | int icon = _incomingData.data[6]; 1496 | int state = _incomingData.data[7]; 1497 | 1498 | String message = ""; 1499 | for (int i = 8; i < len; i++) 1500 | { 1501 | message += (char)_incomingData.data[i]; 1502 | } 1503 | 1504 | if (icon == 0x01) 1505 | { 1506 | // ringer command 1507 | if (ringerAlertCallback != nullptr) 1508 | { 1509 | ringerAlertCallback(message, true); 1510 | } 1511 | break; 1512 | } 1513 | if (icon == 0x02) 1514 | { 1515 | // cancel ringer command 1516 | if (ringerAlertCallback != nullptr) 1517 | { 1518 | ringerAlertCallback(message, false); 1519 | } 1520 | break; 1521 | } 1522 | if (state == 0x02) 1523 | { 1524 | _notificationIndex++; 1525 | _notifications[_notificationIndex % NOTIF_SIZE].icon = icon; 1526 | _notifications[_notificationIndex % NOTIF_SIZE].app = appName(icon); 1527 | _notifications[_notificationIndex % NOTIF_SIZE].time = this->getTime("%H:%M"); 1528 | splitTitle(message, _notifications[_notificationIndex % NOTIF_SIZE].title, _notifications[_notificationIndex % NOTIF_SIZE].message, icon); 1529 | 1530 | if (notificationReceivedCallback != nullptr) 1531 | { 1532 | notificationReceivedCallback(_notifications[_notificationIndex % NOTIF_SIZE]); 1533 | } 1534 | } 1535 | } 1536 | break; 1537 | case 0x73: 1538 | { 1539 | uint8_t hour = _incomingData.data[8]; 1540 | uint8_t minute = _incomingData.data[9]; 1541 | uint8_t repeat = _incomingData.data[10]; 1542 | bool enabled = _incomingData.data[7]; 1543 | uint32_t index = (uint32_t)_incomingData.data[6]; 1544 | _alarms[index % ALARM_SIZE].hour = hour; 1545 | _alarms[index % ALARM_SIZE].minute = minute; 1546 | _alarms[index % ALARM_SIZE].repeat = repeat; 1547 | _alarms[index % ALARM_SIZE].enabled = enabled; 1548 | if (configurationReceivedCallback != nullptr) 1549 | { 1550 | uint32_t alarm = ((uint32_t)hour << 24) | ((uint32_t)minute << 16) | ((uint32_t)repeat << 8) | ((uint32_t)enabled); 1551 | configurationReceivedCallback(CF_ALARM, index, alarm); 1552 | } 1553 | } 1554 | break; 1555 | case 0x74: 1556 | if (configurationReceivedCallback != nullptr) 1557 | { 1558 | // user.step, user.age, user.height, user.weight, si, user.target/1000, temp 1559 | uint8_t age = _incomingData.data[7]; 1560 | uint8_t height = _incomingData.data[8]; 1561 | uint8_t weight = _incomingData.data[9]; 1562 | uint8_t step = _incomingData.data[6]; 1563 | uint32_t u1 = ((uint32_t)age << 24) | ((uint32_t)height << 16) | ((uint32_t)weight << 8) | ((uint32_t)step); 1564 | uint8_t unit = _incomingData.data[10]; 1565 | uint8_t target = _incomingData.data[11]; 1566 | uint8_t temp = _incomingData.data[12]; 1567 | uint32_t u2 = ((uint32_t)unit << 24) | ((uint32_t)target << 16) | ((uint32_t)temp << 8) | ((uint32_t)step); 1568 | 1569 | configurationReceivedCallback(CF_USER, u1, u2); 1570 | } 1571 | break; 1572 | case 0x75: 1573 | if (configurationReceivedCallback != nullptr) 1574 | { 1575 | uint8_t hour = _incomingData.data[7]; 1576 | uint8_t minute = _incomingData.data[8]; 1577 | uint8_t hour2 = _incomingData.data[9]; 1578 | uint8_t minute2 = _incomingData.data[10]; 1579 | bool enabled = _incomingData.data[6]; 1580 | uint32_t interval = ((uint32_t)_incomingData.data[11] << 16) | (uint16_t)_incomingData.data[6]; 1581 | uint32_t sed = ((uint32_t)hour << 24) | ((uint32_t)minute << 16) | ((uint32_t)hour2 << 8) | ((uint32_t)minute2); 1582 | configurationReceivedCallback(CF_SED, interval, sed); 1583 | } 1584 | break; 1585 | case 0x76: 1586 | if (configurationReceivedCallback != nullptr) 1587 | { 1588 | uint8_t hour = _incomingData.data[7]; 1589 | uint8_t minute = _incomingData.data[8]; 1590 | uint8_t hour2 = _incomingData.data[9]; 1591 | uint8_t minute2 = _incomingData.data[10]; 1592 | bool enabled = (uint32_t)_incomingData.data[6]; 1593 | uint32_t qt = ((uint32_t)hour << 24) | ((uint32_t)minute << 16) | ((uint32_t)hour2 << 8) | ((uint32_t)minute2); 1594 | configurationReceivedCallback(CF_QUIET, enabled, qt); 1595 | } 1596 | break; 1597 | case 0x77: 1598 | if (configurationReceivedCallback != nullptr) 1599 | { 1600 | configurationReceivedCallback(CF_RTW, 0, (uint32_t)_incomingData.data[6]); 1601 | } 1602 | break; 1603 | case 0x78: 1604 | if (configurationReceivedCallback != nullptr) 1605 | { 1606 | configurationReceivedCallback(CF_HOURLY, 0, (uint32_t)_incomingData.data[6]); 1607 | } 1608 | break; 1609 | case 0x79: 1610 | _cameraReady = ((uint8_t)_incomingData.data[6] == 1); 1611 | if (configurationReceivedCallback != nullptr) 1612 | { 1613 | configurationReceivedCallback(CF_CAMERA, 0, (uint32_t)_incomingData.data[6]); 1614 | } 1615 | break; 1616 | case 0x7B: 1617 | if (configurationReceivedCallback != nullptr) 1618 | { 1619 | configurationReceivedCallback(CF_LANG, 0, (uint32_t)_incomingData.data[6]); 1620 | } 1621 | break; 1622 | case 0x7C: 1623 | _hour24 = ((uint8_t)_incomingData.data[6] == 0); 1624 | if (configurationReceivedCallback != nullptr) 1625 | { 1626 | configurationReceivedCallback(CF_HR24, 0, (uint32_t)(_incomingData.data[6] == 0)); 1627 | } 1628 | break; 1629 | case 0x7E: 1630 | { 1631 | _weatherTime = this->getTime("%H:%M"); 1632 | _weatherSize = 0; 1633 | for (int k = 0; k < (len - 6) / 2; k++) 1634 | { 1635 | if (k >= WEATHER_SIZE) 1636 | { 1637 | break; 1638 | } 1639 | int icon = _incomingData.data[(k * 2) + 6] >> 4; 1640 | int sign = (_incomingData.data[(k * 2) + 6] & 1) ? -1 : 1; 1641 | int temp = ((int)_incomingData.data[(k * 2) + 7]) * sign; 1642 | int dy = this->getDayofWeek() + k; 1643 | _weather[k].day = dy % 7; 1644 | _weather[k].icon = icon; 1645 | _weather[k].temp = temp; 1646 | _weatherSize++; 1647 | } 1648 | if (configurationReceivedCallback != nullptr) 1649 | { 1650 | configurationReceivedCallback(CF_WEATHER, 1, 0); 1651 | } 1652 | } 1653 | break; 1654 | case 0x88: 1655 | { 1656 | for (int k = 0; k < (len - 6) / 2; k++) 1657 | { 1658 | if (k >= WEATHER_SIZE) 1659 | { 1660 | break; 1661 | } 1662 | int signH = (_incomingData.data[(k * 2) + 6] >> 7 & 1) ? -1 : 1; 1663 | int tempH = ((int)_incomingData.data[(k * 2) + 6] & 0x7F) * signH; 1664 | 1665 | int signL = (_incomingData.data[(k * 2) + 7] >> 7 & 1) ? -1 : 1; 1666 | int tempL = ((int)_incomingData.data[(k * 2) + 7] & 0x7F) * signL; 1667 | 1668 | _weather[k].high = tempH; 1669 | _weather[k].low = tempL; 1670 | } 1671 | if (configurationReceivedCallback != nullptr) 1672 | { 1673 | configurationReceivedCallback(CF_WEATHER, 2, 0); 1674 | } 1675 | } 1676 | break; 1677 | case 0x8A: 1678 | { 1679 | _weather[0].uv = _incomingData.data[6]; 1680 | _weather[0].pressure = (_incomingData.data[7] * 256) + _incomingData.data[8]; 1681 | } 1682 | break; 1683 | case 0x7F: 1684 | if (configurationReceivedCallback != nullptr) 1685 | { 1686 | uint8_t hour = _incomingData.data[7]; 1687 | uint8_t minute = _incomingData.data[8]; 1688 | uint8_t hour2 = _incomingData.data[9]; 1689 | uint8_t minute2 = _incomingData.data[10]; 1690 | bool enabled = _incomingData.data[6]; 1691 | uint32_t slp = ((uint32_t)hour << 24) | ((uint32_t)minute << 16) | ((uint32_t)hour2 << 8) | ((uint32_t)minute2); 1692 | configurationReceivedCallback(CF_SLEEP, enabled, slp); 1693 | } 1694 | break; 1695 | case 0x91: 1696 | 1697 | if (_incomingData.data[3] == 0xFE) 1698 | { 1699 | _phoneCharging = _incomingData.data[6] == 1; 1700 | _phoneBatteryLevel = _incomingData.data[7]; 1701 | if (configurationReceivedCallback != nullptr) 1702 | { 1703 | configurationReceivedCallback(CF_PBAT, _incomingData.data[6], _phoneBatteryLevel); 1704 | } 1705 | } 1706 | 1707 | break; 1708 | case 0x93: 1709 | if (configurationReceivedCallback != nullptr) 1710 | { 1711 | configurationReceivedCallback(CF_TIME, 0, 0); 1712 | } 1713 | 1714 | this->setTime(_incomingData.data[13], _incomingData.data[12], _incomingData.data[11], _incomingData.data[10], _incomingData.data[9], _incomingData.data[7] * 256 + _incomingData.data[8]); 1715 | 1716 | if (configurationReceivedCallback != nullptr) 1717 | { 1718 | configurationReceivedCallback(CF_TIME, 1, 0); 1719 | } 1720 | break; 1721 | case 0x9C: 1722 | if (configurationReceivedCallback != nullptr) 1723 | { 1724 | uint32_t color = ((uint32_t)_incomingData.data[5] << 16) | ((uint32_t)_incomingData.data[6] << 8) | (uint32_t)_incomingData.data[7]; 1725 | uint32_t select = ((uint32_t)(_incomingData.data[8]) << 16) | (uint32_t)_incomingData.data[9]; 1726 | configurationReceivedCallback(CF_FONT, color, select); 1727 | } 1728 | break; 1729 | case 0xA2: 1730 | { 1731 | int pos = _incomingData.data[5]; 1732 | _contacts[pos].name = ""; 1733 | for (int i = 6; i < len; i++) 1734 | { 1735 | _contacts[pos].name += (char)_incomingData.data[i]; 1736 | } 1737 | } 1738 | break; 1739 | case 0xA3: 1740 | { 1741 | int pos = _incomingData.data[5]; 1742 | int nSize = _incomingData.data[6]; 1743 | _contacts[pos].number = ""; 1744 | for (int i = 7; i < len; i++) 1745 | { 1746 | char digit[3]; 1747 | sprintf(digit, "%02X", _incomingData.data[i]); 1748 | // reverse characters 1749 | digit[2] = digit[0]; // save digit at 0 to 2 1750 | digit[0] = digit[1]; // swap 1 to 0 1751 | digit[1] = digit[2]; // swap saved 2 to 1 1752 | digit[2] = 0; // null termination character 1753 | _contacts[pos].number += digit; 1754 | } 1755 | _contacts[pos].number.replace("A", "+"); 1756 | _contacts[pos].number = _contacts[pos].number.substring(0, nSize); 1757 | 1758 | if (configurationReceivedCallback != nullptr && pos == (_contactSize - 1)) 1759 | { 1760 | configurationReceivedCallback(CF_CONTACT, 1, uint32_t(_sosContact << 8) | uint32_t(_contactSize)); 1761 | } 1762 | } 1763 | break; 1764 | case 0xA5: 1765 | _sosContact = _incomingData.data[6]; 1766 | _contactSize = _incomingData.data[7]; 1767 | if (configurationReceivedCallback != nullptr) 1768 | { 1769 | configurationReceivedCallback(CF_CONTACT, 0, uint32_t(_sosContact << 8) | uint32_t(_contactSize)); 1770 | } 1771 | break; 1772 | case 0xA8: 1773 | if (_incomingData.data[3] == 0xFE) 1774 | { 1775 | // end of qr data 1776 | int size = _incomingData.data[5]; // number of links received 1777 | if (configurationReceivedCallback != nullptr) 1778 | { 1779 | configurationReceivedCallback(CF_QR, 1, size); 1780 | } 1781 | } 1782 | if (_incomingData.data[3] == 0xFF) 1783 | { 1784 | // receiving qr data 1785 | int index = _incomingData.data[5]; // index of the curent link 1786 | _qrLinks[index] = ""; // clear existing 1787 | for (int i = 6; i < len; i++) 1788 | { 1789 | _qrLinks[index] += (char)_incomingData.data[i]; 1790 | } 1791 | if (configurationReceivedCallback != nullptr) 1792 | { 1793 | configurationReceivedCallback(CF_QR, 0, index); 1794 | } 1795 | } 1796 | break; 1797 | case 0xBF: 1798 | if (_incomingData.data[3] == 0xFE) 1799 | { 1800 | _touch.state = _incomingData.data[5] == 1; 1801 | _touch.x = uint32_t(_incomingData.data[6] << 8) | uint32_t(_incomingData.data[7]); 1802 | _touch.y = uint32_t(_incomingData.data[8] << 8) | uint32_t(_incomingData.data[9]); 1803 | } 1804 | break; 1805 | case 0xCA: 1806 | if (_incomingData.data[3] == 0xFE) 1807 | { 1808 | _appCode = (_incomingData.data[6] * 256) + _incomingData.data[7]; 1809 | _appVersion = ""; 1810 | for (int i = 8; i < len; i++) 1811 | { 1812 | _appVersion += (char)_incomingData.data[i]; 1813 | } 1814 | if (configurationReceivedCallback != nullptr) 1815 | { 1816 | configurationReceivedCallback(CF_APP, _appCode, 0); 1817 | } 1818 | _sendESP = true; 1819 | } 1820 | break; 1821 | case 0xCC: 1822 | if (_incomingData.data[3] == 0xFE) 1823 | { 1824 | setChunkedTransfer(_incomingData.data[5] != 0x00); 1825 | } 1826 | break; 1827 | case 0xEE: 1828 | if (_incomingData.data[3] == 0xFE) 1829 | { 1830 | // navigation icon data received 1831 | uint8_t pos = _incomingData.data[6]; 1832 | uint32_t crc = uint32_t(_incomingData.data[7] << 24) | uint32_t(_incomingData.data[8] << 16) | uint32_t(_incomingData.data[9] << 8) | uint32_t(_incomingData.data[10]); 1833 | for (int i = 0; i < 96; i++) 1834 | { 1835 | _navigation.icon[i + (96 * pos)] = _incomingData.data[11 + i]; 1836 | } 1837 | 1838 | if (configurationReceivedCallback != nullptr) 1839 | { 1840 | configurationReceivedCallback(CF_NAV_ICON, pos, crc); 1841 | } 1842 | } 1843 | break; 1844 | case 0xEF: 1845 | if (_incomingData.data[3] == 0xFE) 1846 | { 1847 | // navigation data received 1848 | if (_incomingData.data[5] == 0x00) 1849 | { 1850 | _navigation.active = false; 1851 | _navigation.eta = "Navigation"; 1852 | _navigation.title = "Chronos"; 1853 | _navigation.duration = "Inactive"; 1854 | _navigation.distance = ""; 1855 | _navigation.speed = ""; 1856 | _navigation.directions = "Start navigation on Google maps"; 1857 | _navigation.hasIcon = false; 1858 | _navigation.isNavigation = false; 1859 | _navigation.iconCRC = 0xFFFFFFFF; 1860 | } 1861 | else if (_incomingData.data[5] == 0xFF) 1862 | { 1863 | _navigation.active = true; 1864 | _navigation.title = "Chronos"; 1865 | _navigation.duration = "Disabled"; 1866 | _navigation.distance = ""; 1867 | _navigation.speed = ""; 1868 | _navigation.eta = "Navigation"; 1869 | _navigation.directions = "Check Chronos app settings"; 1870 | _navigation.hasIcon = false; 1871 | _navigation.isNavigation = false; 1872 | _navigation.iconCRC = 0xFFFFFFFF; 1873 | } 1874 | else if (_incomingData.data[5] == 0x80) 1875 | { 1876 | _navigation.active = true; 1877 | _navigation.hasIcon = _incomingData.data[6] == 1; 1878 | _navigation.isNavigation = _incomingData.data[7] == 1; 1879 | _navigation.iconCRC = uint32_t(_incomingData.data[8] << 24) | uint32_t(_incomingData.data[9] << 16) | uint32_t(_incomingData.data[10] << 8) | uint32_t(_incomingData.data[11]); 1880 | 1881 | int i = 12; 1882 | _navigation.title = ""; 1883 | while (_incomingData.data[i] != 0 && i < len) 1884 | { 1885 | _navigation.title += char(_incomingData.data[i]); 1886 | i++; 1887 | } 1888 | i++; 1889 | 1890 | _navigation.duration = ""; 1891 | while (_incomingData.data[i] != 0 && i < len) 1892 | { 1893 | _navigation.duration += char(_incomingData.data[i]); 1894 | i++; 1895 | } 1896 | i++; 1897 | 1898 | _navigation.distance = ""; 1899 | while (_incomingData.data[i] != 0 && i < len) 1900 | { 1901 | _navigation.distance += char(_incomingData.data[i]); 1902 | i++; 1903 | } 1904 | i++; 1905 | 1906 | _navigation.eta = ""; 1907 | while (_incomingData.data[i] != 0 && i < len) 1908 | { 1909 | _navigation.eta += char(_incomingData.data[i]); 1910 | i++; 1911 | } 1912 | i++; 1913 | 1914 | _navigation.directions = ""; 1915 | while (_incomingData.data[i] != 0 && i < len) 1916 | { 1917 | _navigation.directions += char(_incomingData.data[i]); 1918 | i++; 1919 | } 1920 | i++; 1921 | 1922 | _navigation.speed = ""; 1923 | while (_incomingData.data[i] != 0 && i < len) 1924 | { 1925 | _navigation.speed += char(_incomingData.data[i]); 1926 | i++; 1927 | } 1928 | i++; 1929 | } 1930 | if (configurationReceivedCallback != nullptr) 1931 | { 1932 | configurationReceivedCallback(CF_NAV_DATA, _navigation.active ? 1 : 0, 0); 1933 | } 1934 | } 1935 | break; 1936 | } 1937 | } 1938 | else if (_incomingData.data[0] == 0xEA) 1939 | { 1940 | switch (_incomingData.data[4]) 1941 | { 1942 | case 0x7E: 1943 | /* code */ 1944 | switch (_incomingData.data[5]) 1945 | { 1946 | case 0x01: 1947 | { 1948 | String city = ""; 1949 | for (int c = 7; c < len; c++) 1950 | { 1951 | city += (char)_incomingData.data[c]; 1952 | } 1953 | _weatherCity = city; 1954 | if (configurationReceivedCallback != nullptr) 1955 | { 1956 | configurationReceivedCallback(CF_WEATHER, 0, 1); 1957 | } 1958 | } 1959 | break; 1960 | case 0x02: 1961 | { 1962 | int size = _incomingData.data[6]; 1963 | int hour = _incomingData.data[7]; 1964 | for (int z = 0; z < size; z++) 1965 | { 1966 | if (hour + z >= FORECAST_SIZE) 1967 | { 1968 | break; 1969 | } 1970 | int icon = _incomingData.data[8 + (6 * z)] >> 4; 1971 | int sign = (_incomingData.data[8 + (6 * z)] & 1) ? -1 : 1; 1972 | int temp = ((int)_incomingData.data[9 + (6 * z)]) * sign; 1973 | 1974 | _hourlyForecast[hour + z].day = this->getDayofYear(); 1975 | _hourlyForecast[hour + z].hour = hour + z; 1976 | _hourlyForecast[hour + z].wind = (_incomingData.data[10 + (6 * z)] * 256) + _incomingData.data[11 + (6 * z)]; 1977 | _hourlyForecast[hour + z].humidity = _incomingData.data[12 + (6 * z)]; 1978 | _hourlyForecast[hour + z].uv = _incomingData.data[13 + (6 * z)]; 1979 | _hourlyForecast[hour + z].icon = icon; 1980 | _hourlyForecast[hour + z].temp = temp; 1981 | } 1982 | } 1983 | break; 1984 | } /* END switch (_incomingData.data[5]) */ 1985 | break; 1986 | 1987 | case 0x7F: 1988 | if (_incomingData.data[3] == 0xFE) 1989 | { 1990 | uint8_t payloadLen = _incomingData.data[6]; 1991 | const uint8_t *payload = &_incomingData.data[7]; 1992 | 1993 | // Read coordinates (Little Endian) 1994 | float latitude; 1995 | float longitude; 1996 | memcpy(&latitude, payload, 4); 1997 | memcpy(&longitude, payload + 4, 4); 1998 | 1999 | // Move pointer past coordinates 2000 | int index = 8; 2001 | 2002 | // Read city (null-terminated) 2003 | String city = ""; 2004 | while (index < payloadLen && payload[index] != 0x00) 2005 | city += (char)payload[index++]; 2006 | index++; // skip null 2007 | 2008 | // Read region (null-terminated) 2009 | String region = ""; 2010 | while (index < payloadLen && payload[index] != 0x00) 2011 | region += (char)payload[index++]; 2012 | index++; // skip null 2013 | 2014 | // Remaining bytes = country 2015 | String country = ""; 2016 | while (index < payloadLen) 2017 | country += (char)payload[index++]; 2018 | 2019 | // Assign to struct 2020 | _weatherLocation.city = city; 2021 | _weatherLocation.region = region; 2022 | _weatherLocation.country = country; 2023 | _weatherLocation.latitude = latitude; 2024 | _weatherLocation.longitude = longitude; 2025 | } 2026 | 2027 | break; 2028 | } 2029 | } 2030 | } 2031 | --------------------------------------------------------------------------------