diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..1d953f4b --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index a66b3e93..51449c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ +.direnv .pio .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch out/ +.direnv/ +.DS_Store +.vscode/settings.json diff --git a/README.md b/README.md index 1413a4b4..b4943dea 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,52 @@ MeshCore provides the ability to create wireless mesh networks, similar to Mesht ## 🚀 How to Get Started -Andy Kirby has published a very useful [intro video](https://www.youtube.com/watch?v=t1qne8uJBAc) which explains the steps for beginners. +- Watch the [MeshCore Intro Video](https://www.youtube.com/watch?v=t1qne8uJBAc) by Andy Kirby. +- Read through our [Frequently Asked Questions](./docs/faq.md) section. +- Flash the MeshCore firmware on a supported device. +- Connect with a supported client. -For developers, install [PlatformIO](https://docs.platformio.org) in Visual Studio Code. -Download & Open the MeshCore repository. -Select a Sample Application: Choose from chat, repeater, other example app. -Monitor & Communicate using the Serial Monitor (e.g., Serial USB Terminal on Android). +For developers; -📁 Included Example Applications -* 📡 Terminal Chat: Secure text communication between devices. -* 📡 Simple Repeater: Extends network coverage by relaying messages. -* 📡 Companion Radio: For use with an external chat app, over BLE or USB. -* 📡 Room Server: A simple BBS server for shared Posts. +- Install [PlatformIO](https://docs.platformio.org) in [Visual Studio Code](https://code.visualstudio.com). +- Clone and open the MeshCore repository in Visual Studio Code. +- See the example applications you can modify and run: + - [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or WiFi. + - [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages. + - [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts. + - [Simple Secure Chat](./examples/simple_secure_chat) - Secure terminal based text communication between devices. + +The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android. + +## ⚡️ MeshCore Flasher + +We have prebuilt firmware ready to flash on supported devices. + +- Launch https://flasher.meshcore.co.uk +- Select a supported device +- Flash one of the firmware types: + - Companion, Repeater or Room Server +- Once flashing is complete, you can connect with one of the MeshCore clients below. + +## 📱 MeshCore Clients + +**Companion Firmware** + +The companion firmware can be connected to via BLE, USB or WiFi depending on the firmware type you flashed. + +- Web: https://app.meshcore.nz +- Android: https://play.google.com/store/apps/details?id=com.liamcottle.meshcore.android +- iOS: https://apps.apple.com/us/app/meshcore/id6742354151?platform=iphone +- NodeJS: https://github.com/liamcottle/meshcore.js +- Python: https://github.com/fdlamotte/meshcore-cli + +**Repeater and Room Server Firmware** + +The repeater and room server firmwares can be setup via USB in the web config tool. + +- https://config.meshcore.dev + +They can also be managed via LoRa in the mobile app by using the Remote Management feature. ## 🛠 Hardware Compatibility @@ -53,6 +87,7 @@ MeshCore is designed for use with: * LilyGo TLora32 v1.6 ## 📜 License + MeshCore is open-source software released under the MIT License. You are free to use, modify, and distribute it for personal and commercial projects. ## Contributing @@ -62,9 +97,9 @@ For minor changes just submit your PR and I'll try to review it, but for anythin ## 📞 Get Support -Check out the GitHub Issues page to report bugs or request features. - -You will be able to find additional guides and components at [my site](https://buymeacoffee.com/ripplebiz), or [join Andy Kirby's Discord](https://discord.gg/GBxVx2JMAy) for discussions. +- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page. +- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz). +- Join [Andy Kirby's Discord](https://discord.gg/GBxVx2JMAy) to chat with the developers and get help from the community. ## RAK Wireless Board Support in PlatformIO diff --git a/arch/stm32/Adafruit_LittleFS_stm32/README.md b/arch/stm32/Adafruit_LittleFS_stm32/README.md new file mode 100644 index 00000000..27ab29e8 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/README.md @@ -0,0 +1 @@ +This is LittleFS from Adafruit, stripped from things that makes it not compile with stm32 (refs to TinyUSB and free_rtos, mostly) \ No newline at end of file diff --git a/arch/stm32/Adafruit_LittleFS_stm32/library.properties b/arch/stm32/Adafruit_LittleFS_stm32/library.properties new file mode 100644 index 00000000..750ed4d8 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/library.properties @@ -0,0 +1,10 @@ +name=Adafruit Little File System Libraries +version=0.11.0 +author=Adafruit +maintainer=Adafruit +sentence=Arduino library for ARM Little File System +paragraph=Arduino library for ARM Little File System +category=Data Storage +url=https://github.com/adafruit/Adafruit_nRF52_Arduino +architectures=* +includes=Adafruit_LittleFS.h diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp new file mode 100644 index 00000000..0c9c97b5 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.cpp @@ -0,0 +1,273 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include "Adafruit_LittleFS.h" + +//#include // for Serial + +using namespace Adafruit_LittleFS_Namespace; + +#define memclr(buffer, size) memset(buffer, 0, size) +#define varclr(_var) memclr(_var, sizeof(*(_var))) + +//--------------------------------------------------------------------+ +// Implementation +//--------------------------------------------------------------------+ + +Adafruit_LittleFS::Adafruit_LittleFS (void) + : Adafruit_LittleFS(NULL) +{ + +} + +Adafruit_LittleFS::Adafruit_LittleFS (struct lfs_config* cfg) +{ + varclr(&_lfs); + _lfs_cfg = cfg; + _mounted = false; +// _mutex = xSemaphoreCreateMutexStatic(&this->_MutexStorageSpace); +} + +Adafruit_LittleFS::~Adafruit_LittleFS () +{ + +} + +// Initialize and mount the file system +// Return true if mounted successfully else probably corrupted. +// User should format the disk and try again +bool Adafruit_LittleFS::begin (struct lfs_config * cfg) +{ + _lockFS(); + + bool ret; + // not a loop, just an quick way to short-circuit on error + do { + if (_mounted) { ret = true; break; } + if (cfg) { _lfs_cfg = cfg; } + if (nullptr == _lfs_cfg) { ret = false; break; } + // actually attempt to mount, and log error if one occurs + int err = lfs_mount(&_lfs, _lfs_cfg); + PRINT_LFS_ERR(err); + _mounted = (err == LFS_ERR_OK); + ret = _mounted; + } while(0); + + _unlockFS(); + return ret; +} + +// Tear down and unmount file system +void Adafruit_LittleFS::end(void) +{ + _lockFS(); + + if (_mounted) + { + _mounted = false; + int err = lfs_unmount(&_lfs); + PRINT_LFS_ERR(err); + (void)err; + } + + _unlockFS(); +} + +bool Adafruit_LittleFS::format (void) +{ + _lockFS(); + + int err = LFS_ERR_OK; + bool attemptMount = _mounted; + // not a loop, just an quick way to short-circuit on error + do + { + // if already mounted: umount first -> format -> remount + if (_mounted) + { + _mounted = false; + err = lfs_unmount(&_lfs); + if ( LFS_ERR_OK != err) { PRINT_LFS_ERR(err); break; } + } + err = lfs_format(&_lfs, _lfs_cfg); + if ( LFS_ERR_OK != err ) { PRINT_LFS_ERR(err); break; } + + if (attemptMount) + { + err = lfs_mount(&_lfs, _lfs_cfg); + if ( LFS_ERR_OK != err ) { PRINT_LFS_ERR(err); break; } + _mounted = true; + } + // success! + } while(0); + + _unlockFS(); + return LFS_ERR_OK == err; +} + +// Open a file or folder +Adafruit_LittleFS_Namespace::File Adafruit_LittleFS::open (char const *filepath, uint8_t mode) +{ + // No lock is required here ... the File() object will synchronize with the mutex provided + return Adafruit_LittleFS_Namespace::File(filepath, mode, *this); +} + +// Check if file or folder exists +bool Adafruit_LittleFS::exists (char const *filepath) +{ + struct lfs_info info; + _lockFS(); + + bool ret = (0 == lfs_stat(&_lfs, filepath, &info)); + + _unlockFS(); + return ret; +} + + +// Create a directory, create intermediate parent if needed +bool Adafruit_LittleFS::mkdir (char const *filepath) +{ + bool ret = true; + const char* slash = filepath; + if ( slash[0] == '/' ) slash++; // skip root '/' + + _lockFS(); + + // make intermediate parent directory(ies) + while ( NULL != (slash = strchr(slash, '/')) ) + { + char parent[slash - filepath + 1] = { 0 }; + memcpy(parent, filepath, slash - filepath); + + int rc = lfs_mkdir(&_lfs, parent); + if ( rc != LFS_ERR_OK && rc != LFS_ERR_EXIST ) + { + PRINT_LFS_ERR(rc); + ret = false; + break; + } + slash++; + } + // make the final requested directory + if (ret) + { + int rc = lfs_mkdir(&_lfs, filepath); + if ( rc != LFS_ERR_OK && rc != LFS_ERR_EXIST ) + { + PRINT_LFS_ERR(rc); + ret = false; + } + } + + _unlockFS(); + return ret; +} + +// Remove a file +bool Adafruit_LittleFS::remove (char const *filepath) +{ + _lockFS(); + + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); + + _unlockFS(); + return LFS_ERR_OK == err; +} + +// Rename a file +bool Adafruit_LittleFS::rename (char const *oldfilepath, char const *newfilepath) +{ + _lockFS(); + + int err = lfs_rename(&_lfs, oldfilepath, newfilepath); + PRINT_LFS_ERR(err); + + _unlockFS(); + return LFS_ERR_OK == err; +} + +// Remove a folder +bool Adafruit_LittleFS::rmdir (char const *filepath) +{ + _lockFS(); + + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); + + _unlockFS(); + return LFS_ERR_OK == err; +} + +// Remove a folder recursively +bool Adafruit_LittleFS::rmdir_r (char const *filepath) +{ + /* adafruit: lfs is modified to remove non-empty folder, + According to below issue, comment these 2 line won't corrupt filesystem + at least when using LFS v1. If moving to LFS v2, see tracked issue + to see if issues (such as the orphans in threaded linked list) are resolved. + https://github.com/ARMmbed/littlefs/issues/43 + */ + _lockFS(); + + int err = lfs_remove(&_lfs, filepath); + PRINT_LFS_ERR(err); + + _unlockFS(); + return LFS_ERR_OK == err; +} + +//------------- Debug -------------// +#if CFG_DEBUG + +const char* dbg_strerr_lfs (int32_t err) +{ + switch ( err ) + { + case LFS_ERR_OK : return "LFS_ERR_OK"; + case LFS_ERR_IO : return "LFS_ERR_IO"; + case LFS_ERR_CORRUPT : return "LFS_ERR_CORRUPT"; + case LFS_ERR_NOENT : return "LFS_ERR_NOENT"; + case LFS_ERR_EXIST : return "LFS_ERR_EXIST"; + case LFS_ERR_NOTDIR : return "LFS_ERR_NOTDIR"; + case LFS_ERR_ISDIR : return "LFS_ERR_ISDIR"; + case LFS_ERR_NOTEMPTY : return "LFS_ERR_NOTEMPTY"; + case LFS_ERR_BADF : return "LFS_ERR_BADF"; + case LFS_ERR_INVAL : return "LFS_ERR_INVAL"; + case LFS_ERR_NOSPC : return "LFS_ERR_NOSPC"; + case LFS_ERR_NOMEM : return "LFS_ERR_NOMEM"; + + default: + static char errcode[10]; + sprintf(errcode, "%ld", err); + return errcode; + } + + return NULL; +} + +#endif diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.h b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.h new file mode 100644 index 00000000..c183de1b --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS.h @@ -0,0 +1,102 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef ADAFRUIT_LITTLEFS_H_ +#define ADAFRUIT_LITTLEFS_H_ + +#include + +// Internal Flash uses ARM Little FileSystem +// https://github.com/ARMmbed/littlefs +#include "littlefs/lfs.h" +#include "Adafruit_LittleFS_File.h" +//#include "rtos.h" // tied to FreeRTOS for serialization + +class Adafruit_LittleFS +{ + public: + Adafruit_LittleFS (void); + Adafruit_LittleFS (struct lfs_config* cfg); + virtual ~Adafruit_LittleFS (); + + bool begin(struct lfs_config * cfg = NULL); + void end(void); + + // Open the specified file/directory with the supplied mode (e.g. read or + // write, etc). Returns a File object for interacting with the file. + // Note that currently only one file can be open at a time. + Adafruit_LittleFS_Namespace::File open (char const *filename, uint8_t mode = Adafruit_LittleFS_Namespace::FILE_O_READ); + + // Methods to determine if the requested file path exists. + bool exists (char const *filepath); + + // Create the requested directory hierarchy--if intermediate directories + // do not exist they will be created. + bool mkdir (char const *filepath); + + // Delete the file. + bool remove (char const *filepath); + + // Rename the file. + bool rename (char const *oldfilepath, char const *newfilepath); + + // Delete a folder (must be empty) + bool rmdir (char const *filepath); + + // Delete a folder (recursively) + bool rmdir_r (char const *filepath); + + // format file system + bool format (void); + + /*------------------------------------------------------------------*/ + /* INTERNAL USAGE ONLY + * Although declare as public, it is meant to be invoked by internal + * code. User should not call these directly + *------------------------------------------------------------------*/ + lfs_t* _getFS (void) { return &_lfs; } + void _lockFS (void) { }//xSemaphoreTake(_mutex, portMAX_DELAY); } + void _unlockFS(void) { }//xSemaphoreGive(_mutex); } + + protected: + bool _mounted; + struct lfs_config* _lfs_cfg; + lfs_t _lfs; +// SemaphoreHandle_t _mutex; + + private: +// StaticSemaphore_t _MutexStorageSpace; +}; + +#if !CFG_DEBUG + #define VERIFY_LFS(...) _GET_3RD_ARG(__VA_ARGS__, VERIFY_ERR_2ARGS, VERIFY_ERR_1ARGS)(__VA_ARGS__, NULL) + #define PRINT_LFS_ERR(_err) +#else + #define VERIFY_LFS(...) _GET_3RD_ARG(__VA_ARGS__, VERIFY_ERR_2ARGS, VERIFY_ERR_1ARGS)(__VA_ARGS__, dbg_strerr_lfs) + #define PRINT_LFS_ERR(_err) do { if (_err) { VERIFY_MESS((long int)_err, dbg_strerr_lfs); } } while(0) // LFS_ERR are of type int, VERIFY_MESS expects long_int + + const char* dbg_strerr_lfs (int32_t err); +#endif + +#endif /* ADAFRUIT_LITTLEFS_H_ */ diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp new file mode 100644 index 00000000..41ba3570 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.cpp @@ -0,0 +1,420 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include "Adafruit_LittleFS.h" +#include "littlefs/lfs.h" + +//--------------------------------------------------------------------+ +// MACRO TYPEDEF CONSTANT ENUM DECLARATION +//--------------------------------------------------------------------+ + +using namespace Adafruit_LittleFS_Namespace; + +File::File (Adafruit_LittleFS &fs) +{ + _fs = &fs; + _is_dir = false; + _name[0] = 0; + _name[LFS_NAME_MAX] = 0; + _dir_path = NULL; + + _dir = NULL; + _file = NULL; +} + +File::File (char const *filename, uint8_t mode, Adafruit_LittleFS &fs) + : File(fs) +{ + // public constructor calls public API open(), which will obtain the mutex + this->open(filename, mode); +} + +bool File::_open_file (char const *filepath, uint8_t mode) +{ + int flags = (mode == FILE_O_READ) ? LFS_O_RDONLY : + (mode == FILE_O_WRITE) ? (LFS_O_RDWR | LFS_O_CREAT) : 0; + + if ( flags ) + { + _file = (lfs_file_t*) malloc(sizeof(lfs_file_t)); + if (!_file) return false; + + int rc = lfs_file_open(_fs->_getFS(), _file, filepath, flags); + + if ( rc ) + { + // failed to open + PRINT_LFS_ERR(rc); + // free memory + free(_file); + _file = NULL; + return false; + } + + // move to end of file + if ( mode == FILE_O_WRITE ) lfs_file_seek(_fs->_getFS(), _file, 0, LFS_SEEK_END); + + _is_dir = false; + } + + return true; +} + +bool File::_open_dir (char const *filepath) +{ + _dir = (lfs_dir_t*) malloc(sizeof(lfs_dir_t)); + if (!_dir) return false; + + int rc = lfs_dir_open(_fs->_getFS(), _dir, filepath); + + if ( rc ) + { + // failed to open + PRINT_LFS_ERR(rc); + // free memory + free(_dir); + _dir = NULL; + return false; + } + + _is_dir = true; + + _dir_path = (char*) malloc(strlen(filepath) + 1); + strcpy(_dir_path, filepath); + + return true; +} + +bool File::open (char const *filepath, uint8_t mode) +{ + bool ret = false; + _fs->_lockFS(); + + ret = this->_open(filepath, mode); + + _fs->_unlockFS(); + return ret; +} + +bool File::_open (char const *filepath, uint8_t mode) +{ + bool ret = false; + + // close if currently opened + if ( this->isOpen() ) _close(); + + struct lfs_info info; + int rc = lfs_stat(_fs->_getFS(), filepath, &info); + + if ( LFS_ERR_OK == rc ) + { + // file existed, open file or directory accordingly + ret = (info.type == LFS_TYPE_REG) ? _open_file(filepath, mode) : _open_dir(filepath); + } + else if ( LFS_ERR_NOENT == rc ) + { + // file not existed, only proceed with FILE_O_WRITE mode + if ( mode == FILE_O_WRITE ) ret = _open_file(filepath, mode); + } + else + { + PRINT_LFS_ERR(rc); + } + + // save bare file name + if (ret) + { + char const* splash = strrchr(filepath, '/'); + strncpy(_name, splash ? (splash + 1) : filepath, LFS_NAME_MAX); + } + return ret; +} + +size_t File::write (uint8_t ch) +{ + return write(&ch, 1); +} + +size_t File::write (uint8_t const *buf, size_t size) +{ + lfs_ssize_t wrcount = 0; + _fs->_lockFS(); + + if (!this->_is_dir) + { + wrcount = lfs_file_write(_fs->_getFS(), _file, buf, size); + if (wrcount < 0) + { + wrcount = 0; + } + } + + _fs->_unlockFS(); + return wrcount; +} + +int File::read (void) +{ + // this thin wrapper relies on called function to synchronize + int ret = -1; + uint8_t ch; + if (read(&ch, 1) > 0) + { + ret = static_cast(ch); + } + return ret; +} + +int File::read (void *buf, uint16_t nbyte) +{ + int ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) + { + ret = lfs_file_read(_fs->_getFS(), _file, buf, nbyte); + } + + _fs->_unlockFS(); + return ret; +} + +int File::peek (void) +{ + int ret = -1; + _fs->_lockFS(); + + if (!this->_is_dir) + { + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + uint8_t ch = 0; + if (lfs_file_read(_fs->_getFS(), _file, &ch, 1) > 0) + { + ret = static_cast(ch); + } + (void) lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET); + } + + _fs->_unlockFS(); + return ret; +} + +int File::available (void) +{ + int ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) + { + uint32_t size = lfs_file_size(_fs->_getFS(), _file); + uint32_t pos = lfs_file_tell(_fs->_getFS(), _file); + ret = size - pos; + } + + _fs->_unlockFS(); + return ret; +} + +bool File::seek (uint32_t pos) +{ + bool ret = false; + _fs->_lockFS(); + + if (!this->_is_dir) + { + ret = lfs_file_seek(_fs->_getFS(), _file, pos, LFS_SEEK_SET) >= 0; + } + + _fs->_unlockFS(); + return ret; +} + +uint32_t File::position (void) +{ + uint32_t ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) + { + ret = lfs_file_tell(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return ret; +} + +uint32_t File::size (void) +{ + uint32_t ret = 0; + _fs->_lockFS(); + + if (!this->_is_dir) + { + ret = lfs_file_size(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return ret; +} + +bool File::truncate (uint32_t pos) +{ + int32_t ret=LFS_ERR_ISDIR; + _fs->_lockFS(); + if (!this->_is_dir) + { + ret = lfs_file_truncate(_fs->_getFS(), _file, pos); + } + _fs->_unlockFS(); + return ( ret == 0 ); +} + +bool File::truncate (void) +{ + int32_t ret=LFS_ERR_ISDIR; + uint32_t pos; + _fs->_lockFS(); + if (!this->_is_dir) + { + pos = lfs_file_tell(_fs->_getFS(), _file); + ret = lfs_file_truncate(_fs->_getFS(), _file, pos); + } + _fs->_unlockFS(); + return ( ret == 0 ); +} + +void File::flush (void) +{ + _fs->_lockFS(); + + if (!this->_is_dir) + { + lfs_file_sync(_fs->_getFS(), _file); + } + + _fs->_unlockFS(); + return; +} + +void File::close (void) +{ + _fs->_lockFS(); + this->_close(); + _fs->_unlockFS(); +} + +void File::_close(void) +{ + if ( this->isOpen() ) + { + if ( this->_is_dir ) + { + lfs_dir_close(_fs->_getFS(), _dir); + free(_dir); + _dir = NULL; + + if ( this->_dir_path ) free(_dir_path); + _dir_path = NULL; + } + else + { + lfs_file_close(this->_fs->_getFS(), _file); + free(_file); + _file = NULL; + } + } +} + +File::operator bool (void) +{ + return isOpen(); +} + +bool File::isOpen(void) +{ + return (_file != NULL) || (_dir != NULL); +} + +// WARNING -- although marked as `const`, the values pointed +// to may change. For example, if the same File +// object has `open()` called with a different +// file or directory name, this same pointer will +// suddenly (unexpectedly?) have different values. +char const* File::name (void) +{ + return this->_name; +} + +bool File::isDirectory (void) +{ + return this->_is_dir; +} + +File File::openNextFile (uint8_t mode) +{ + _fs->_lockFS(); + + File ret(*_fs); + if (this->_is_dir) + { + struct lfs_info info; + int rc; + + // lfs_dir_read returns 0 when reaching end of directory, 1 if found an entry + // Skip the "." and ".." entries ... + do + { + rc = lfs_dir_read(_fs->_getFS(), _dir, &info); + } while ( rc == 1 && (!strcmp(".", info.name) || !strcmp("..", info.name)) ); + + if ( rc == 1 ) + { + // string cat name with current folder + char filepath[strlen(_dir_path) + 1 + strlen(info.name) + 1]; // potential for significant stack usage + strcpy(filepath, _dir_path); + if ( !(_dir_path[0] == '/' && _dir_path[1] == 0) ) strcat(filepath, "/"); // only add '/' if cwd is not root + strcat(filepath, info.name); + + (void)ret._open(filepath, mode); // return value is ignored ... caller is expected to check isOpened() + } + else if ( rc < 0 ) + { + PRINT_LFS_ERR(rc); + } + } + _fs->_unlockFS(); + return ret; +} + +void File::rewindDirectory (void) +{ + _fs->_lockFS(); + if (this->_is_dir) + { + lfs_dir_rewind(_fs->_getFS(), _dir); + } + _fs->_unlockFS(); +} + diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.h b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.h new file mode 100644 index 00000000..13c7058a --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/Adafruit_LittleFS_File.h @@ -0,0 +1,108 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef ADAFRUIT_LITTLEFS_FILE_H_ +#define ADAFRUIT_LITTLEFS_FILE_H_ + +// Forward declaration +class Adafruit_LittleFS; + +namespace Adafruit_LittleFS_Namespace +{ + +// avoid conflict with other FileSystem FILE_READ/FILE_WRITE +enum +{ + FILE_O_READ = 0, + FILE_O_WRITE = 1, +}; + +class File : public Stream +{ + public: + File (Adafruit_LittleFS &fs); + File (char const *filename, uint8_t mode, Adafruit_LittleFS &fs); + + public: + + bool open (char const *filename, uint8_t mode); + + //------------- Stream API -------------// + virtual size_t write (uint8_t ch); + virtual size_t write (uint8_t const *buf, size_t size); + size_t write(const char *str) { + if (str == NULL) return 0; + return write((const uint8_t *)str, strlen(str)); + } + size_t write(const char *buffer, size_t size) { + return write((const uint8_t *)buffer, size); + } + + virtual int read (void); + int read (void *buf, uint16_t nbyte); + + virtual int peek (void); + virtual int available (void); + virtual void flush (void); + + bool seek (uint32_t pos); + uint32_t position (void); + uint32_t size (void); + + bool truncate (uint32_t pos); + bool truncate (void); + + void close (void); + + operator bool (void); + + bool isOpen(void); + char const* name (void); + + bool isDirectory (void); + File openNextFile (uint8_t mode = FILE_O_READ); + void rewindDirectory (void); + + private: + Adafruit_LittleFS* _fs; + + bool _is_dir; + + union { + lfs_file_t* _file; + lfs_dir_t* _dir; + }; + + char* _dir_path; + char _name[LFS_NAME_MAX+1]; + + bool _open(char const *filepath, uint8_t mode); + bool _open_file(char const *filepath, uint8_t mode); + bool _open_dir (char const *filepath); + void _close(void); +}; + +} + +#endif /* ADAFRUIT_LITTLEFS_FILE_H_ */ diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md new file mode 100644 index 00000000..ed69bea4 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md @@ -0,0 +1,24 @@ +Copyright (c) 2017, Arm Limited. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +- Neither the name of ARM nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md new file mode 100644 index 00000000..623ba0ae --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md @@ -0,0 +1,177 @@ +## The little filesystem + +A little fail-safe filesystem designed for embedded systems. + +``` + | | | .---._____ + .-----. | | +--|o |---| littlefs | +--| |---| | + '-----' '----------' + | | | +``` + +**Bounded RAM/ROM** - The littlefs is designed to work with a limited amount +of memory. Recursion is avoided and dynamic memory is limited to configurable +buffers that can be provided statically. + +**Power-loss resilient** - The littlefs is designed for systems that may have +random power failures. The littlefs has strong copy-on-write guarantees and +storage on disk is always kept in a valid state. + +**Wear leveling** - Since the most common form of embedded storage is erodible +flash memories, littlefs provides a form of dynamic wear leveling for systems +that can not fit a full flash translation layer. + +## Example + +Here's a simple example that updates a file named `boot_count` every time +main runs. The program can be interrupted at any time without losing track +of how many times it has been booted and without corrupting the filesystem: + +``` c +#include "lfs.h" + +// variables used by the filesystem +lfs_t lfs; +lfs_file_t file; + +// configuration of the filesystem is provided by this struct +const struct lfs_config cfg = { + // block device operations + .read = user_provided_block_device_read, + .prog = user_provided_block_device_prog, + .erase = user_provided_block_device_erase, + .sync = user_provided_block_device_sync, + + // block device configuration + .read_size = 16, + .prog_size = 16, + .block_size = 4096, + .block_count = 128, + .lookahead = 128, +}; + +// entry point +int main(void) { + // mount the filesystem + int err = lfs_mount(&lfs, &cfg); + + // reformat if we can't mount the filesystem + // this should only happen on the first boot + if (err) { + lfs_format(&lfs, &cfg); + lfs_mount(&lfs, &cfg); + } + + // read current count + uint32_t boot_count = 0; + lfs_file_open(&lfs, &file, "boot_count", LFS_O_RDWR | LFS_O_CREAT); + lfs_file_read(&lfs, &file, &boot_count, sizeof(boot_count)); + + // update boot count + boot_count += 1; + lfs_file_rewind(&lfs, &file); + lfs_file_write(&lfs, &file, &boot_count, sizeof(boot_count)); + + // remember the storage is not updated until the file is closed successfully + lfs_file_close(&lfs, &file); + + // release any resources we were using + lfs_unmount(&lfs); + + // print the boot count + printf("boot_count: %d\n", boot_count); +} +``` + +## Usage + +Detailed documentation (or at least as much detail as is currently available) +can be found in the comments in [lfs.h](lfs.h). + +As you may have noticed, littlefs takes in a configuration structure that +defines how the filesystem operates. The configuration struct provides the +filesystem with the block device operations and dimensions, tweakable +parameters that tradeoff memory usage for performance, and optional +static buffers if the user wants to avoid dynamic memory. + +The state of the littlefs is stored in the `lfs_t` type which is left up +to the user to allocate, allowing multiple filesystems to be in use +simultaneously. With the `lfs_t` and configuration struct, a user can +format a block device or mount the filesystem. + +Once mounted, the littlefs provides a full set of POSIX-like file and +directory functions, with the deviation that the allocation of filesystem +structures must be provided by the user. + +All POSIX operations, such as remove and rename, are atomic, even in event +of power-loss. Additionally, no file updates are actually committed to the +filesystem until sync or close is called on the file. + +## Other notes + +All littlefs have the potential to return a negative error code. The errors +can be either one of those found in the `enum lfs_error` in [lfs.h](lfs.h), +or an error returned by the user's block device operations. + +In the configuration struct, the `prog` and `erase` function provided by the +user may return a `LFS_ERR_CORRUPT` error if the implementation already can +detect corrupt blocks. However, the wear leveling does not depend on the return +code of these functions, instead all data is read back and checked for +integrity. + +If your storage caches writes, make sure that the provided `sync` function +flushes all the data to memory and ensures that the next read fetches the data +from memory, otherwise data integrity can not be guaranteed. If the `write` +function does not perform caching, and therefore each `read` or `write` call +hits the memory, the `sync` function can simply return 0. + +## Reference material + +[DESIGN.md](DESIGN.md) - DESIGN.md contains a fully detailed dive into how +littlefs actually works. I would encourage you to read it since the +solutions and tradeoffs at work here are quite interesting. + +[SPEC.md](SPEC.md) - SPEC.md contains the on-disk specification of littlefs +with all the nitty-gritty details. Can be useful for developing tooling. + +## Testing + +The littlefs comes with a test suite designed to run on a PC using the +[emulated block device](emubd/lfs_emubd.h) found in the emubd directory. +The tests assume a Linux environment and can be started with make: + +``` bash +make test +``` + +## License + +The littlefs is provided under the [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) +license. See [LICENSE.md](LICENSE.md) for more information. Contributions to +this project are accepted under the same license. + +Individual files contain the following tag instead of the full license text. + + SPDX-License-Identifier: BSD-3-Clause + +This enables machine processing of license information based on the SPDX +License Identifiers that are here available: http://spdx.org/licenses/ + +## Related projects + +[Mbed OS](https://github.com/ARMmbed/mbed-os/tree/master/features/filesystem/littlefs) - +The easiest way to get started with littlefs is to jump into [Mbed](https://os.mbed.com/), +which already has block device drivers for most forms of embedded storage. The +littlefs is available in Mbed OS as the [LittleFileSystem](https://os.mbed.com/docs/latest/reference/littlefilesystem.html) +class. + +[littlefs-fuse](https://github.com/geky/littlefs-fuse) - A [FUSE](https://github.com/libfuse/libfuse) +wrapper for littlefs. The project allows you to mount littlefs directly on a +Linux machine. Can be useful for debugging littlefs if you have an SD card +handy. + +[littlefs-js](https://github.com/geky/littlefs-js) - A javascript wrapper for +littlefs. I'm not sure why you would want this, but it is handy for demos. +You can see it in action [here](http://littlefs.geky.net/demo.html). diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs.c b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs.c new file mode 100644 index 00000000..f9d9ce34 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs.c @@ -0,0 +1,2585 @@ +/* + * The little filesystem + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs.h" +#include "lfs_util.h" + +#include + + +/// Caching block device operations /// +static int lfs_cache_read(lfs_t *lfs, lfs_cache_t *rcache, + const lfs_cache_t *pcache, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size) { + uint8_t *data = buffer; + LFS_ASSERT(block < lfs->cfg->block_count); + + while (size > 0) { + if (pcache && block == pcache->block && off >= pcache->off && + off < pcache->off + lfs->cfg->prog_size) { + // is already in pcache? + lfs_size_t diff = lfs_min(size, + lfs->cfg->prog_size - (off-pcache->off)); + memcpy(data, &pcache->buffer[off-pcache->off], diff); + + data += diff; + off += diff; + size -= diff; + continue; + } + + if (block == rcache->block && off >= rcache->off && + off < rcache->off + lfs->cfg->read_size) { + // is already in rcache? + lfs_size_t diff = lfs_min(size, + lfs->cfg->read_size - (off-rcache->off)); + memcpy(data, &rcache->buffer[off-rcache->off], diff); + + data += diff; + off += diff; + size -= diff; + continue; + } + + if (off % lfs->cfg->read_size == 0 && size >= lfs->cfg->read_size) { + // bypass cache? + lfs_size_t diff = size - (size % lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); + if (err) { + return err; + } + + data += diff; + off += diff; + size -= diff; + continue; + } + + // load to cache, first condition can no longer fail + rcache->block = block; + rcache->off = off - (off % lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, rcache->block, + rcache->off, rcache->buffer, lfs->cfg->read_size); + if (err) { + return err; + } + } + + return 0; +} + +static int lfs_cache_cmp(lfs_t *lfs, lfs_cache_t *rcache, + const lfs_cache_t *pcache, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs_cache_read(lfs, rcache, pcache, + block, off+i, &c, 1); + if (err) { + return err; + } + + if (c != data[i]) { + return false; + } + } + + return true; +} + +static int lfs_cache_crc(lfs_t *lfs, lfs_cache_t *rcache, + const lfs_cache_t *pcache, lfs_block_t block, + lfs_off_t off, lfs_size_t size, uint32_t *crc) { + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs_cache_read(lfs, rcache, pcache, + block, off+i, &c, 1); + if (err) { + return err; + } + + lfs_crc(crc, &c, 1); + } + + return 0; +} + +static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { + // do not zero, cheaper if cache is readonly or only going to be + // written with identical data (during relocates) + (void)lfs; + rcache->block = 0xffffffff; +} + +static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) { + // zero to avoid information leak + memset(pcache->buffer, 0xff, lfs->cfg->prog_size); + pcache->block = 0xffffffff; +} + +static int lfs_cache_flush(lfs_t *lfs, + lfs_cache_t *pcache, lfs_cache_t *rcache) { + if (pcache->block != 0xffffffff) { + int err = lfs->cfg->prog(lfs->cfg, pcache->block, + pcache->off, pcache->buffer, lfs->cfg->prog_size); + if (err) { + return err; + } + + if (rcache) { + int res = lfs_cache_cmp(lfs, rcache, NULL, pcache->block, + pcache->off, pcache->buffer, lfs->cfg->prog_size); + if (res < 0) { + return res; + } + + if (!res) { + return LFS_ERR_CORRUPT; + } + } + + lfs_cache_zero(lfs, pcache); + } + + return 0; +} + +static int lfs_cache_prog(lfs_t *lfs, lfs_cache_t *pcache, + lfs_cache_t *rcache, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + LFS_ASSERT(block < lfs->cfg->block_count); + + while (size > 0) { + if (block == pcache->block && off >= pcache->off && + off < pcache->off + lfs->cfg->prog_size) { + // is already in pcache? + lfs_size_t diff = lfs_min(size, + lfs->cfg->prog_size - (off-pcache->off)); + memcpy(&pcache->buffer[off-pcache->off], data, diff); + + data += diff; + off += diff; + size -= diff; + + if (off % lfs->cfg->prog_size == 0) { + // eagerly flush out pcache if we fill up + int err = lfs_cache_flush(lfs, pcache, rcache); + if (err) { + return err; + } + } + + continue; + } + + // pcache must have been flushed, either by programming and + // entire block or manually flushing the pcache + LFS_ASSERT(pcache->block == 0xffffffff); + + if (off % lfs->cfg->prog_size == 0 && + size >= lfs->cfg->prog_size) { + // bypass pcache? + lfs_size_t diff = size - (size % lfs->cfg->prog_size); + int err = lfs->cfg->prog(lfs->cfg, block, off, data, diff); + if (err) { + return err; + } + + if (rcache) { + int res = lfs_cache_cmp(lfs, rcache, NULL, + block, off, data, diff); + if (res < 0) { + return res; + } + + if (!res) { + return LFS_ERR_CORRUPT; + } + } + + data += diff; + off += diff; + size -= diff; + continue; + } + + // prepare pcache, first condition can no longer fail + pcache->block = block; + pcache->off = off - (off % lfs->cfg->prog_size); + } + + return 0; +} + + +/// General lfs block device operations /// +static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size) { + // if we ever do more than writes to alternating pairs, + // this may need to consider pcache + return lfs_cache_read(lfs, &lfs->rcache, NULL, + block, off, buffer, size); +} + +static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size) { + return lfs_cache_prog(lfs, &lfs->pcache, NULL, + block, off, buffer, size); +} + +static int lfs_bd_cmp(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size) { + return lfs_cache_cmp(lfs, &lfs->rcache, NULL, block, off, buffer, size); +} + +static int lfs_bd_crc(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, lfs_size_t size, uint32_t *crc) { + return lfs_cache_crc(lfs, &lfs->rcache, NULL, block, off, size, crc); +} + +static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { + return lfs->cfg->erase(lfs->cfg, block); +} + +static int lfs_bd_sync(lfs_t *lfs) { + lfs_cache_drop(lfs, &lfs->rcache); + + int err = lfs_cache_flush(lfs, &lfs->pcache, NULL); + if (err) { + return err; + } + + return lfs->cfg->sync(lfs->cfg); +} + + +/// Internal operations predeclared here /// +int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data); +static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir); +static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], + lfs_dir_t *parent, lfs_entry_t *entry); +static int lfs_moved(lfs_t *lfs, const void *e); +static int lfs_relocate(lfs_t *lfs, + const lfs_block_t oldpair[2], const lfs_block_t newpair[2]); +int lfs_deorphan(lfs_t *lfs); + + +/// Block allocator /// +static int lfs_alloc_lookahead(void *p, lfs_block_t block) { + lfs_t *lfs = p; + + lfs_block_t off = ((block - lfs->free.off) + + lfs->cfg->block_count) % lfs->cfg->block_count; + + if (off < lfs->free.size) { + lfs->free.buffer[off / 32] |= 1U << (off % 32); + } + + return 0; +} + +static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { + while (true) { + while (lfs->free.i != lfs->free.size) { + lfs_block_t off = lfs->free.i; + lfs->free.i += 1; + lfs->free.ack -= 1; + + if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { + // found a free block + *block = (lfs->free.off + off) % lfs->cfg->block_count; + + // eagerly find next off so an alloc ack can + // discredit old lookahead blocks + while (lfs->free.i != lfs->free.size && + (lfs->free.buffer[lfs->free.i / 32] + & (1U << (lfs->free.i % 32)))) { + lfs->free.i += 1; + lfs->free.ack -= 1; + } + + return 0; + } + } + + // check if we have looked at all blocks since last ack + if (lfs->free.ack == 0) { + LFS_WARN("No more free space %" PRIu32, + lfs->free.i + lfs->free.off); + return LFS_ERR_NOSPC; + } + + lfs->free.off = (lfs->free.off + lfs->free.size) + % lfs->cfg->block_count; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->free.ack); + lfs->free.i = 0; + + // find mask of free blocks from tree + memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8); + int err = lfs_traverse(lfs, lfs_alloc_lookahead, lfs); + if (err) { + return err; + } + } +} + +static void lfs_alloc_ack(lfs_t *lfs) { + lfs->free.ack = lfs->cfg->block_count; +} + + +/// Endian swapping functions /// +static void lfs_dir_fromle32(struct lfs_disk_dir *d) { + d->rev = lfs_fromle32(d->rev); + d->size = lfs_fromle32(d->size); + d->tail[0] = lfs_fromle32(d->tail[0]); + d->tail[1] = lfs_fromle32(d->tail[1]); +} + +static void lfs_dir_tole32(struct lfs_disk_dir *d) { + d->rev = lfs_tole32(d->rev); + d->size = lfs_tole32(d->size); + d->tail[0] = lfs_tole32(d->tail[0]); + d->tail[1] = lfs_tole32(d->tail[1]); +} + +static void lfs_entry_fromle32(struct lfs_disk_entry *d) { + d->u.dir[0] = lfs_fromle32(d->u.dir[0]); + d->u.dir[1] = lfs_fromle32(d->u.dir[1]); +} + +static void lfs_entry_tole32(struct lfs_disk_entry *d) { + d->u.dir[0] = lfs_tole32(d->u.dir[0]); + d->u.dir[1] = lfs_tole32(d->u.dir[1]); +} + +static void lfs_superblock_fromle32(struct lfs_disk_superblock *d) { + d->root[0] = lfs_fromle32(d->root[0]); + d->root[1] = lfs_fromle32(d->root[1]); + d->block_size = lfs_fromle32(d->block_size); + d->block_count = lfs_fromle32(d->block_count); + d->version = lfs_fromle32(d->version); +} + +static void lfs_superblock_tole32(struct lfs_disk_superblock *d) { + d->root[0] = lfs_tole32(d->root[0]); + d->root[1] = lfs_tole32(d->root[1]); + d->block_size = lfs_tole32(d->block_size); + d->block_count = lfs_tole32(d->block_count); + d->version = lfs_tole32(d->version); +} + + +/// Metadata pair and directory operations /// +static inline void lfs_pairswap(lfs_block_t pair[2]) { + lfs_block_t t = pair[0]; + pair[0] = pair[1]; + pair[1] = t; +} + +static inline bool lfs_pairisnull(const lfs_block_t pair[2]) { + return pair[0] == 0xffffffff || pair[1] == 0xffffffff; +} + +static inline int lfs_paircmp( + const lfs_block_t paira[2], + const lfs_block_t pairb[2]) { + return !(paira[0] == pairb[0] || paira[1] == pairb[1] || + paira[0] == pairb[1] || paira[1] == pairb[0]); +} + +static inline bool lfs_pairsync( + const lfs_block_t paira[2], + const lfs_block_t pairb[2]) { + return (paira[0] == pairb[0] && paira[1] == pairb[1]) || + (paira[0] == pairb[1] && paira[1] == pairb[0]); +} + +static inline lfs_size_t lfs_entry_size(const lfs_entry_t *entry) { + return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; +} + +static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) { + // allocate pair of dir blocks + for (int i = 0; i < 2; i++) { + int err = lfs_alloc(lfs, &dir->pair[i]); + if (err) { + return err; + } + } + + // rather than clobbering one of the blocks we just pretend + // the revision may be valid + int err = lfs_bd_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + if (err != LFS_ERR_CORRUPT) { + dir->d.rev = lfs_fromle32(dir->d.rev); + } + + // set defaults + dir->d.rev += 1; + dir->d.size = sizeof(dir->d)+4; + dir->d.tail[0] = 0xffffffff; + dir->d.tail[1] = 0xffffffff; + dir->off = sizeof(dir->d); + + // don't write out yet, let caller take care of that + return 0; +} + +static int lfs_dir_fetch(lfs_t *lfs, + lfs_dir_t *dir, const lfs_block_t pair[2]) { + // copy out pair, otherwise may be aliasing dir + const lfs_block_t tpair[2] = {pair[0], pair[1]}; + bool valid = false; + + // check both blocks for the most recent revision + for (int i = 0; i < 2; i++) { + struct lfs_disk_dir test; + int err = lfs_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); + lfs_dir_fromle32(&test); + if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } + return err; + } + + if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { + continue; + } + + if ((0x7fffffff & test.size) < sizeof(test)+4 || + (0x7fffffff & test.size) > lfs->cfg->block_size) { + continue; + } + + uint32_t crc = 0xffffffff; + lfs_dir_tole32(&test); + lfs_crc(&crc, &test, sizeof(test)); + lfs_dir_fromle32(&test); + err = lfs_bd_crc(lfs, tpair[i], sizeof(test), + (0x7fffffff & test.size) - sizeof(test), &crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } + return err; + } + + if (crc != 0) { + continue; + } + + valid = true; + + // setup dir in case it's valid + dir->pair[0] = tpair[(i+0) % 2]; + dir->pair[1] = tpair[(i+1) % 2]; + dir->off = sizeof(dir->d); + dir->d = test; + } + + if (!valid) { + LFS_ERROR("Corrupted dir pair at %" PRIu32 " %" PRIu32 , + tpair[0], tpair[1]); + return LFS_ERR_CORRUPT; + } + + return 0; +} + +struct lfs_region { + lfs_off_t oldoff; + lfs_size_t oldlen; + const void *newdata; + lfs_size_t newlen; +}; + +static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, + const struct lfs_region *regions, int count) { + // increment revision count + dir->d.rev += 1; + + // keep pairs in order such that pair[0] is most recent + lfs_pairswap(dir->pair); + for (int i = 0; i < count; i++) { + dir->d.size += regions[i].newlen - regions[i].oldlen; + } + + const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]}; + bool relocated = false; + + while (true) { + if (true) { + int err = lfs_bd_erase(lfs, dir->pair[0]); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + uint32_t crc = 0xffffffff; + lfs_dir_tole32(&dir->d); + lfs_crc(&crc, &dir->d, sizeof(dir->d)); + err = lfs_bd_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); + lfs_dir_fromle32(&dir->d); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + int i = 0; + lfs_off_t oldoff = sizeof(dir->d); + lfs_off_t newoff = sizeof(dir->d); + while (newoff < (0x7fffffff & dir->d.size)-4) { + if (i < count && regions[i].oldoff == oldoff) { + lfs_crc(&crc, regions[i].newdata, regions[i].newlen); + err = lfs_bd_prog(lfs, dir->pair[0], + newoff, regions[i].newdata, regions[i].newlen); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + oldoff += regions[i].oldlen; + newoff += regions[i].newlen; + i += 1; + } else { + uint8_t data; + err = lfs_bd_read(lfs, oldpair[1], oldoff, &data, 1); + if (err) { + return err; + } + + lfs_crc(&crc, &data, 1); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + oldoff += 1; + newoff += 1; + } + } + + crc = lfs_tole32(crc); + err = lfs_bd_prog(lfs, dir->pair[0], newoff, &crc, 4); + crc = lfs_fromle32(crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + err = lfs_bd_sync(lfs); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // successful commit, check checksum to make sure + uint32_t ncrc = 0xffffffff; + err = lfs_bd_crc(lfs, dir->pair[0], 0, + (0x7fffffff & dir->d.size)-4, &ncrc); + if (err) { + return err; + } + + if (ncrc != crc) { + goto relocate; + } + } + + break; +relocate: + //commit was corrupted + LFS_DEBUG("Bad block at %" PRIu32, dir->pair[0]); + + // drop caches and prepare to relocate block + relocated = true; + lfs_cache_drop(lfs, &lfs->pcache); + + // can't relocate superblock, filesystem is now frozen + if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) { + LFS_WARN("Superblock %" PRIu32 " has become unwritable", + oldpair[0]); + return LFS_ERR_CORRUPT; + } + + // relocate half of pair + int err = lfs_alloc(lfs, &dir->pair[0]); + if (err) { + return err; + } + } + + if (relocated) { + // update references if we relocated + LFS_DEBUG("Relocating %" PRIu32 " %" PRIu32 " to %" PRIu32 " %" PRIu32, + oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]); + int err = lfs_relocate(lfs, oldpair, dir->pair); + if (err) { + return err; + } + } + + // shift over any directories that are affected + for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { + if (lfs_paircmp(d->pair, dir->pair) == 0) { + d->pair[0] = dir->pair[0]; + d->pair[1] = dir->pair[1]; + } + } + + return 0; +} + +static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir, + lfs_entry_t *entry, const void *data) { + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ + {entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)}, + {entry->off+sizeof(entry->d), entry->d.nlen, data, entry->d.nlen} + }, data ? 2 : 1); + lfs_entry_fromle32(&entry->d); + return err; +} + +static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, + lfs_entry_t *entry, const void *data) { + // check if we fit, if top bit is set we do not and move on + while (true) { + if (dir->d.size + lfs_entry_size(entry) <= lfs->cfg->block_size) { + entry->off = dir->d.size - 4; + + lfs_entry_tole32(&entry->d); + int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ + {entry->off, 0, &entry->d, sizeof(entry->d)}, + {entry->off, 0, data, entry->d.nlen} + }, 2); + lfs_entry_fromle32(&entry->d); + return err; + } + + // we need to allocate a new dir block + if (!(0x80000000 & dir->d.size)) { + lfs_dir_t olddir = *dir; + int err = lfs_dir_alloc(lfs, dir); + if (err) { + return err; + } + + dir->d.tail[0] = olddir.d.tail[0]; + dir->d.tail[1] = olddir.d.tail[1]; + entry->off = dir->d.size - 4; + lfs_entry_tole32(&entry->d); + err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ + {entry->off, 0, &entry->d, sizeof(entry->d)}, + {entry->off, 0, data, entry->d.nlen} + }, 2); + lfs_entry_fromle32(&entry->d); + if (err) { + return err; + } + + olddir.d.size |= 0x80000000; + olddir.d.tail[0] = dir->pair[0]; + olddir.d.tail[1] = dir->pair[1]; + return lfs_dir_commit(lfs, &olddir, NULL, 0); + } + + int err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + } +} + +static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { + // check if we should just drop the directory block + if ((dir->d.size & 0x7fffffff) == sizeof(dir->d)+4 + + lfs_entry_size(entry)) { + lfs_dir_t pdir; + int res = lfs_pred(lfs, dir->pair, &pdir); + if (res < 0) { + return res; + } + + if (pdir.d.size & 0x80000000) { + pdir.d.size &= dir->d.size | 0x7fffffff; + pdir.d.tail[0] = dir->d.tail[0]; + pdir.d.tail[1] = dir->d.tail[1]; + return lfs_dir_commit(lfs, &pdir, NULL, 0); + } + } + + // shift out the entry + int err = lfs_dir_commit(lfs, dir, (struct lfs_region[]){ + {entry->off, lfs_entry_size(entry), NULL, 0}, + }, 1); + if (err) { + return err; + } + + // shift over any files/directories that are affected + for (lfs_file_t *f = lfs->files; f; f = f->next) { + if (lfs_paircmp(f->pair, dir->pair) == 0) { + if (f->poff == entry->off) { + f->pair[0] = 0xffffffff; + f->pair[1] = 0xffffffff; + } else if (f->poff > entry->off) { + f->poff -= lfs_entry_size(entry); + } + } + } + + for (lfs_dir_t *d = lfs->dirs; d; d = d->next) { + if (lfs_paircmp(d->pair, dir->pair) == 0) { + if (d->off > entry->off) { + d->off -= lfs_entry_size(entry); + d->pos -= lfs_entry_size(entry); + } + } + } + + return 0; +} + +static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { + while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) { + if (!(0x80000000 & dir->d.size)) { + entry->off = dir->off; + return LFS_ERR_NOENT; + } + + int err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + + dir->off = sizeof(dir->d); + dir->pos += sizeof(dir->d) + 4; + } + + int err = lfs_bd_read(lfs, dir->pair[0], dir->off, + &entry->d, sizeof(entry->d)); + lfs_entry_fromle32(&entry->d); + if (err) { + return err; + } + + entry->off = dir->off; + dir->off += lfs_entry_size(entry); + dir->pos += lfs_entry_size(entry); + return 0; +} + +static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, + lfs_entry_t *entry, const char **path) { + const char *pathname = *path; + size_t pathlen; + entry->d.type = LFS_TYPE_DIR; + entry->d.elen = sizeof(entry->d) - 4; + entry->d.alen = 0; + entry->d.nlen = 0; + entry->d.u.dir[0] = lfs->root[0]; + entry->d.u.dir[1] = lfs->root[1]; + + while (true) { +nextname: + // skip slashes + pathname += strspn(pathname, "/"); + pathlen = strcspn(pathname, "/"); + + // skip '.' and root '..' + if ((pathlen == 1 && memcmp(pathname, ".", 1) == 0) || + (pathlen == 2 && memcmp(pathname, "..", 2) == 0)) { + pathname += pathlen; + goto nextname; + } + + // skip if matched by '..' in name + const char *suffix = pathname + pathlen; + size_t sufflen; + int depth = 1; + while (true) { + suffix += strspn(suffix, "/"); + sufflen = strcspn(suffix, "/"); + if (sufflen == 0) { + break; + } + + if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { + depth -= 1; + if (depth == 0) { + pathname = suffix + sufflen; + goto nextname; + } + } else { + depth += 1; + } + + suffix += sufflen; + } + + // found path + if (pathname[0] == '\0') { + return 0; + } + + // update what we've found + *path = pathname; + + // continue on if we hit a directory + if (entry->d.type != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + int err = lfs_dir_fetch(lfs, dir, entry->d.u.dir); + if (err) { + return err; + } + + // find entry matching name + while (true) { + err = lfs_dir_next(lfs, dir, entry); + if (err) { + return err; + } + + if (((0x7f & entry->d.type) != LFS_TYPE_REG && + (0x7f & entry->d.type) != LFS_TYPE_DIR) || + entry->d.nlen != pathlen) { + continue; + } + + int res = lfs_bd_cmp(lfs, dir->pair[0], + entry->off + 4+entry->d.elen+entry->d.alen, + pathname, pathlen); + if (res < 0) { + return res; + } + + // found match + if (res) { + break; + } + } + + // check that entry has not been moved + if (!lfs->moving && entry->d.type & 0x80) { + int moved = lfs_moved(lfs, &entry->d.u); + if (moved < 0 || moved) { + return (moved < 0) ? moved : LFS_ERR_NOENT; + } + + entry->d.type &= ~0x80; + } + + // to next name + pathname += pathlen; + } +} + + +/// Top level directory operations /// +int lfs_mkdir(lfs_t *lfs, const char *path) { + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // fetch parent directory + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err != LFS_ERR_NOENT || strchr(path, '/') != NULL) { + return err ? err : LFS_ERR_EXIST; + } + + // build up new directory + lfs_alloc_ack(lfs); + + lfs_dir_t dir; + err = lfs_dir_alloc(lfs, &dir); + if (err) { + return err; + } + dir.d.tail[0] = cwd.d.tail[0]; + dir.d.tail[1] = cwd.d.tail[1]; + + err = lfs_dir_commit(lfs, &dir, NULL, 0); + if (err) { + return err; + } + + entry.d.type = LFS_TYPE_DIR; + entry.d.elen = sizeof(entry.d) - 4; + entry.d.alen = 0; + entry.d.nlen = strlen(path); + entry.d.u.dir[0] = dir.pair[0]; + entry.d.u.dir[1] = dir.pair[1]; + + cwd.d.tail[0] = dir.pair[0]; + cwd.d.tail[1] = dir.pair[1]; + + err = lfs_dir_append(lfs, &cwd, &entry, path); + if (err) { + return err; + } + + lfs_alloc_ack(lfs); + return 0; +} + +int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { + dir->pair[0] = lfs->root[0]; + dir->pair[1] = lfs->root[1]; + + lfs_entry_t entry; + int err = lfs_dir_find(lfs, dir, &entry, &path); + if (err) { + return err; + } else if (entry.d.type != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + err = lfs_dir_fetch(lfs, dir, entry.d.u.dir); + if (err) { + return err; + } + + // setup head dir + // special offset for '.' and '..' + dir->head[0] = dir->pair[0]; + dir->head[1] = dir->pair[1]; + dir->pos = sizeof(dir->d) - 2; + dir->off = sizeof(dir->d); + + // add to list of directories + dir->next = lfs->dirs; + lfs->dirs = dir; + + return 0; +} + +int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { + // remove from list of directories + for (lfs_dir_t **p = &lfs->dirs; *p; p = &(*p)->next) { + if (*p == dir) { + *p = dir->next; + break; + } + } + + return 0; +} + +int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { + memset(info, 0, sizeof(*info)); + + // special offset for '.' and '..' + if (dir->pos == sizeof(dir->d) - 2) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, "."); + dir->pos += 1; + return 1; + } else if (dir->pos == sizeof(dir->d) - 1) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, ".."); + dir->pos += 1; + return 1; + } + + lfs_entry_t entry; + while (true) { + int err = lfs_dir_next(lfs, dir, &entry); + if (err) { + return (err == LFS_ERR_NOENT) ? 0 : err; + } + + if ((0x7f & entry.d.type) != LFS_TYPE_REG && + (0x7f & entry.d.type) != LFS_TYPE_DIR) { + continue; + } + + // check that entry has not been moved + if (entry.d.type & 0x80) { + int moved = lfs_moved(lfs, &entry.d.u); + if (moved < 0) { + return moved; + } + + if (moved) { + continue; + } + + entry.d.type &= ~0x80; + } + + break; + } + + info->type = entry.d.type; + if (info->type == LFS_TYPE_REG) { + info->size = entry.d.u.file.size; + } + + int err = lfs_bd_read(lfs, dir->pair[0], + entry.off + 4+entry.d.elen+entry.d.alen, + info->name, entry.d.nlen); + if (err) { + return err; + } + + return 1; +} + +int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { + // simply walk from head dir + int err = lfs_dir_rewind(lfs, dir); + if (err) { + return err; + } + dir->pos = off; + + while (off > (0x7fffffff & dir->d.size)) { + off -= 0x7fffffff & dir->d.size; + if (!(0x80000000 & dir->d.size)) { + return LFS_ERR_INVAL; + } + + err = lfs_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + } + + dir->off = off; + return 0; +} + +lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir) { + (void)lfs; + return dir->pos; +} + +int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) { + // reload the head dir + int err = lfs_dir_fetch(lfs, dir, dir->head); + if (err) { + return err; + } + + dir->pair[0] = dir->head[0]; + dir->pair[1] = dir->head[1]; + dir->pos = sizeof(dir->d) - 2; + dir->off = sizeof(dir->d); + return 0; +} + + +/// File index list operations /// +static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { + lfs_off_t size = *off; + lfs_off_t b = lfs->cfg->block_size - 2*4; + lfs_off_t i = size / b; + if (i == 0) { + return 0; + } + + i = (size - 4*(lfs_popc(i-1)+2)) / b; + *off = size - b*i - 4*lfs_popc(i); + return i; +} + +static int lfs_ctz_find(lfs_t *lfs, + lfs_cache_t *rcache, const lfs_cache_t *pcache, + lfs_block_t head, lfs_size_t size, + lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) { + if (size == 0) { + *block = 0xffffffff; + *off = 0; + return 0; + } + + lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1}); + lfs_off_t target = lfs_ctz_index(lfs, &pos); + + while (current > target) { + lfs_size_t skip = lfs_min( + lfs_npw2(current-target+1) - 1, + lfs_ctz(current)); + + int err = lfs_cache_read(lfs, rcache, pcache, head, 4*skip, &head, 4); + head = lfs_fromle32(head); + if (err) { + return err; + } + + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); + current -= 1 << skip; + } + + *block = head; + *off = pos; + return 0; +} + +static int lfs_ctz_extend(lfs_t *lfs, + lfs_cache_t *rcache, lfs_cache_t *pcache, + lfs_block_t head, lfs_size_t size, + lfs_block_t *block, lfs_off_t *off) { + while (true) { + // go ahead and grab a block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); + + if (true) { + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (size == 0) { + *block = nblock; + *off = 0; + return 0; + } + + size -= 1; + lfs_off_t index = lfs_ctz_index(lfs, &size); + size += 1; + + // just copy out the last block if it is incomplete + if (size != lfs->cfg->block_size) { + for (lfs_off_t i = 0; i < size; i++) { + uint8_t data; + err = lfs_cache_read(lfs, rcache, NULL, + head, i, &data, 1); + if (err) { + return err; + } + + err = lfs_cache_prog(lfs, pcache, rcache, + nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + *block = nblock; + *off = size; + return 0; + } + + // append block + index += 1; + lfs_size_t skips = lfs_ctz(index) + 1; + + for (lfs_off_t i = 0; i < skips; i++) { + head = lfs_tole32(head); + err = lfs_cache_prog(lfs, pcache, rcache, + nblock, 4*i, &head, 4); + head = lfs_fromle32(head); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (i != skips-1) { + err = lfs_cache_read(lfs, rcache, NULL, + head, 4*i, &head, 4); + head = lfs_fromle32(head); + if (err) { + return err; + } + } + + LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); + } + + *block = nblock; + *off = 4*skips; + return 0; + } + +relocate: + LFS_DEBUG("Bad block at %" PRIu32, nblock); + + // just clear cache and try a new block + lfs_cache_drop(lfs, &lfs->pcache); + } +} + +static int lfs_ctz_traverse(lfs_t *lfs, + lfs_cache_t *rcache, const lfs_cache_t *pcache, + lfs_block_t head, lfs_size_t size, + int (*cb)(void*, lfs_block_t), void *data) { + if (size == 0) { + return 0; + } + + lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size-1}); + + while (true) { + int err = cb(data, head); + if (err) { + return err; + } + + if (index == 0) { + return 0; + } + + lfs_block_t heads[2]; + int count = 2 - (index & 1); + err = lfs_cache_read(lfs, rcache, pcache, head, 0, &heads, count*4); + heads[0] = lfs_fromle32(heads[0]); + heads[1] = lfs_fromle32(heads[1]); + if (err) { + return err; + } + + for (int i = 0; i < count-1; i++) { + err = cb(data, heads[i]); + if (err) { + return err; + } + } + + head = heads[count-1]; + index -= count; + } +} + + +/// Top level file operations /// +int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags, + const struct lfs_file_config *cfg) { + // deorphan if we haven't yet, needed at most once after poweron + if ((flags & 3) != LFS_O_RDONLY && !lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // allocate entry for file if it doesn't exist + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err && (err != LFS_ERR_NOENT || strchr(path, '/') != NULL)) { + return err; + } + + if (err == LFS_ERR_NOENT) { + if (!(flags & LFS_O_CREAT)) { + return LFS_ERR_NOENT; + } + + // create entry to remember name + entry.d.type = LFS_TYPE_REG; + entry.d.elen = sizeof(entry.d) - 4; + entry.d.alen = 0; + entry.d.nlen = strlen(path); + entry.d.u.file.head = 0xffffffff; + entry.d.u.file.size = 0; + err = lfs_dir_append(lfs, &cwd, &entry, path); + if (err) { + return err; + } + } else if (entry.d.type == LFS_TYPE_DIR) { + return LFS_ERR_ISDIR; + } else if (flags & LFS_O_EXCL) { + return LFS_ERR_EXIST; + } + + // setup file struct + file->cfg = cfg; + file->pair[0] = cwd.pair[0]; + file->pair[1] = cwd.pair[1]; + file->poff = entry.off; + file->head = entry.d.u.file.head; + file->size = entry.d.u.file.size; + file->flags = flags; + file->pos = 0; + + if (flags & LFS_O_TRUNC) { + if (file->size != 0) { + file->flags |= LFS_F_DIRTY; + } + file->head = 0xffffffff; + file->size = 0; + } + + // allocate buffer if needed + file->cache.block = 0xffffffff; + if (file->cfg && file->cfg->buffer) { + file->cache.buffer = file->cfg->buffer; + } else if (lfs->cfg->file_buffer) { + if (lfs->files) { + // already in use + return LFS_ERR_NOMEM; + } + file->cache.buffer = lfs->cfg->file_buffer; + } else if ((file->flags & 3) == LFS_O_RDONLY) { + file->cache.buffer = lfs_malloc(lfs->cfg->read_size); + if (!file->cache.buffer) { + return LFS_ERR_NOMEM; + } + } else { + file->cache.buffer = lfs_malloc(lfs->cfg->prog_size); + if (!file->cache.buffer) { + return LFS_ERR_NOMEM; + } + } + + // zero to avoid information leak + lfs_cache_drop(lfs, &file->cache); + if ((file->flags & 3) != LFS_O_RDONLY) { + lfs_cache_zero(lfs, &file->cache); + } + + // add to list of files + file->next = lfs->files; + lfs->files = file; + + return 0; +} + +int lfs_file_open(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags) { + return lfs_file_opencfg(lfs, file, path, flags, NULL); +} + +int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { + int err = lfs_file_sync(lfs, file); + + // remove from list of files + for (lfs_file_t **p = &lfs->files; *p; p = &(*p)->next) { + if (*p == file) { + *p = file->next; + break; + } + } + + // clean up memory + if (!(file->cfg && file->cfg->buffer) && !lfs->cfg->file_buffer) { + lfs_free(file->cache.buffer); + } + + return err; +} + +static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) { +relocate: + LFS_DEBUG("Bad block at %" PRIu32, file->block); + + // just relocate what exists into new block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // either read from dirty cache or disk + for (lfs_off_t i = 0; i < file->off; i++) { + uint8_t data; + err = lfs_cache_read(lfs, &lfs->rcache, &file->cache, + file->block, i, &data, 1); + if (err) { + return err; + } + + err = lfs_cache_prog(lfs, &lfs->pcache, &lfs->rcache, + nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // copy over new state of file + memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size); + file->cache.block = lfs->pcache.block; + file->cache.off = lfs->pcache.off; + lfs_cache_zero(lfs, &lfs->pcache); + + file->block = nblock; + return 0; +} + +static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) { + if (file->flags & LFS_F_READING) { + // just drop read cache + lfs_cache_drop(lfs, &file->cache); + file->flags &= ~LFS_F_READING; + } + + if (file->flags & LFS_F_WRITING) { + lfs_off_t pos = file->pos; + + // copy over anything after current branch + lfs_file_t orig = { + .head = file->head, + .size = file->size, + .flags = LFS_O_RDONLY, + .pos = file->pos, + .cache = lfs->rcache, + }; + lfs_cache_drop(lfs, &lfs->rcache); + + while (file->pos < file->size) { + // copy over a byte at a time, leave it up to caching + // to make this efficient + uint8_t data; + lfs_ssize_t res = lfs_file_read(lfs, &orig, &data, 1); + if (res < 0) { + return res; + } + + res = lfs_file_write(lfs, file, &data, 1); + if (res < 0) { + return res; + } + + // keep our reference to the rcache in sync + if (lfs->rcache.block != 0xffffffff) { + lfs_cache_drop(lfs, &orig.cache); + lfs_cache_drop(lfs, &lfs->rcache); + } + } + + // write out what we have + while (true) { + int err = lfs_cache_flush(lfs, &file->cache, &lfs->rcache); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + break; +relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + return err; + } + } + + // actual file updates + file->head = file->block; + file->size = file->pos; + file->flags &= ~LFS_F_WRITING; + file->flags |= LFS_F_DIRTY; + + file->pos = pos; + } + + return 0; +} + +int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + if ((file->flags & LFS_F_DIRTY) && + !(file->flags & LFS_F_ERRED) && + !lfs_pairisnull(file->pair)) { + // update dir entry + lfs_dir_t cwd; + err = lfs_dir_fetch(lfs, &cwd, file->pair); + if (err) { + return err; + } + + lfs_entry_t entry = {.off = file->poff}; + err = lfs_bd_read(lfs, cwd.pair[0], entry.off, + &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); + if (err) { + return err; + } + + LFS_ASSERT(entry.d.type == LFS_TYPE_REG); + entry.d.u.file.head = file->head; + entry.d.u.file.size = file->size; + + err = lfs_dir_update(lfs, &cwd, &entry, NULL); + if (err) { + return err; + } + + file->flags &= ~LFS_F_DIRTY; + } + + return 0; +} + +lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size) { + uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & 3) == LFS_O_WRONLY) { + return LFS_ERR_BADF; + } + + if (file->flags & LFS_F_WRITING) { + // flush out any writes + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + } + + if (file->pos >= file->size) { + // eof if past end + return 0; + } + + size = lfs_min(size, file->size - file->pos); + nsize = size; + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_READING) || + file->off == lfs->cfg->block_size) { + int err = lfs_ctz_find(lfs, &file->cache, NULL, + file->head, file->size, + file->pos, &file->block, &file->off); + if (err) { + return err; + } + + file->flags |= LFS_F_READING; + } + + // read as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + int err = lfs_cache_read(lfs, &file->cache, NULL, + file->block, file->off, data, diff); + if (err) { + return err; + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + } + + return size; +} + +lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + if (file->flags & LFS_F_READING) { + // drop any reads + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + } + + if ((file->flags & LFS_O_APPEND) && file->pos < file->size) { + file->pos = file->size; + } + + if (file->pos + size > LFS_FILE_MAX) { + // larger than file limit? + return LFS_ERR_FBIG; + } + + if (!(file->flags & LFS_F_WRITING) && file->pos > file->size) { + // fill with zeros + lfs_off_t pos = file->pos; + file->pos = file->size; + + while (file->pos < pos) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + } + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_WRITING) || + file->off == lfs->cfg->block_size) { + if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { + // find out which block we're extending from + int err = lfs_ctz_find(lfs, &file->cache, NULL, + file->head, file->size, + file->pos-1, &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + // mark cache as dirty since we may have read data into it + lfs_cache_zero(lfs, &file->cache); + } + + // extend file with new blocks + lfs_alloc_ack(lfs); + int err = lfs_ctz_extend(lfs, &lfs->rcache, &file->cache, + file->block, file->pos, + &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + file->flags |= LFS_F_WRITING; + } + + // program as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + while (true) { + int err = lfs_cache_prog(lfs, &file->cache, &lfs->rcache, + file->block, file->off, data, diff); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + file->flags |= LFS_F_ERRED; + return err; + } + + break; +relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + + lfs_alloc_ack(lfs); + } + + file->flags &= ~LFS_F_ERRED; + return size; +} + +lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, + lfs_soff_t off, int whence) { + // write out everything beforehand, may be noop if rdonly + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // find new pos + lfs_soff_t npos = file->pos; + if (whence == LFS_SEEK_SET) { + npos = off; + } else if (whence == LFS_SEEK_CUR) { + npos = file->pos + off; + } else if (whence == LFS_SEEK_END) { + npos = file->size + off; + } + + if (npos < 0 || npos > LFS_FILE_MAX) { + // file position out of range + return LFS_ERR_INVAL; + } + + // update pos + file->pos = npos; + return npos; +} + +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { + if ((file->flags & 3) == LFS_O_RDONLY) { + return LFS_ERR_BADF; + } + + lfs_off_t oldsize = lfs_file_size(lfs, file); + if (size < oldsize) { + // need to flush since directly changing metadata + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // lookup new head in ctz skip list + err = lfs_ctz_find(lfs, &file->cache, NULL, + file->head, file->size, + size, &file->head, &(lfs_off_t){0}); + if (err) { + return err; + } + + file->size = size; + file->flags |= LFS_F_DIRTY; + } else if (size > oldsize) { + lfs_off_t pos = file->pos; + + // flush+seek if not already at end + if (file->pos != oldsize) { + int err = lfs_file_seek(lfs, file, 0, LFS_SEEK_END); + if (err < 0) { + return err; + } + } + + // fill with zeros + while (file->pos < size) { + lfs_ssize_t res = lfs_file_write(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + + // restore pos + int err = lfs_file_seek(lfs, file, pos, LFS_SEEK_SET); + if (err < 0) { + return err; + } + } + + return 0; +} + +lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file) { + (void)lfs; + return file->pos; +} + +int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { + lfs_soff_t res = lfs_file_seek(lfs, file, 0, LFS_SEEK_SET); + if (res < 0) { + return res; + } + + return 0; +} + +lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { + (void)lfs; + if (file->flags & LFS_F_WRITING) { + return lfs_max(file->pos, file->size); + } else { + return file->size; + } +} + + +/// General fs operations /// +int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err) { + return err; + } + + memset(info, 0, sizeof(*info)); + info->type = entry.d.type; + if (info->type == LFS_TYPE_REG) { + info->size = entry.d.u.file.size; + } + + if (lfs_paircmp(entry.d.u.dir, lfs->root) == 0) { + strcpy(info->name, "/"); + } else { + err = lfs_bd_read(lfs, cwd.pair[0], + entry.off + 4+entry.d.elen+entry.d.alen, + info->name, entry.d.nlen); + if (err) { + return err; + } + } + + return 0; +} + +int lfs_remove(lfs_t *lfs, const char *path) { + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + lfs_dir_t cwd; + lfs_entry_t entry; + int err = lfs_dir_find(lfs, &cwd, &entry, &path); + if (err) { + return err; + } + + lfs_dir_t dir; + if (entry.d.type == LFS_TYPE_DIR) { + // must be empty before removal, checking size + // without masking top bit checks for any case where + // dir is not empty + err = lfs_dir_fetch(lfs, &dir, entry.d.u.dir); + if (err) { + return err; + } /* else if (dir.d.size != sizeof(dir.d)+4) { + return LFS_ERR_NOTEMPTY; + } adafruit: allow to remove non-empty folder, + According to below issue, comment these 2 line won't corrupt filesystem + https://github.com/ARMmbed/littlefs/issues/43 */ + } + + // remove the entry + err = lfs_dir_remove(lfs, &cwd, &entry); + if (err) { + return err; + } + + // if we were a directory, find pred, replace tail + if (entry.d.type == LFS_TYPE_DIR) { + int res = lfs_pred(lfs, dir.pair, &cwd); + if (res < 0) { + return res; + } + + LFS_ASSERT(res); // must have pred + cwd.d.tail[0] = dir.d.tail[0]; + cwd.d.tail[1] = dir.d.tail[1]; + + err = lfs_dir_commit(lfs, &cwd, NULL, 0); + if (err) { + return err; + } + } + + return 0; +} + +int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { + // deorphan if we haven't yet, needed at most once after poweron + if (!lfs->deorphaned) { + int err = lfs_deorphan(lfs); + if (err) { + return err; + } + } + + // find old entry + lfs_dir_t oldcwd; + lfs_entry_t oldentry; + int err = lfs_dir_find(lfs, &oldcwd, &oldentry, &(const char *){oldpath}); + if (err) { + return err; + } + + // mark as moving + oldentry.d.type |= 0x80; + err = lfs_dir_update(lfs, &oldcwd, &oldentry, NULL); + if (err) { + return err; + } + + // allocate new entry + lfs_dir_t newcwd; + lfs_entry_t preventry; + err = lfs_dir_find(lfs, &newcwd, &preventry, &newpath); + if (err && (err != LFS_ERR_NOENT || strchr(newpath, '/') != NULL)) { + return err; + } + + // must have same type + bool prevexists = (err != LFS_ERR_NOENT); + if (prevexists && preventry.d.type != (0x7f & oldentry.d.type)) { + return LFS_ERR_ISDIR; + } + + lfs_dir_t dir; + if (prevexists && preventry.d.type == LFS_TYPE_DIR) { + // must be empty before removal, checking size + // without masking top bit checks for any case where + // dir is not empty + err = lfs_dir_fetch(lfs, &dir, preventry.d.u.dir); + if (err) { + return err; + } else if (dir.d.size != sizeof(dir.d)+4) { + return LFS_ERR_NOTEMPTY; + } + } + + // move to new location + lfs_entry_t newentry = preventry; + newentry.d = oldentry.d; + newentry.d.type &= ~0x80; + newentry.d.nlen = strlen(newpath); + + if (prevexists) { + err = lfs_dir_update(lfs, &newcwd, &newentry, newpath); + if (err) { + return err; + } + } else { + err = lfs_dir_append(lfs, &newcwd, &newentry, newpath); + if (err) { + return err; + } + } + + // fetch old pair again in case dir block changed + lfs->moving = true; + err = lfs_dir_find(lfs, &oldcwd, &oldentry, &oldpath); + if (err) { + return err; + } + lfs->moving = false; + + // remove old entry + err = lfs_dir_remove(lfs, &oldcwd, &oldentry); + if (err) { + return err; + } + + // if we were a directory, find pred, replace tail + if (prevexists && preventry.d.type == LFS_TYPE_DIR) { + int res = lfs_pred(lfs, dir.pair, &newcwd); + if (res < 0) { + return res; + } + + LFS_ASSERT(res); // must have pred + newcwd.d.tail[0] = dir.d.tail[0]; + newcwd.d.tail[1] = dir.d.tail[1]; + + err = lfs_dir_commit(lfs, &newcwd, NULL, 0); + if (err) { + return err; + } + } + + return 0; +} + + +/// Filesystem operations /// +static void lfs_deinit(lfs_t *lfs) { + // free allocated memory + if (!lfs->cfg->read_buffer) { + lfs_free(lfs->rcache.buffer); + } + + if (!lfs->cfg->prog_buffer) { + lfs_free(lfs->pcache.buffer); + } + + if (!lfs->cfg->lookahead_buffer) { + lfs_free(lfs->free.buffer); + } +} + +static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { + lfs->cfg = cfg; + + // setup read cache + if (lfs->cfg->read_buffer) { + lfs->rcache.buffer = lfs->cfg->read_buffer; + } else { + lfs->rcache.buffer = lfs_malloc(lfs->cfg->read_size); + if (!lfs->rcache.buffer) { + goto cleanup; + } + } + + // setup program cache + if (lfs->cfg->prog_buffer) { + lfs->pcache.buffer = lfs->cfg->prog_buffer; + } else { + lfs->pcache.buffer = lfs_malloc(lfs->cfg->prog_size); + if (!lfs->pcache.buffer) { + goto cleanup; + } + } + + // zero to avoid information leaks + lfs_cache_zero(lfs, &lfs->pcache); + lfs_cache_drop(lfs, &lfs->rcache); + + // setup lookahead, round down to nearest 32-bits + LFS_ASSERT(lfs->cfg->lookahead % 32 == 0); + LFS_ASSERT(lfs->cfg->lookahead > 0); + if (lfs->cfg->lookahead_buffer) { + lfs->free.buffer = lfs->cfg->lookahead_buffer; + } else { + lfs->free.buffer = lfs_malloc(lfs->cfg->lookahead/8); + if (!lfs->free.buffer) { + goto cleanup; + } + } + + // check that program and read sizes are multiples of the block size + LFS_ASSERT(lfs->cfg->prog_size % lfs->cfg->read_size == 0); + LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->prog_size == 0); + + // check that the block size is large enough to fit ctz pointers + LFS_ASSERT(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4)) + <= lfs->cfg->block_size); + + // setup default state + lfs->root[0] = 0xffffffff; + lfs->root[1] = 0xffffffff; + lfs->files = NULL; + lfs->dirs = NULL; + lfs->deorphaned = false; + lfs->moving = false; + + return 0; + +cleanup: + lfs_deinit(lfs); + return LFS_ERR_NOMEM; +} + +int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { + int err = 0; + if (true) { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + // create free lookahead + memset(lfs->free.buffer, 0, lfs->cfg->lookahead/8); + lfs->free.off = 0; + lfs->free.size = lfs_min(lfs->cfg->lookahead, lfs->cfg->block_count); + lfs->free.i = 0; + lfs_alloc_ack(lfs); + + // create superblock dir + lfs_dir_t superdir; + err = lfs_dir_alloc(lfs, &superdir); + if (err) { + goto cleanup; + } + + // write root directory + lfs_dir_t root; + err = lfs_dir_alloc(lfs, &root); + if (err) { + goto cleanup; + } + + err = lfs_dir_commit(lfs, &root, NULL, 0); + if (err) { + goto cleanup; + } + + lfs->root[0] = root.pair[0]; + lfs->root[1] = root.pair[1]; + + // write superblocks + lfs_superblock_t superblock = { + .off = sizeof(superdir.d), + .d.type = LFS_TYPE_SUPERBLOCK, + .d.elen = sizeof(superblock.d) - sizeof(superblock.d.magic) - 4, + .d.nlen = sizeof(superblock.d.magic), + .d.version = LFS_DISK_VERSION, + .d.magic = {"littlefs"}, + .d.block_size = lfs->cfg->block_size, + .d.block_count = lfs->cfg->block_count, + .d.root = {lfs->root[0], lfs->root[1]}, + }; + superdir.d.tail[0] = root.pair[0]; + superdir.d.tail[1] = root.pair[1]; + superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d) + 4; + + // write both pairs to be safe + lfs_superblock_tole32(&superblock.d); + bool valid = false; + for (int i = 0; i < 2; i++) { + err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){ + {sizeof(superdir.d), sizeof(superblock.d), + &superblock.d, sizeof(superblock.d)} + }, 1); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + valid = valid || !err; + } + + if (!valid) { + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + // sanity check that fetch works + err = lfs_dir_fetch(lfs, &superdir, (const lfs_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + + lfs_alloc_ack(lfs); + } + +cleanup: + lfs_deinit(lfs); + return err; +} + +int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { + int err = 0; + if (true) { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + // setup free lookahead + lfs->free.off = 0; + lfs->free.size = 0; + lfs->free.i = 0; + lfs_alloc_ack(lfs); + + // load superblock + lfs_dir_t dir; + lfs_superblock_t superblock; + err = lfs_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + if (!err) { + err = lfs_bd_read(lfs, dir.pair[0], sizeof(dir.d), + &superblock.d, sizeof(superblock.d)); + lfs_superblock_fromle32(&superblock.d); + if (err) { + goto cleanup; + } + + lfs->root[0] = superblock.d.root[0]; + lfs->root[1] = superblock.d.root[1]; + } + + if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { + LFS_ERROR("Invalid superblock at %d %d", 0, 1); + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + uint16_t major_version = (0xffff & (superblock.d.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); + if ((major_version != LFS_DISK_VERSION_MAJOR || + minor_version > LFS_DISK_VERSION_MINOR)) { + LFS_ERROR("Invalid version %d.%d", major_version, minor_version); + err = LFS_ERR_INVAL; + goto cleanup; + } + + return 0; + } + +cleanup: + + lfs_deinit(lfs); + return err; +} + +int lfs_unmount(lfs_t *lfs) { + lfs_deinit(lfs); + return 0; +} + + +/// Littlefs specific operations /// +int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // iterate over metadata pairs + lfs_dir_t dir; + lfs_entry_t entry; + lfs_block_t cwd[2] = {0, 1}; + + while (true) { + for (int i = 0; i < 2; i++) { + int err = cb(data, cwd[i]); + if (err) { + return err; + } + } + + int err = lfs_dir_fetch(lfs, &dir, cwd); + if (err) { + return err; + } + + // iterate over contents + while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) { + err = lfs_bd_read(lfs, dir.pair[0], dir.off, + &entry.d, sizeof(entry.d)); + lfs_entry_fromle32(&entry.d); + if (err) { + return err; + } + + dir.off += lfs_entry_size(&entry); + if ((0x70 & entry.d.type) == (0x70 & LFS_TYPE_REG)) { + err = lfs_ctz_traverse(lfs, &lfs->rcache, NULL, + entry.d.u.file.head, entry.d.u.file.size, cb, data); + if (err) { + return err; + } + } + } + + cwd[0] = dir.d.tail[0]; + cwd[1] = dir.d.tail[1]; + + if (lfs_pairisnull(cwd)) { + break; + } + } + + // iterate over any open files + for (lfs_file_t *f = lfs->files; f; f = f->next) { + if (f->flags & LFS_F_DIRTY) { + int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, + f->head, f->size, cb, data); + if (err) { + return err; + } + } + + if (f->flags & LFS_F_WRITING) { + int err = lfs_ctz_traverse(lfs, &lfs->rcache, &f->cache, + f->block, f->pos, cb, data); + if (err) { + return err; + } + } + } + + return 0; +} + +static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // iterate over all directory directory entries + int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } + + while (!lfs_pairisnull(pdir->d.tail)) { + if (lfs_paircmp(pdir->d.tail, dir) == 0) { + return true; + } + + err = lfs_dir_fetch(lfs, pdir, pdir->d.tail); + if (err) { + return err; + } + } + + return false; +} + +static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2], + lfs_dir_t *parent, lfs_entry_t *entry) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + parent->d.tail[0] = 0; + parent->d.tail[1] = 1; + + // iterate over all directory directory entries + while (!lfs_pairisnull(parent->d.tail)) { + int err = lfs_dir_fetch(lfs, parent, parent->d.tail); + if (err) { + return err; + } + + while (true) { + err = lfs_dir_next(lfs, parent, entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + if (((0x70 & entry->d.type) == (0x70 & LFS_TYPE_DIR)) && + lfs_paircmp(entry->d.u.dir, dir) == 0) { + return true; + } + } + } + + return false; +} + +static int lfs_moved(lfs_t *lfs, const void *e) { + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + // skip superblock + lfs_dir_t cwd; + int err = lfs_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } + + // iterate over all directory directory entries + lfs_entry_t entry; + while (!lfs_pairisnull(cwd.d.tail)) { + err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; + } + + while (true) { + err = lfs_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + if (!(0x80 & entry.d.type) && + memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { + return true; + } + } + } + + return false; +} + +static int lfs_relocate(lfs_t *lfs, + const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) { + // find parent + lfs_dir_t parent; + lfs_entry_t entry; + int res = lfs_parent(lfs, oldpair, &parent, &entry); + if (res < 0) { + return res; + } + + if (res) { + // update disk, this creates a desync + entry.d.u.dir[0] = newpair[0]; + entry.d.u.dir[1] = newpair[1]; + + int err = lfs_dir_update(lfs, &parent, &entry, NULL); + if (err) { + return err; + } + + // update internal root + if (lfs_paircmp(oldpair, lfs->root) == 0) { + LFS_DEBUG("Relocating root %" PRIu32 " %" PRIu32, + newpair[0], newpair[1]); + lfs->root[0] = newpair[0]; + lfs->root[1] = newpair[1]; + } + + // clean up bad block, which should now be a desync + return lfs_deorphan(lfs); + } + + // find pred + res = lfs_pred(lfs, oldpair, &parent); + if (res < 0) { + return res; + } + + if (res) { + // just replace bad pair, no desync can occur + parent.d.tail[0] = newpair[0]; + parent.d.tail[1] = newpair[1]; + + return lfs_dir_commit(lfs, &parent, NULL, 0); + } + + // couldn't find dir, must be new + return 0; +} + +int lfs_deorphan(lfs_t *lfs) { + lfs->deorphaned = true; + + if (lfs_pairisnull(lfs->root)) { + return 0; + } + + lfs_dir_t pdir = {.d.size = 0x80000000}; + lfs_dir_t cwd = {.d.tail[0] = 0, .d.tail[1] = 1}; + + // iterate over all directory directory entries + for (lfs_size_t i = 0; i < lfs->cfg->block_count; i++) { + if (lfs_pairisnull(cwd.d.tail)) { + return 0; + } + + int err = lfs_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; + } + + // check head blocks for orphans + if (!(0x80000000 & pdir.d.size)) { + // check if we have a parent + lfs_dir_t parent; + lfs_entry_t entry; + int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry); + if (res < 0) { + return res; + } + + if (!res) { + // we are an orphan + LFS_DEBUG("Found orphan %" PRIu32 " %" PRIu32, + pdir.d.tail[0], pdir.d.tail[1]); + + pdir.d.tail[0] = cwd.d.tail[0]; + pdir.d.tail[1] = cwd.d.tail[1]; + + err = lfs_dir_commit(lfs, &pdir, NULL, 0); + if (err) { + return err; + } + + return 0; + } + + if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) { + // we have desynced + LFS_DEBUG("Found desync %" PRIu32 " %" PRIu32, + entry.d.u.dir[0], entry.d.u.dir[1]); + + pdir.d.tail[0] = entry.d.u.dir[0]; + pdir.d.tail[1] = entry.d.u.dir[1]; + + err = lfs_dir_commit(lfs, &pdir, NULL, 0); + if (err) { + return err; + } + + return 0; + } + } + + // check entries for moves + lfs_entry_t entry; + while (true) { + err = lfs_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + // found moved entry + if (entry.d.type & 0x80) { + int moved = lfs_moved(lfs, &entry.d.u); + if (moved < 0) { + return moved; + } + + if (moved) { + LFS_DEBUG("Found move %" PRIu32 " %" PRIu32, + entry.d.u.dir[0], entry.d.u.dir[1]); + err = lfs_dir_remove(lfs, &cwd, &entry); + if (err) { + return err; + } + } else { + LFS_DEBUG("Found partial move %" PRIu32 " %" PRIu32, + entry.d.u.dir[0], entry.d.u.dir[1]); + entry.d.type &= ~0x80; + err = lfs_dir_update(lfs, &cwd, &entry, NULL); + if (err) { + return err; + } + } + } + } + + memcpy(&pdir, &cwd, sizeof(pdir)); + } + + // If we reached here, we have more directory pairs than blocks in the + // filesystem... So something must be horribly wrong + return LFS_ERR_CORRUPT; +} diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs.h b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs.h new file mode 100644 index 00000000..9c3174e7 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs.h @@ -0,0 +1,501 @@ +/* + * The little filesystem + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS_H +#define LFS_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + +/// Version info /// + +// Software library version +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS_VERSION 0x00010007 +#define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16)) +#define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS_DISK_VERSION 0x00010001 +#define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16)) +#define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >> 0)) + + +/// Definitions /// + +// Type definitions +typedef uint32_t lfs_size_t; +typedef uint32_t lfs_off_t; + +typedef int32_t lfs_ssize_t; +typedef int32_t lfs_soff_t; + +typedef uint32_t lfs_block_t; + +// Max name size in bytes +#ifndef LFS_NAME_MAX +#define LFS_NAME_MAX 255 +#endif + +// Max file size in bytes +#ifndef LFS_FILE_MAX +#define LFS_FILE_MAX 2147483647 +#endif + +// Possible error codes, these are negative to allow +// valid positive return values +enum lfs_error { + LFS_ERR_OK = 0, // No error + LFS_ERR_IO = -5, // Error during device operation + LFS_ERR_CORRUPT = -52, // Corrupted + LFS_ERR_NOENT = -2, // No directory entry + LFS_ERR_EXIST = -17, // Entry already exists + LFS_ERR_NOTDIR = -20, // Entry is not a dir + LFS_ERR_ISDIR = -21, // Entry is a dir + LFS_ERR_NOTEMPTY = -39, // Dir is not empty + LFS_ERR_BADF = -9, // Bad file number + LFS_ERR_FBIG = -27, // File too large + LFS_ERR_INVAL = -22, // Invalid parameter + LFS_ERR_NOSPC = -28, // No space left on device + LFS_ERR_NOMEM = -12, // No more memory available +}; + +// File types +enum lfs_type { + LFS_TYPE_REG = 0x11, + LFS_TYPE_DIR = 0x22, + LFS_TYPE_SUPERBLOCK = 0x2e, +}; + +// File open flags +enum lfs_open_flags { + // open flags + LFS_O_RDONLY = 1, // Open a file as read only + LFS_O_WRONLY = 2, // Open a file as write only + LFS_O_RDWR = 3, // Open a file as read and write + LFS_O_CREAT = 0x0100, // Create a file if it does not exist + LFS_O_EXCL = 0x0200, // Fail if a file already exists + LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size + LFS_O_APPEND = 0x0800, // Move to end of file on every write + + // internally used flags + LFS_F_DIRTY = 0x10000, // File does not match storage + LFS_F_WRITING = 0x20000, // File has been written since last flush + LFS_F_READING = 0x40000, // File has been read since last flush + LFS_F_ERRED = 0x80000, // An error occured during write +}; + +// File seek flags +enum lfs_whence_flags { + LFS_SEEK_SET = 0, // Seek relative to an absolute position + LFS_SEEK_CUR = 1, // Seek relative to the current file position + LFS_SEEK_END = 2, // Seek relative to the end of the file +}; + + +// Configuration provided during initialization of the littlefs +struct lfs_config { + // Opaque user provided context that can be used to pass + // information to the block device operations + void *context; + + // Read a region in a block. Negative error codes are propogated + // to the user. + int (*read)(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size); + + // Program a region in a block. The block must have previously + // been erased. Negative error codes are propogated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*prog)(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size); + + // Erase a block. A block must be erased before being programmed. + // The state of an erased block is undefined. Negative error codes + // are propogated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*erase)(const struct lfs_config *c, lfs_block_t block); + + // Sync the state of the underlying block device. Negative error codes + // are propogated to the user. + int (*sync)(const struct lfs_config *c); + + // Minimum size of a block read. This determines the size of read buffers. + // This may be larger than the physical read size to improve performance + // by caching more of the block device. + lfs_size_t read_size; + + // Minimum size of a block program. This determines the size of program + // buffers. This may be larger than the physical program size to improve + // performance by caching more of the block device. + // Must be a multiple of the read size. + lfs_size_t prog_size; + + // Size of an erasable block. This does not impact ram consumption and + // may be larger than the physical erase size. However, this should be + // kept small as each file currently takes up an entire block. + // Must be a multiple of the program size. + lfs_size_t block_size; + + // Number of erasable blocks on the device. + lfs_size_t block_count; + + // Number of blocks to lookahead during block allocation. A larger + // lookahead reduces the number of passes required to allocate a block. + // The lookahead buffer requires only 1 bit per block so it can be quite + // large with little ram impact. Should be a multiple of 32. + lfs_size_t lookahead; + + // Optional, statically allocated read buffer. Must be read sized. + void *read_buffer; + + // Optional, statically allocated program buffer. Must be program sized. + void *prog_buffer; + + // Optional, statically allocated lookahead buffer. Must be 1 bit per + // lookahead block. + void *lookahead_buffer; + + // Optional, statically allocated buffer for files. Must be program sized. + // If enabled, only one file may be opened at a time. + void *file_buffer; +}; + +// Optional configuration provided during lfs_file_opencfg +struct lfs_file_config { + // Optional, statically allocated buffer for files. Must be program sized. + // If NULL, malloc will be used by default. + void *buffer; +}; + +// File info structure +struct lfs_info { + // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR + uint8_t type; + + // Size of the file, only valid for REG files + lfs_size_t size; + + // Name of the file stored as a null-terminated string + char name[LFS_NAME_MAX+1]; +}; + + +/// littlefs data structures /// +typedef struct lfs_entry { + lfs_off_t off; + + struct lfs_disk_entry { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + union { + struct { + lfs_block_t head; + lfs_size_t size; + } file; + lfs_block_t dir[2]; + } u; + } d; +} lfs_entry_t; + +typedef struct lfs_cache { + lfs_block_t block; + lfs_off_t off; + uint8_t *buffer; +} lfs_cache_t; + +typedef struct lfs_file { + struct lfs_file *next; + lfs_block_t pair[2]; + lfs_off_t poff; + + lfs_block_t head; + lfs_size_t size; + + const struct lfs_file_config *cfg; + uint32_t flags; + lfs_off_t pos; + lfs_block_t block; + lfs_off_t off; + lfs_cache_t cache; +} lfs_file_t; + +typedef struct lfs_dir { + struct lfs_dir *next; + lfs_block_t pair[2]; + lfs_off_t off; + + lfs_block_t head[2]; + lfs_off_t pos; + + struct lfs_disk_dir { + uint32_t rev; + lfs_size_t size; + lfs_block_t tail[2]; + } d; +} lfs_dir_t; + +typedef struct lfs_superblock { + lfs_off_t off; + + struct lfs_disk_superblock { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + lfs_block_t root[2]; + uint32_t block_size; + uint32_t block_count; + uint32_t version; + char magic[8]; + } d; +} lfs_superblock_t; + +typedef struct lfs_free { + lfs_block_t off; + lfs_block_t size; + lfs_block_t i; + lfs_block_t ack; + uint32_t *buffer; +} lfs_free_t; + +// The littlefs type +typedef struct lfs { + const struct lfs_config *cfg; + + lfs_block_t root[2]; + lfs_file_t *files; + lfs_dir_t *dirs; + + lfs_cache_t rcache; + lfs_cache_t pcache; + + lfs_free_t free; + bool deorphaned; + bool moving; +} lfs_t; + + +/// Filesystem functions /// + +// Format a block device with the littlefs +// +// Requires a littlefs object and config struct. This clobbers the littlefs +// object, and does not leave the filesystem mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_format(lfs_t *lfs, const struct lfs_config *config); + +// Mounts a littlefs +// +// Requires a littlefs object and config struct. Multiple filesystems +// may be mounted simultaneously with multiple littlefs objects. Both +// lfs and config must be allocated while mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_mount(lfs_t *lfs, const struct lfs_config *config); + +// Unmounts a littlefs +// +// Does nothing besides releasing any allocated resources. +// Returns a negative error code on failure. +int lfs_unmount(lfs_t *lfs); + +/// General operations /// + +// Removes a file or directory +// +// If removing a directory, the directory must be empty. +// Returns a negative error code on failure. +int lfs_remove(lfs_t *lfs, const char *path); + +// Rename or move a file or directory +// +// If the destination exists, it must match the source in type. +// If the destination is a directory, the directory must be empty. +// +// Returns a negative error code on failure. +int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath); + +// Find info about a file or directory +// +// Fills out the info structure, based on the specified file or directory. +// Returns a negative error code on failure. +int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info); + + +/// File operations /// + +// Open a file +// +// The mode that the file is opened in is determined by the flags, which +// are values from the enum lfs_open_flags that are bitwise-ored together. +// +// Returns a negative error code on failure. +int lfs_file_open(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags); + +// Open a file with extra configuration +// +// The mode that the file is opened in is determined by the flags, which +// are values from the enum lfs_open_flags that are bitwise-ored together. +// +// The config struct provides additional config options per file as described +// above. The config struct must be allocated while the file is open, and the +// config struct must be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags, + const struct lfs_file_config *config); + +// Close a file +// +// Any pending writes are written out to storage as though +// sync had been called and releases any allocated resources. +// +// Returns a negative error code on failure. +int lfs_file_close(lfs_t *lfs, lfs_file_t *file); + +// Synchronize a file on storage +// +// Any pending writes are written out to storage. +// Returns a negative error code on failure. +int lfs_file_sync(lfs_t *lfs, lfs_file_t *file); + +// Read data from file +// +// Takes a buffer and size indicating where to store the read data. +// Returns the number of bytes read, or a negative error code on failure. +lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size); + +// Write data to file +// +// Takes a buffer and size indicating the data to write. The file will not +// actually be updated on the storage until either sync or close is called. +// +// Returns the number of bytes written, or a negative error code on failure. +lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size); + +// Change the position of the file +// +// The change in position is determined by the offset and whence flag. +// Returns the old position of the file, or a negative error code on failure. +lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, + lfs_soff_t off, int whence); + +// Truncates the size of the file to the specified size +// +// Returns a negative error code on failure. +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size); + +// Return the position of the file +// +// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) +// Returns the position of the file, or a negative error code on failure. +lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file); + +// Change the position of the file to the beginning of the file +// +// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) +// Returns a negative error code on failure. +int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file); + +// Return the size of the file +// +// Similar to lfs_file_seek(lfs, file, 0, LFS_SEEK_END) +// Returns the size of the file, or a negative error code on failure. +lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file); + + +/// Directory operations /// + +// Create a directory +// +// Returns a negative error code on failure. +int lfs_mkdir(lfs_t *lfs, const char *path); + +// Open a directory +// +// Once open a directory can be used with read to iterate over files. +// Returns a negative error code on failure. +int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path); + +// Close a directory +// +// Releases any allocated resources. +// Returns a negative error code on failure. +int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir); + +// Read an entry in the directory +// +// Fills out the info structure, based on the specified file or directory. +// Returns a negative error code on failure. +int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info); + +// Change the position of the directory +// +// The new off must be a value previous returned from tell and specifies +// an absolute offset in the directory seek. +// +// Returns a negative error code on failure. +int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off); + +// Return the position of the directory +// +// The returned offset is only meant to be consumed by seek and may not make +// sense, but does indicate the current position in the directory iteration. +// +// Returns the position of the directory, or a negative error code on failure. +lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir); + +// Change the position of the directory to the beginning of the directory +// +// Returns a negative error code on failure. +int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir); + + +/// Miscellaneous littlefs specific operations /// + +// Traverse through all blocks in use by the filesystem +// +// The provided callback will be called with each block address that is +// currently in use by the filesystem. This can be used to determine which +// blocks are in use or how much of the storage is available. +// +// Returns a negative error code on failure. +int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data); + +// Prunes any recoverable errors that may have occured in the filesystem +// +// Not needed to be called by user unless an operation is interrupted +// but the filesystem is still mounted. This is already called on first +// allocation. +// +// Returns a negative error code on failure. +int lfs_deorphan(lfs_t *lfs); + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs_util.c b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs_util.c new file mode 100644 index 00000000..9ca0756d --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs_util.c @@ -0,0 +1,31 @@ +/* + * lfs util functions + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs_util.h" + +// Only compile if user does not provide custom config +#ifndef LFS_CONFIG + + +// Software CRC implementation with small lookup table +void lfs_crc(uint32_t *restrict crc, const void *buffer, size_t size) { + static const uint32_t rtable[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, + 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, + 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, + }; + + const uint8_t *data = buffer; + + for (size_t i = 0; i < size; i++) { + *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 0)) & 0xf]; + *crc = (*crc >> 4) ^ rtable[(*crc ^ (data[i] >> 4)) & 0xf]; + } +} + + +#endif diff --git a/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs_util.h b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs_util.h new file mode 100644 index 00000000..4abbb762 --- /dev/null +++ b/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/lfs_util.h @@ -0,0 +1,197 @@ +/* + * lfs utility functions + * + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS_UTIL_H +#define LFS_UTIL_H + +// Users can override lfs_util.h with their own configuration by defining +// LFS_CONFIG as a header file to include (-DLFS_CONFIG=lfs_config.h). +// +// If LFS_CONFIG is used, none of the default utils will be emitted and must be +// provided by the config file. To start I would suggest copying lfs_util.h and +// modifying as needed. +#ifdef LFS_CONFIG +#define LFS_STRINGIZE(x) LFS_STRINGIZE2(x) +#define LFS_STRINGIZE2(x) #x +#include LFS_STRINGIZE(LFS_CONFIG) +#else + +// System includes +#include +#include +#include + +#ifndef LFS_NO_MALLOC +#include +#endif +#ifndef LFS_NO_ASSERT +#include +#endif + +#if !CFG_DEBUG +#define LFS_NO_DEBUG +#define LFS_NO_WARN +#define LFS_NO_ERROR +#endif + +#if !defined(LFS_NO_DEBUG) || !defined(LFS_NO_WARN) || !defined(LFS_NO_ERROR) +#include +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + + +// Macros, may be replaced by system specific wrappers. Arguments to these +// macros must not have side-effects as the macros can be removed for a smaller +// code footprint + +// Logging functions +#ifndef LFS_NO_DEBUG +#define LFS_DEBUG(fmt, ...) \ + printf("lfs debug:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_DEBUG(fmt, ...) +#endif + +#ifndef LFS_NO_WARN +#define LFS_WARN(fmt, ...) \ + printf("lfs warn:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_WARN(fmt, ...) +#endif + +#ifndef LFS_NO_ERROR +#define LFS_ERROR(fmt, ...) \ + printf("lfs error:%d: " fmt "\n", __LINE__, __VA_ARGS__) +#else +#define LFS_ERROR(fmt, ...) +#endif + +// Runtime assertions +#ifndef LFS_NO_ASSERT +#define LFS_ASSERT(test) assert(test) +#else +#define LFS_ASSERT(test) +#endif + + +// Builtin functions, these may be replaced by more efficient +// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more +// expensive basic C implementation for debugging purposes + +// Min/max functions for unsigned 32-bit numbers +static inline uint32_t lfs_max(uint32_t a, uint32_t b) { + return (a > b) ? a : b; +} + +static inline uint32_t lfs_min(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +// Find the next smallest power of 2 less than or equal to a +static inline uint32_t lfs_npw2(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) + return 32 - __builtin_clz(a-1); +#else + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; a >>= s; r |= s; + s = (a > 0xff ) << 3; a >>= s; r |= s; + s = (a > 0xf ) << 2; a >>= s; r |= s; + s = (a > 0x3 ) << 1; a >>= s; r |= s; + return (r | (a >> 1)) + 1; +#endif +} + +// Count the number of trailing binary zeros in a +// lfs_ctz(0) may be undefined +static inline uint32_t lfs_ctz(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) + return __builtin_ctz(a); +#else + return lfs_npw2((a & -a) + 1) - 1; +#endif +} + +// Count the number of binary ones in a +static inline uint32_t lfs_popc(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) + return __builtin_popcount(a); +#else + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +#endif +} + +// Find the sequence comparison of a and b, this is the distance +// between a and b ignoring overflow +static inline int lfs_scmp(uint32_t a, uint32_t b) { + return (int)(unsigned)(a - b); +} + +// Convert from 32-bit little-endian to native order +static inline uint32_t lfs_fromle32(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && ( \ + (defined( BYTE_ORDER ) && BYTE_ORDER == ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return a; +#elif !defined(LFS_NO_INTRINSICS) && ( \ + (defined( BYTE_ORDER ) && BYTE_ORDER == ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && __BYTE_ORDER == __ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) + return __builtin_bswap32(a); +#else + return (((uint8_t*)&a)[0] << 0) | + (((uint8_t*)&a)[1] << 8) | + (((uint8_t*)&a)[2] << 16) | + (((uint8_t*)&a)[3] << 24); +#endif +} + +// Convert to 32-bit little-endian from native order +static inline uint32_t lfs_tole32(uint32_t a) { + return lfs_fromle32(a); +} + +// Calculate CRC-32 with polynomial = 0x04c11db7 +void lfs_crc(uint32_t *crc, const void *buffer, size_t size); + +// Allocate memory, only used if buffers are not provided to littlefs +static inline void *lfs_malloc(size_t size) { +#ifndef LFS_NO_MALLOC + //extern void *pvPortMalloc( size_t xWantedSize ); + //return pvPortMalloc(size); + return malloc(size); +#else + (void)size; + return NULL; +#endif +} + +// Deallocate memory, only used if buffers are not provided to littlefs +static inline void lfs_free(void *p) { +#ifndef LFS_NO_MALLOC + //extern void vPortFree( void *pv ); + //vPortFree(p); + free(p); +#else + (void)p; +#endif +} + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif +#endif diff --git a/arch/stm32/build_hex.py b/arch/stm32/build_hex.py new file mode 100644 index 00000000..ffdb5d2f --- /dev/null +++ b/arch/stm32/build_hex.py @@ -0,0 +1,10 @@ +Import("env") + +# Make custom HEX from ELF +env.AddPostAction( + "$BUILD_DIR/${PROGNAME}.elf", + env.VerboseAction(" ".join([ + "$OBJCOPY", "-O", "ihex", "-R", ".eeprom", + '"$BUILD_DIR/${PROGNAME}.elf"', '"$BUILD_DIR/${PROGNAME}.hex"' + ]), "Building $BUILD_DIR/${PROGNAME}.hex") +) \ No newline at end of file diff --git a/boards/heltec_t114.json b/boards/heltec_t114.json index 6df7266d..86f72e73 100644 --- a/boards/heltec_t114.json +++ b/boards/heltec_t114.json @@ -34,7 +34,8 @@ ], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" }, "frameworks": [ "arduino" diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json new file mode 100644 index 00000000..11e7ebaa --- /dev/null +++ b/boards/nano-g2-ultra.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x8029" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ], + [ + "0x239A", + "0x802A" + ] + ], + "usb_product": "BQ nRF52840", + "mcu": "nrf52840", + "variant": "nano-g2-ultra", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": [ + "arduino" + ], + "name": "BQ nRF52840", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra", + "vendor": "BQ Consulting" +} \ No newline at end of file diff --git a/boards/promicro_nrf52840.json b/boards/promicro_nrf52840.json index 346f0f26..a428ffde 100644 --- a/boards/promicro_nrf52840.json +++ b/boards/promicro_nrf52840.json @@ -8,7 +8,7 @@ "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ - [ + [ "0x239A", "0x00B3" ], @@ -51,7 +51,8 @@ ], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" }, "frameworks": [ "arduino", diff --git a/boards/rak3172.json b/boards/rak3172.json new file mode 100644 index 00000000..dbb70b03 --- /dev/null +++ b/boards/rak3172.json @@ -0,0 +1,33 @@ +{ + "build": { + "arduino": { + "variant_h": "variant_RAK3172_MODULE.h" + }, + "core": "stm32", + "cpu": "cortex-m4", + "extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WLE5xx", + "framework_extra_flags": { + "arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_RAK3172_MODULE" + }, + "f_cpu": "48000000L", + "mcu": "stm32wle5ccu", + "product_line": "STM32WLE5xx", + "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U" + }, + "debug": { + "default_tools": ["stlink"], + "jlink_device": "STM32WLE5CC", + "openocd_target": "stm32wlx", + "svd_path": "STM32WLE5_CM4.svd" + }, + "frameworks": ["arduino"], + "name": "BB-STM32WL", + "upload": { + "maximum_ram_size": 65536, + "maximum_size": 262144, + "protocol": "stlink", + "protocols": ["stlink", "jlink"] + }, + "url": "https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172", + "vendor": "RAK" +} diff --git a/boards/t-echo.json b/boards/t-echo.json index 6bfa64df..8deea1bc 100644 --- a/boards/t-echo.json +++ b/boards/t-echo.json @@ -1,32 +1,32 @@ { "build": { "arduino": { - "ldscript": "nrf52840_s140_v6.ld" + "ldscript": "nrf52840_s140_v6.ld" }, "core": "nRF5", "cpu": "cortex-m4", "extra_flags": "-DARDUINO_NRF52840_PCA10056 -DNRF52840_XXAA", "f_cpu": "64000000L", "hwids": [ - [ - "0x239A", - "0x8029" - ] + [ + "0x239A", + "0x8029" + ] ], "usb_product": "NRF52 DK", "mcu": "nrf52840", "variant": "pca10056", "bsp": { - "name": "adafruit" + "name": "adafruit" }, "softdevice": { - "sd_flags": "-DS140", - "sd_name": "s140", - "sd_version": "6.1.1", - "sd_fwid": "0x00B6" + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" }, "bootloader": { - "settings_addr": "0xFF000" + "settings_addr": "0xFF000" } }, "connectivity": [ @@ -35,9 +35,10 @@ "debug": { "jlink_device": "nRF52840_xxAA", "onboard_tools": [ - "jlink" + "jlink" ], - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" }, "frameworks": [ "arduino" @@ -50,13 +51,13 @@ "speed": 115200, "protocol": "jlink", "protocols": [ - "jlink", - "nrfjprog", - "stlink", - "cmsis-dap", - "blackmagic" + "jlink", + "nrfjprog", + "stlink", + "cmsis-dap", + "blackmagic" ] }, "url": "https://os.mbed.com/platforms/Nordic-nRF52840-DK/", "vendor": "Nordic" - } \ No newline at end of file +} \ No newline at end of file diff --git a/boards/thinknode_m1.json b/boards/thinknode_m1.json new file mode 100644 index 00000000..9f486285 --- /dev/null +++ b/boards/thinknode_m1.json @@ -0,0 +1,72 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_TTGO_EINK -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + [ + "0x239A", + "0x4405" + ], + [ + "0x239A", + "0x0029" + ], + [ + "0x239A", + "0x002A" + ] + ], + "usb_product": "elecrow_eink", + "mcu": "nrf52840", + "variant": "ELECROW-ThinkNode-M1", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": [ + "bluetooth" + ], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": [ + "jlink" + ], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "elecrow eink", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink" + ] + }, + "url": "https://github.com/Elecrow-RD", + "vendor": "ELECROW" +} \ No newline at end of file diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index 2be716e2..fc740ac5 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52.cfg" }, "frameworks": ["arduino"], "name": "Seeed T1000-E", diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 10cabe7a..095a1633 --- a/build.sh +++ b/build.sh @@ -58,7 +58,7 @@ build_firmware() { fi # build .uf2 for nrf52 boards - if [[ $1 == *"RAK_4631"* || $1 == *"t1000e"* || $1 == *"t114"* || $1 == *"T-Echo"* || $1 == *"Faketec"* || $1 == *"ProMicro"* ]]; then + if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then python bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 fi diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..828c0ee8 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {} }: +let +in + pkgs.mkShell { + buildInputs = [ + pkgs.platformio + # optional: needed as a programmer i.e. for esp32 + pkgs.avrdude + ]; +} diff --git a/docs/faq.md b/docs/faq.md index 411de350..030c5531 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -16,7 +16,7 @@ author: https://github.com/LitBomb - [1.2.4. Repeater](#124-repeater) - [1.2.5. Room Server](#125-room-server) - [2. Initial Setup](#2-initial-setup) - - [2.1. Q: How many devices do I need to start using meshcore?](#21-q-how-many-devices-do-i-need-to-start-using-meshcore) + - [2.1. Q: How many devices do I need to start using MeshCore?](#21-q-how-many-devices-do-i-need-to-start-using-meshcore) - [2.2. Q: Does MeshCore cost any money?](#22-q-does-meshcore-cost-any-money) - [2.3. Q: What frequencies are supported by MeshCore?](#23-q-what-frequencies-are-supported-by-meshcore) - [2.4. Q: What is an "advert" in MeshCore?](#24-q-what-is-an-advert-in-meshcore) @@ -61,8 +61,8 @@ author: https://github.com/LitBomb - [6. Troubleshooting](#6-troubleshooting) - [6.1. Q: My client says another client or a repeater or a room server was last seen many, many days ago.](#61-q-my-client-says-another-client-or-a-repeater-or-a-room-server-was-last-seen-many-many-days-ago) - [6.2. Q: A repeater or a client or a room server I expect to see on my discover list (on T-Deck) or contact list (on a smart device client) are not listed.](#62-q-a-repeater-or-a-client-or-a-room-server-i-expect-to-see-on-my-discover-list-on-t-deck-or-contact-list-on-a-smart-device-client-are-not-listed) - - [6.3. Q: How to connect to a repeater via BLE (bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth) - - [6.4. Q: I can't connect via bluetooth, what is the bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code) + - [6.3. Q: How to connect to a repeater via BLE (Bluetooth)?](#63-q-how-to-connect-to-a-repeater-via-ble-bluetooth) + - [6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code?](#64-q-i-cant-connect-via-bluetooth-what-is-the-bluetooth-pairing-code) - [6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection.](#65-q-my-heltec-v3-keeps-disconnecting-from-my-smartphone--it-cant-hold-a-solid-bluetooth-connection) - [7. Other Questions:](#7-other-questions) - [7.1. Q: How to Update repeater and room server firmware over the air?](#71-q-how-to--update-repeater-and-room-server-firmware-over-the-air) @@ -89,7 +89,7 @@ Anyone is able to build anything they like on top of MeshCore without paying any Main web site: [https://meshcore.co.uk/](https://meshcore.co.uk/) Firmware Flasher: https://flasher.meshcore.co.uk/ Phone Client Applications: https://meshcore.co.uk/apps.html - MeshCore Fimrware Github: https://github.com/ripplebiz/MeshCore + MeshCore Fimrware GitHub: https://github.com/ripplebiz/MeshCore NOTE: Andy Kirby has a very useful [intro video](https://www.youtube.com/watch?v=t1qne8uJBAc) for beginners. @@ -119,16 +119,16 @@ Companion radios are for connecting to the Android app or web app as a messenger #### 1.2.4. Repeater Repeaters are used to extend the range of a MeshCore network. Repeater firmware runs on the same devices that run client firmware. A repeater's job is to forward MeshCore packets to the destination device. It does **not** forward or retransmit every packet it receives, unlike other LoRa mesh systems. -A repeater can be remotely administered using a T-Deck running the MeshCore firwmware with remote admistration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app. +A repeater can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app. #### 1.2.5. Room Server A room server is a simple BBS server for sharing posts. T-Deck devices running MeshCore firmware or a BLE Companion client connected to a smartphone running the MeshCore app can connect to a room server. -room servers store message history on them, and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. Contrast to channels, messages are either received when it's sent, or not received and missed if the a room user is out of range. You can think of room servers like email servers where you can come back later and get your emails from your mail server +Room servers store message history on them and push the stored messages to users. Room servers allow roaming users to come back later and retrieve message history. With channels, messages are either received when it's sent, or not received and missed if the channel user is out of range. Room servers are different and more like email servers where you can come back later and get your emails from your mail server. -A room server can be remotely administered using a T-Deck running the MeshCore firwmware with remote admistration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app. +A room server can be remotely administered using a T-Deck running the MeshCore firmware with remote administration features unlocked, or from a BLE Companion client connected to a smartphone running the MeshCore app. -When a client logs into a room server, the client will receive the previously 16 unseen messages. +When a client logs into a room server, the client will receive the previously 32 unseen messages. A room server can also take on the repeater role. To enable repeater role on a room server, use this command: @@ -138,26 +138,26 @@ A room server can also take on the repeater role. To enable repeater role on a ## 2. Initial Setup -### 2.1. Q: How many devices do I need to start using meshcore? -**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android client via bluetooth (iOS client will be available later). You can start communiating with other MeshCore users near you. +### 2.1. Q: How many devices do I need to start using MeshCore? +**A:** If you have one supported device, flash the BLE Companion firmware and use your device as a client. You can connect to the device using the Android client via Bluetooth (iOS client will be available later). You can start communicating with other MeshCore users near you. -If you have two supported devices, and there are not many MeshCore users near you, flash both of them to BLE Companion firmware so you can use your devices to communiate with your near-by friends and family. +If you have two supported devices, and there are not many MeshCore users near you, flash both to BLE Companion firmware so you can use your devices to communicate with your near-by friends and family. -If you have two supported devices, and there are other MeshcCore users near by, you can flash one of your devices with BLE Companion firmware, and flash another supported device to repeater firmware. Place the repeater high above ground o extend your MeshCore network's reach. +If you have two supported devices, and there are other MeshcCore users nearby, you can flash one of your devices with BLE Companion firmware and flash another supported device to repeater firmware. Place the repeater high above ground to extend your MeshCore network's reach. -After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the rpeater or room server over RF: +After you flashed the latest firmware onto your repeater device, keep the device connected to your computer via USB serial, use the console feature on the web flasher and set the frequency for your region or country, so your client can remote administer the repeater or room server over RF: `set freq {frequency}` The repeater and room server CLI reference is here: https://github.com/ripplebiz/MeshCore/wiki/Repeater-&-Room-Server-CLI-Reference -If you have more supported devices, you can use your additional deivces with the room server firmware. +If you have more supported devices, you can use your additional devices with the room server firmware. ### 2.2. Q: Does MeshCore cost any money? **A:** All radio firmware versions (e.g. for Heltec V3, RAK, T-1000E, etc) are free and open source developed by Scott at Ripple Radios. -The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at [meshtastic.liamcottle.net](https://meshtastic.liamcottle.net) on [github ](https://github.com/liamcottle/meshtastic-map)and [reticulum-meshchat on github](https://github.com/liamcottle/reticulum-meshchat). +The native Android and iOS client uses the freemium model and is developed by Liam Cottle, developer of meshtastic map at [meshtastic.liamcottle.net](https://meshtastic.liamcottle.net) on [GitHub](https://github.com/liamcottle/meshtastic-map) and [reticulum-meshchat on github](https://github.com/liamcottle/reticulum-meshchat). The T-Deck firmware is free to download and most features are available without cost. To support the firmware developer, you can pay for a registration key to unlock your T-Deck for deeper map zoom and remote server administration over RF using the T-Deck. You do not need to pay for the registration to use your T-Deck for direct messaging and connecting to repeaters and room servers. @@ -174,18 +174,20 @@ In UK and EU, 867.5MHz is not allowed to use 250kHz bandwidth and it only allows [Source]([https://](https://discord.com/channels/826570251612323860/1330643963501351004/1356540643853209641)) the rest of the radio settings are the same for all frequencies: -- Spread Factor (SF): 10 +- Spread Factor (SF): 11 - Coding Rate (CR): 5 - Bandwidth (BW): 250.00 +(Originally MeshCore started with SF 10. recently (as of late April 2025) the community has advocated SF 11 also a viable option for longer range but a little slower transmission. Currently there are MeshCore meshes with SF 10 and SF 11. Liam Cottle's smartphone app's presets now recommend SF 10 for Australia and SF 11 for all other regions and countries. EU and UK has SF 10 and SF 11 presets. Work with your local meshers on deciding with SF number is best for your use cases. In the future, there may be bridge nodes that can bridge SF 10 and SF 11 (or even different frequencies) traffic.) + ### 2.4. Q: What is an "advert" in MeshCore? **A:** -Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending it's node info. +Advert means to advertise yourself on the network. In Reticulum terms it would be to announce. In Meshtastic terms it would be the node sending its node info. MeshCore allows you to manually broadcast your name, position and public encryption key, which is also signed to prevent spoofing. When you click the advert button, it broadcasts that data over LoRa. MeshCore calls that an Advert. There's two ways to advert, "zero hop" and "flood". * Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it. -* Flooded means it's broadcasted out, and then repeated by all the repeaters that hear it. +* Flooded means it's broadcasted out and then repeated by all the repeaters that hear it. MeshCore clients only advertise themselves when the user initiates it. A repeater (and room server?) advertises its presence once every 240 minutes. This interval can be configured using the following command: @@ -257,9 +259,9 @@ You can get the latitude and longitude from Google Maps by right-clicking the lo 8. At this point you can begin flashing using ### 4.2. Q: Why is my T-Deck Plus not getting any satellite lock? -**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, a number of T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open up the device to check the GPS orientation. +**A:** For T-Deck Plus, the GPS baud rate should be set to **38400**. Also, some T-Deck Plus devices were found to have the GPS module installed upside down, with the GPS antenna facing down instead of up. If your T-Deck Plus still doesn't get any satellite lock after setting the baud rate to 38400, you might need to open the device to check the GPS orientation. -GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-Deck will continue to try to get a GPS lock. You can go to the `GPS Info` screen, you should see the `Sentences:` coutner increasing if the baud rate is correct. +GPS on T-Deck is always enabled. You can skip the "GPS clock sync" and the T-Deck will continue to try to get a GPS lock. You can go to the `GPS Info` screen; you should see the `Sentences:` counter increasing if the baud rate is correct. [Source]([https://](https://discord.com/channels/826570251612323860/1330643963501351004/1356609240302616689)) @@ -292,7 +294,7 @@ Unlock page: ### 4.8. Q: How to decipher the diagnostics screen on T-Deck? -**A: ** Space is tight on T-Deck's screen so the information is a bit cryptic. Format is : +**A: ** Space is tight on T-Deck's screen, so the information is a bit cryptic. The format is : `{hops} l:{packet-length}({payload-len}) t:{packet-type} snr:{n} rssi:{n}` See here for packet-type: [https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19](https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19 "https://github.com/ripplebiz/MeshCore/blob/main/src/Packet.h#L19") @@ -335,12 +337,12 @@ Making the bandwidth 2x wider (from BW125 to BW250) allows you to send 2x more b Lowering the spreading factor makes it more difficult for the gateway to receive a transmission, as it will be more sensitive to noise. You could compare this to two people taking in a noisy place (a bar for example). If you’re far from each other, you have to talk slow (SF10), but if you’re close, you can talk faster (SF7) -So it's balancing act between speed of the transmission and resistance to noise. +So, it's balancing act between speed of the transmission and resistance to noise. things network is mainly focused on LoRaWAN, but the LoRa low-level stuff still checks out for any LoRa project ### 5.2. Q: Do MeshCore clients repeat? -**A:** No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the air ware and create endless collisions so messages sent aren't received. -In MeshCore, only repeaters and room server with '`set repeat on` repeat. +**A:** No, MeshCore clients do not repeat. This is the core of MeshCore's messaging-first design. This is to avoid devices flooding the air ware and create endless collisions, so messages sent aren't received. +In MeshCore, only repeaters and room server with `set repeat on` repeat. ### 5.3. Q: What happens when a node learns a route via a mobile repeater, and that repeater is gone? @@ -350,13 +352,13 @@ In the case if users are moving around frequently, and the paths are breaking, t ### 5.4. Q: How does a node discovery a path to its destination and then use it to send messages in the future, instead of flooding every message it sends like Meshtastic? -Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing, When your destination node gets the message, it sends back to the sender a delivery report with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. when you send the next message, the path will get embedded into the packet and be evaluated by repeaters. if the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization. +Routes are stored in sender's contact list. When you send a message the first time, the message first gets to your destination by flood routing. When your destination node gets the message, it will send back a delivery report to the sender with all repeaters that the original message went through. This delivery report is flood-routed back to you the sender and is a basis for future direct path. When you send the next message, the path will get embedded into the packet and be evaluated by repeaters. If the hop and address of the repeater matches, it will retransmit the message, otherwise it will not retransmit, hence minimizing utilization. [Source](https://discord.com/channels/826570251612323860/1330643963501351004/1351279141630119996) ### 5.5. Q: Do public channels always flood? Do private channels always flood? -**A:** Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the `set flood.max` CLI command. Admistrators of repeaters get to set the rules of their repeaters. +**A:** Yes, group channels are A to B, so there is no defined path. They have to flood. Repeaters can however deny flood traffic up to some hop limit, with the `set flood.max` CLI command. Administrators of repeaters get to set the rules of their repeaters. [Source](https://discord.com/channels/1343693475589263471/1343693475589263474/1350023009527664672) @@ -488,7 +490,7 @@ adafruit-nrfutil --verbose dfu serial --package t1000_e_bootloader-0.9.1-5-g4887 **A:** Yes. See the following: #### 5.14.1. meshcoremqtt -A python based script to send meshore debug and packet capture data to MQTT for analysis +A Python script to send meshore debug and packet capture data to MQTT for analysis https://github.com/Andrew-a-g/meshcoretomqtt #### 5.14.2. MeshCore for Home Assistant @@ -504,7 +506,7 @@ CLI interface to MeshCore companion radio over BLE, TCP, or serial. Uses Pyton https://github.com/fdlamotte/meshcore-cli #### 5.14.5. meshcore.js -A Javascript library for interacting with a MeshCore device running the companion radio firmware +A JavaScript library for interacting with a MeshCore device running the companion radio firmware https://github.com/liamcottle/meshcore.js --- @@ -519,23 +521,23 @@ https://github.com/liamcottle/meshcore.js You can get the epoch time on and use it to set your T-Deck clock. For a repeater and room server, the admin can use a T-Deck to remotely set their clock (clock sync), or use the `time` command in the USB serial console with the server device connected. -### 6.3. Q: How to connect to a repeater via BLE (bluetooth)? -**A:** You can't connect to a device running repeater firmware via bluetooth. Devices running the BLE companion firmware you can connect to it via bluetooth using the android app +### 6.3. Q: How to connect to a repeater via BLE (Bluetooth)? +**A:** You can't connect to a device running repeater firmware via Bluetooth. Devices running the BLE companion firmware you can connect to it via Bluetooth using the android app -### 6.4. Q: I can't connect via bluetooth, what is the bluetooth pairing code? +### 6.4. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code? -**A:** the default bluetooth pairing code is `123456` +**A:** the default Bluetooth pairing code is `123456` ### 6.5. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. -**A:** Heltec V3 has a very small coil antenna on its PCB for WiFi and Bluetooth connectivty. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification. +**A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification. --- ## 7. Other Questions: ### 7.1. Q: How to Update repeater and room server firmware over the air? **A:** Only nRF-based RAK4631 and Heltec T114 OTA firmware update are verified using nRF smartphone app. Lilygo T-Echo doesn't work currently. -You can update repeater and room server firmware with a bluetooth connection between your smartphone and your LoRa radio using the nRF app. +You can update repeater and room server firmware with a Bluetooth connection between your smartphone and your LoRa radio using the nRF app. 1. Download the ZIP file for the specific node from the web flasher to your smartphone 2. On the phone client, log on to the repeater as administrator (default password is `password`) to issue the `start ota`command to the repeater or room server to get the device into OTA DFU mode diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h new file mode 100644 index 00000000..09d04266 --- /dev/null +++ b/examples/companion_radio/NodePrefs.h @@ -0,0 +1,27 @@ +#ifndef NODE_PREFS_H +#define NODE_PREFS_H + +#include // For uint8_t, uint32_t + +#define TELEM_MODE_DENY 0 +#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags +#define TELEM_MODE_ALLOW_ALL 2 + +struct NodePrefs { // persisted to file + float airtime_factor; + char node_name[32]; + float freq; + uint8_t sf; + uint8_t cr; + uint8_t reserved1; + uint8_t manual_add_contacts; + float bw; + uint8_t tx_power_dbm; + uint8_t telemetry_mode_base; + uint8_t telemetry_mode_loc; + uint8_t telemetry_mode_env; + float rx_delay_base; + uint32_t ble_pin; +}; + +#endif // NODE_PREFS_H \ No newline at end of file diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp index 7448f303..f4e3fb0b 100644 --- a/examples/companion_radio/UITask.cpp +++ b/examples/companion_radio/UITask.cpp @@ -1,8 +1,16 @@ #include "UITask.h" #include #include +#include "NodePrefs.h" -#define AUTO_OFF_MILLIS 15000 // 15 seconds +#define AUTO_OFF_MILLIS 15000 // 15 seconds +#define BOOT_SCREEN_MILLIS 4000 // 4 seconds + +#ifdef PIN_STATUS_LED +#define LED_ON_MILLIS 20 +#define LED_ON_MSG_MILLIS 200 +#define LED_CYCLE_MILLIS 4000 +#endif #ifndef USER_BTN_PRESSED #define USER_BTN_PRESSED LOW @@ -25,11 +33,11 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(DisplayDriver* display, const char* node_name, const char* build_date, const char* firmware_version, uint32_t pin_code) { +void UITask::begin(DisplayDriver* display, NodePrefs* node_prefs, const char* build_date, const char* firmware_version, uint32_t pin_code) { _display = display; _auto_off = millis() + AUTO_OFF_MILLIS; clearMsgPreview(); - _node_name = node_name; + _node_prefs = node_prefs; _pin_code = pin_code; if (_display != NULL) { _display->turnOn(); @@ -43,12 +51,33 @@ void UITask::begin(DisplayDriver* display, const char* node_name, const char* bu *dash = 0; } - #ifdef PIN_USER_BTN - pinMode(PIN_USER_BTN, INPUT); - #endif - // v1.2.3 (1 Jan 2025) sprintf(_version_info, "%s (%s)", version, build_date); + +#ifdef PIN_BUZZER + buzzer.begin(); +#endif +} + +void UITask::soundBuzzer(UIEventType bet) { +#if defined(PIN_BUZZER) +switch(bet){ + case UIEventType::contactMessage: + // gemini's pick + buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); + break; + case UIEventType::channelMessage: + buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); + break; + case UIEventType::roomMessage: + case UIEventType::newContactMessage: + case UIEventType::none: + default: + break; +} +#endif +// Serial.print("DBG: Buzzzzzz -> "); +// Serial.println((int) bet); } void UITask::msgRead(int msgcount) { @@ -81,16 +110,42 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i } } +void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { + // Convert millivolts to percentage + const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V) + const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V) + int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); + if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% + if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% + + // battery icon + int iconWidth = 24; + int iconHeight = 12; + int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner + int iconY = 0; + _display->setColor(DisplayDriver::GREEN); + + // battery outline + _display->drawRect(iconX, iconY, iconWidth, iconHeight); + + // battery "cap" + _display->fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); + + // fill the battery based on the percentage + int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; + _display->fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); +} + void UITask::renderCurrScreen() { if (_display == NULL) return; // assert() ?? char tmp[80]; - if (_origin[0] && _msg[0]) { + if (_origin[0] && _msg[0]) { // message preview // render message preview _display->setCursor(0, 0); _display->setTextSize(1); _display->setColor(DisplayDriver::GREEN); - _display->print(_node_name); + _display->print(_node_prefs->node_name); _display->setCursor(0, 12); _display->setColor(DisplayDriver::YELLOW); @@ -104,28 +159,50 @@ void UITask::renderCurrScreen() { _display->setColor(DisplayDriver::ORANGE); sprintf(tmp, "%d", _msgcount); _display->print(tmp); - } else { - // render 'home' screen + _display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114 + } else if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + // meshcore logo _display->setColor(DisplayDriver::BLUE); - _display->drawXbm(0, 0, meshcore_logo, 128, 13); - _display->setCursor(0, 20); - _display->setTextSize(1); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); + // version info _display->setColor(DisplayDriver::LIGHT); - _display->print(_node_name); - - _display->setCursor(0, 32); + _display->setTextSize(1); + uint16_t textWidth = _display->getTextWidth(_version_info); + _display->setCursor((_display->width() - textWidth) / 2, 22); _display->print(_version_info); + } else { // home screen + // node name + _display->setCursor(0, 0); + _display->setTextSize(1); + _display->setColor(DisplayDriver::GREEN); + _display->print(_node_prefs->node_name); - if (_connected) { - //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); - //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); - } else if (_pin_code != 0) { + // battery voltage + renderBatteryIndicator(_board->getBattMilliVolts()); + + // freq / sf + _display->setCursor(0, 20); + _display->setColor(DisplayDriver::YELLOW); + sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); + _display->print(tmp); + + // bw / cr + _display->setCursor(0, 30); + sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); + _display->print(tmp); + + // BT pin + if (!_connected && _pin_code != 0) { _display->setColor(DisplayDriver::RED); _display->setTextSize(2); _display->setCursor(0, 43); sprintf(tmp, "Pin:%d", _pin_code); _display->print(tmp); + _display->setColor(DisplayDriver::GREEN); + } else { + _display->setColor(DisplayDriver::LIGHT); } } _need_refresh = false; @@ -135,22 +212,21 @@ void UITask::userLedHandler() { #ifdef PIN_STATUS_LED static int state = 0; static int next_change = 0; + static int last_increment = 0; + int cur_time = millis(); if (cur_time > next_change) { if (state == 0) { - state = 1; // led on, short = unread msg + state = 1; if (_msgcount > 0) { - next_change = cur_time + 500; + last_increment = LED_ON_MSG_MILLIS; } else { - next_change = cur_time + 2000; + last_increment = LED_ON_MILLIS; } + next_change = cur_time + last_increment; } else { state = 0; - if (_board->getBattMilliVolts() > 3800) { - next_change = cur_time + 2000; - } else { - next_change = cur_time + 4000; // 4s blank if bat level low - } + next_change = cur_time + LED_CYCLE_MILLIS - last_increment; } digitalWrite(PIN_STATUS_LED, state); } @@ -158,46 +234,64 @@ void UITask::userLedHandler() { } void UITask::buttonHandler() { -#ifdef PIN_USER_BTN - static int prev_btn_state = !USER_BTN_PRESSED; - static unsigned long btn_state_change_time = 0; - static unsigned long next_read = 0; - int cur_time = millis(); - if (cur_time >= next_read) { - int btn_state = digitalRead(PIN_USER_BTN); - if (btn_state != prev_btn_state) { - if (btn_state == USER_BTN_PRESSED) { // pressed? - if (_display != NULL) { - if (_display->isOn()) { - clearMsgPreview(); - } else { - _display->turnOn(); - _need_refresh = true; + #if defined(PIN_USER_BTN) || defined(PIN_USER_BTN_ANA) + static int prev_btn_state = !USER_BTN_PRESSED; + static int prev_btn_state_ana = !USER_BTN_PRESSED; + static unsigned long btn_state_change_time = 0; + static unsigned long next_read = 0; + int cur_time = millis(); + if (cur_time >= next_read) { + int btn_state = 0; + int btn_state_ana = 0; + #ifdef PIN_USER_BTN + btn_state = digitalRead(PIN_USER_BTN); + #endif + #ifdef PIN_USER_BTN_ANA + btn_state_ana = (analogRead(PIN_USER_BTN_ANA) < 20); // analogRead returns a value hopefully below 20 when button is pressed. + #endif + if (btn_state != prev_btn_state || btn_state_ana != prev_btn_state_ana) { // check for either digital or analogue button change of state + if (btn_state == USER_BTN_PRESSED || btn_state_ana == USER_BTN_PRESSED) { // pressed? + if (_display != NULL) { + if (_display->isOn()) { + clearMsgPreview(); + } else { + _display->turnOn(); + _need_refresh = true; + } + _auto_off = cur_time + AUTO_OFF_MILLIS; // extend auto-off timer + } + } else { // unpressed ? check pressed time ... + if ((cur_time - btn_state_change_time) > 5000) { + #ifdef PIN_STATUS_LED + digitalWrite(PIN_STATUS_LED, LOW); + delay(10); + #endif + _board->powerOff(); } - _auto_off = cur_time + AUTO_OFF_MILLIS; // extend auto-off timer - } - } else { // unpressed ? check pressed time ... - if ((cur_time - btn_state_change_time) > 5000) { - #ifdef PIN_STATUS_LED - digitalWrite(PIN_STATUS_LED, LOW); - delay(10); - #endif - _board->powerOff(); } + btn_state_change_time = millis(); + prev_btn_state = btn_state; + prev_btn_state_ana = btn_state_ana; } - btn_state_change_time = millis(); - prev_btn_state = btn_state; + next_read = millis() + 100; // 10 reads per second } - next_read = millis() + 100; // 10 reads per second + #endif } -#endif -} void UITask::loop() { buttonHandler(); userLedHandler(); +#ifdef PIN_BUZZER + if (buzzer.isPlaying()) buzzer.loop(); +#endif + if (_display != NULL && _display->isOn()) { + static bool _firstBoot = true; + if(_firstBoot && millis() >= BOOT_SCREEN_MILLIS) { + _need_refresh = true; + _firstBoot = false; + } if (millis() >= _next_refresh && _need_refresh) { _display->startFrame(); renderCurrScreen(); diff --git a/examples/companion_radio/UITask.h b/examples/companion_radio/UITask.h index 9050bc42..134b5a16 100644 --- a/examples/companion_radio/UITask.h +++ b/examples/companion_radio/UITask.h @@ -4,13 +4,31 @@ #include #include +#ifdef PIN_BUZZER + #include +#endif + +#include "NodePrefs.h" + + enum class UIEventType +{ + none, + contactMessage, + channelMessage, + roomMessage, + newContactMessage +}; + class UITask { DisplayDriver* _display; mesh::MainBoard* _board; +#ifdef PIN_BUZZER + genericBuzzer buzzer; +#endif unsigned long _next_refresh, _auto_off; bool _connected; uint32_t _pin_code; - const char* _node_name; + NodePrefs* _node_prefs; char _version_info[32]; char _origin[62]; char _msg[80]; @@ -20,19 +38,22 @@ class UITask { void renderCurrScreen(); void buttonHandler(); void userLedHandler(); + void renderBatteryIndicator(uint16_t batteryMilliVolts); + public: UITask(mesh::MainBoard* board) : _board(board), _display(NULL) { _next_refresh = 0; _connected = false; } - void begin(DisplayDriver* display, const char* node_name, const char* build_date, const char* firmware_version, uint32_t pin_code); + void begin(DisplayDriver* display, NodePrefs* node_prefs, const char* build_date, const char* firmware_version, uint32_t pin_code); void setHasConnection(bool connected) { _connected = connected; } bool hasDisplay() const { return _display != NULL; } void clearMsgPreview(); void msgRead(int msgcount); void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount); + void soundBuzzer(UIEventType bet = UIEventType::none); void loop(); }; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 88652697..13db88c0 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1,8 +1,10 @@ #include // needed for PlatformIO #include -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include +#elif defined(RP2040_PLATFORM) + #include #elif defined(ESP32) #include #endif @@ -12,6 +14,7 @@ #include #include #include +#include "NodePrefs.h" #include #include @@ -59,19 +62,6 @@ #ifdef DISPLAY_CLASS #include "UITask.h" - #ifdef ST7789 - #include - #elif defined(HAS_GxEPD) - #include - #else - #include - #endif - static DISPLAY_CLASS display; - #define HAS_UI -#endif - -#if defined(HAS_UI) - #include "UITask.h" static UITask ui_task(&board); #endif @@ -88,14 +78,14 @@ static uint32_t _atoi(const char* sp) { /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 4 +#define FIRMWARE_VER_CODE 5 #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "21 Apr 2025" + #define FIRMWARE_BUILD_DATE "24 May 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.5.1" + #define FIRMWARE_VERSION "v1.6.2" #endif #define CMD_APP_START 1 @@ -136,6 +126,9 @@ static uint32_t _atoi(const char* sp) { #define CMD_SEND_TRACE_PATH 36 #define CMD_SET_DEVICE_PIN 37 #define CMD_SET_OTHER_PARAMS 38 +#define CMD_SEND_TELEMETRY_REQ 39 +#define CMD_GET_CUSTOM_VARS 40 +#define CMD_SET_CUSTOM_VAR 41 #define RESP_CODE_OK 0 #define RESP_CODE_ERR 1 @@ -158,6 +151,7 @@ static uint32_t _atoi(const char* sp) { #define RESP_CODE_CHANNEL_INFO 18 // a reply to CMD_GET_CHANNEL #define RESP_CODE_SIGN_START 19 #define RESP_CODE_SIGNATURE 20 +#define RESP_CODE_CUSTOM_VARS 21 // these are _pushed_ to client app at any time #define PUSH_CODE_ADVERT 0x80 @@ -171,6 +165,7 @@ static uint32_t _atoi(const char* sp) { #define PUSH_CODE_LOG_RX_DATA 0x88 #define PUSH_CODE_TRACE_DATA 0x89 #define PUSH_CODE_NEW_ADVERT 0x8A +#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 @@ -181,23 +176,11 @@ static uint32_t _atoi(const char* sp) { /* -------------------------------------------------------------------------------------- */ -#define MAX_SIGN_DATA_LEN (8*1024) // 8K +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 -struct NodePrefs { // persisted to file - float airtime_factor; - char node_name[32]; - double node_lat, node_lon; - float freq; - uint8_t sf; - uint8_t cr; - uint8_t reserved1; - uint8_t manual_add_contacts; - float bw; - uint8_t tx_power_dbm; - uint8_t unused[3]; - float rx_delay_base; - uint32_t ble_pin; -}; +#define MAX_SIGN_DATA_LEN (8*1024) // 8K class MyMesh : public BaseChatMesh { FILESYSTEM* _fs; @@ -205,6 +188,7 @@ class MyMesh : public BaseChatMesh { NodePrefs _prefs; uint32_t pending_login; uint32_t pending_status; + uint32_t pending_telemetry; BaseSerialInterface* _serial; ContactsIterator _iter; uint32_t _iter_filter_since; @@ -216,6 +200,7 @@ class MyMesh : public BaseChatMesh { uint32_t sign_data_len; uint8_t cmd_frame[MAX_FRAME_SIZE+1]; uint8_t out_frame[MAX_FRAME_SIZE+1]; + CayenneLPP telemetry; struct Frame { uint8_t len; @@ -249,7 +234,11 @@ class MyMesh : public BaseChatMesh { void loadContacts() { if (_fs->exists("/contacts3")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/contacts3", "r"); + #else File file = _fs->open("/contacts3"); + #endif if (file) { bool full = false; while (!full) { @@ -281,9 +270,11 @@ class MyMesh : public BaseChatMesh { } void saveContacts() { -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove("/contacts3"); File file = _fs->open("/contacts3", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = _fs->open("/contacts3", "w"); #else File file = _fs->open("/contacts3", "w", true); #endif @@ -314,7 +305,11 @@ class MyMesh : public BaseChatMesh { void loadChannels() { if (_fs->exists("/channels2")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/channels2", "r"); + #else File file = _fs->open("/channels2"); + #endif if (file) { bool full = false; uint8_t channel_idx = 0; @@ -340,9 +335,11 @@ class MyMesh : public BaseChatMesh { } void saveChannels() { - #if defined(NRF52_PLATFORM) + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove("/channels2"); File file = _fs->open("/channels2", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } + #elif defined(RP2040_PLATFORM) + File file = _fs->open("/channels2", "w"); #else File file = _fs->open("/channels2", "w", true); #endif @@ -373,7 +370,11 @@ class MyMesh : public BaseChatMesh { sprintf(path, "/bl/%s", fname); if (_fs->exists(path)) { + #if defined(RP2040_PLATFORM) + File f = _fs->open(path, "r"); + #else File f = _fs->open(path); + #endif if (f) { int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!! f.close(); @@ -391,9 +392,11 @@ class MyMesh : public BaseChatMesh { mesh::Utils::toHex(fname, key, key_len); sprintf(path, "/bl/%s", fname); - #if defined(NRF52_PLATFORM) + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(path); File f = _fs->open(path, FILE_O_WRITE); - if (f) { f.seek(0); f.truncate(); } + #elif defined(RP2040_PLATFORM) + File f = _fs->open(path, "w"); #else File f = _fs->open(path, "w", true); #endif @@ -480,10 +483,6 @@ class MyMesh : public BaseChatMesh { return 0; // queue is empty } - void soundBuzzer() { - // TODO - } - protected: float getAirtimeBudgetFactor() const override { return _prefs.airtime_factor; @@ -520,7 +519,9 @@ protected: _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); } } else { - soundBuzzer(); + #ifdef DISPLAY_CLASS + ui_task.soundBuzzer(UIEventType::newContactMessage); + #endif } saveContacts(); @@ -581,9 +582,11 @@ protected: frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } else { - soundBuzzer(); + #ifdef DISPLAY_CLASS + ui_task.soundBuzzer(UIEventType::contactMessage); + #endif } - #ifdef HAS_UI + #ifdef DISPLAY_CLASS ui_task.newMsg(path_len, from.name, text, offline_queue_len); #endif } @@ -632,16 +635,57 @@ protected: frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } else { - soundBuzzer(); + #ifdef DISPLAY_CLASS + ui_task.soundBuzzer(UIEventType::channelMessage); + #endif } - #ifdef HAS_UI + #ifdef DISPLAY_CLASS ui_task.newMsg(path_len, "Public", text, offline_queue_len); #endif } + uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) override { + if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { + uint8_t permissions = 0; + uint8_t cp = contact.flags >> 1; // LSB used as 'favourite' bit (so only use upper bits) + + if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_ALL) { + permissions = TELEM_PERM_BASE; + } else if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_FLAGS) { + permissions = cp & TELEM_PERM_BASE; + } + + if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_LOCATION; + } else if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_LOCATION; + } + + if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_ALL) { + permissions |= TELEM_PERM_ENVIRONMENT; + } else if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_FLAGS) { + permissions |= cp & TELEM_PERM_ENVIRONMENT; + } + + if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(permissions, telemetry); + + memcpy(reply, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply[4], telemetry.getBuffer(), tlen); + return 4 + tlen; + } + } + return 0; // unknown + } + void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) override { - uint32_t sender_timestamp; - memcpy(&sender_timestamp, data, 4); + uint32_t tag; + memcpy(&tag, data, 4); if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response // yes, is response to pending sendLogin() @@ -664,8 +708,10 @@ protected: } memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix _serial->writeFrame(out_frame, i); - } else if (len > 4 && pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0) { // check for status response - // yes, is response to pending sendStatusRequest() + } else if (len > 4 && // check for status response + pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme + // FUTURE: tag == pending_status + ) { pending_status = 0; int i = 0; @@ -674,6 +720,15 @@ protected: memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + } else if (len > 4 && tag == pending_telemetry) { // check for telemetry response + pending_telemetry = 0; + + int i = 0; + out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix + memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); + _serial->writeFrame(out_frame, i); } } @@ -729,13 +784,14 @@ protected: public: MyMesh(mesh::Radio& radio, mesh::RNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables) - : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), _serial(NULL) + : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), _serial(NULL), + telemetry(MAX_PACKET_PAYLOAD - 4) { _iter_started = false; offline_queue_len = 0; app_target_ver = 0; _identity_store = NULL; - pending_login = pending_status = 0; + pending_login = pending_status = pending_telemetry = 0; next_ack_idx = 0; sign_data = NULL; @@ -752,15 +808,19 @@ public: } void loadPrefsInt(const char* filename) { +#if defined(RP2040_PLATFORM) + File file = _fs->open(filename, "r"); +#else File file = _fs->open(filename); +#endif if (file) { uint8_t pad[8]; file.read((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0 file.read((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4 file.read(pad, 4); // 36 - file.read((uint8_t *) &_prefs.node_lat, sizeof(_prefs.node_lat)); // 40 - file.read((uint8_t *) &_prefs.node_lon, sizeof(_prefs.node_lon)); // 48 + file.read((uint8_t *) &sensors.node_lat, sizeof(sensors.node_lat)); // 40 + file.read((uint8_t *) &sensors.node_lon, sizeof(sensors.node_lon)); // 48 file.read((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56 file.read((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60 file.read((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61 @@ -768,7 +828,9 @@ public: file.read((uint8_t *) &_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.read((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64 file.read((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 - file.read((uint8_t *) _prefs.unused, sizeof(_prefs.unused)); // 69 + file.read((uint8_t *) &_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + file.read((uint8_t *) &_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + file.read((uint8_t *) &_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 file.read((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 file.read(pad, 4); // 76 file.read((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 @@ -791,14 +853,27 @@ public: BaseChatMesh::begin(); - #if defined(NRF52_PLATFORM) + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) _identity_store = new IdentityStore(fs, ""); + #elif defined(RP2040_PLATFORM) + _identity_store = new IdentityStore(fs, "/identity"); + _identity_store->begin(); #else _identity_store = new IdentityStore(fs, "/identity"); #endif loadMainIdentity(); + // use hex of first 4 bytes of identity public key as default node name + char pub_key_hex[10]; + mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4); + strcpy(_prefs.node_name, pub_key_hex); + + // if name is provided as a build flag, use that as default node name instead + #ifdef ADVERT_NAME + strcpy(_prefs.node_name, ADVERT_NAME); + #endif + // load persisted prefs if (_fs->exists("/new_prefs")) { loadPrefsInt("/new_prefs"); // new filename @@ -810,7 +885,7 @@ public: #ifdef BLE_PIN_CODE if (_prefs.ble_pin == 0) { - #ifdef HAS_UI + #ifdef DISPLAY_CLASS if (has_display) { StdRNG rng; _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session @@ -839,6 +914,9 @@ public: } const char* getNodeName() { return _prefs.node_name; } + NodePrefs* getNodePrefs() { + return &_prefs; + } uint32_t getBLEPin() { return _active_ble_pin; } void startInterface(BaseSerialInterface& serial) { @@ -847,9 +925,11 @@ public: } void savePrefs() { -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove("/new_prefs"); File file = _fs->open("/new_prefs", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = _fs->open("/new_prefs", "w"); #else File file = _fs->open("/new_prefs", "w", true); #endif @@ -860,8 +940,8 @@ public: file.write((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0 file.write((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4 file.write(pad, 4); // 36 - file.write((uint8_t *) &_prefs.node_lat, sizeof(_prefs.node_lat)); // 40 - file.write((uint8_t *) &_prefs.node_lon, sizeof(_prefs.node_lon)); // 48 + file.write((uint8_t *) &sensors.node_lat, sizeof(sensors.node_lat)); // 40 + file.write((uint8_t *) &sensors.node_lon, sizeof(sensors.node_lon)); // 48 file.write((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56 file.write((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60 file.write((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61 @@ -869,7 +949,9 @@ public: file.write((uint8_t *) &_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.write((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64 file.write((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 - file.write((uint8_t *) _prefs.unused, sizeof(_prefs.unused)); // 69 + file.write((uint8_t *) &_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + file.write((uint8_t *) &_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + file.write((uint8_t *) &_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 file.write((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 file.write(pad, 4); // 76 file.write((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 @@ -908,13 +990,13 @@ public: memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; int32_t lat, lon; - lat = (_prefs.node_lat * 1000000.0); - lon = (_prefs.node_lon * 1000000.0); + lat = (sensors.node_lat * 1000000.0); + lon = (sensors.node_lon * 1000000.0); memcpy(&out_frame[i], &lat, 4); i += 4; memcpy(&out_frame[i], &lon, 4); i += 4; out_frame[i++] = 0; // reserved out_frame[i++] = 0; // reserved - out_frame[i++] = 0; // reserved + out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) | (_prefs.telemetry_mode_base); // v5+ out_frame[i++] = _prefs.manual_add_contacts; uint32_t freq = _prefs.freq * 1000; @@ -1022,8 +1104,8 @@ public: memcpy(&alt, &cmd_frame[9], 4); // for FUTURE support } if (lat <= 90*1E6 && lat >= -90*1E6 && lon <= 180*1E6 && lon >= -180*1E6) { - _prefs.node_lat = ((double)lat) / 1000000.0; - _prefs.node_lon = ((double)lon) / 1000000.0; + sensors.node_lat = ((double)lat) / 1000000.0; + sensors.node_lon = ((double)lon) / 1000000.0; savePrefs(); writeOKFrame(); } else { @@ -1046,7 +1128,7 @@ public: writeErrFrame(ERR_CODE_ILLEGAL_ARG); } } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { - auto pkt = createSelfAdvert(_prefs.node_name, _prefs.node_lat, _prefs.node_lon); + auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); if (pkt) { if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) sendFlood(pkt); @@ -1120,7 +1202,7 @@ public: } else if (cmd_frame[0] == CMD_EXPORT_CONTACT) { if (len < 1 + PUB_KEY_SIZE) { // export SELF - auto pkt = createSelfAdvert(_prefs.node_name, _prefs.node_lat, _prefs.node_lon); + auto pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); if (pkt) { pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode @@ -1152,7 +1234,7 @@ public: int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { _serial->writeFrame(out_frame, out_len); - #ifdef HAS_UI + #ifdef DISPLAY_CLASS ui_task.msgRead(offline_queue_len); #endif } else { @@ -1203,6 +1285,11 @@ public: writeOKFrame(); } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { _prefs.manual_add_contacts = cmd_frame[1]; + if (len >= 3) { + _prefs.telemetry_mode_base = cmd_frame[2] & 0x03; // v5+ + _prefs.telemetry_mode_loc = (cmd_frame[2] >> 2) & 0x03; + _prefs.telemetry_mode_env = (cmd_frame[2] >> 4) & 0x03; + } savePrefs(); writeOKFrame(); } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { @@ -1261,7 +1348,7 @@ public: if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_status = 0; + pending_telemetry = pending_status = 0; memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; @@ -1276,16 +1363,37 @@ public: uint8_t* pub_key = &cmd_frame[1]; ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (recipient) { - uint32_t est_timeout; - int result = sendStatusRequest(*recipient, est_timeout); + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_STATUS, tag, est_timeout); if (result == MSG_SEND_FAILED) { writeErrFrame(ERR_CODE_TABLE_FULL); } else { - pending_login = 0; - memcpy(&pending_status, recipient->id.pub_key, 4); // match this to onContactResponse() + pending_telemetry = pending_login = 0; + // FUTURE: pending_status = tag; // match this in onContactResponse() + memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme out_frame[0] = RESP_CODE_SENT; out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &pending_status, 4); + memcpy(&out_frame[2], &tag, 4); + memcpy(&out_frame[6], &est_timeout, 4); + _serial->writeFrame(out_frame, 10); + } + } else { + writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found + } + } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4+PUB_KEY_SIZE) { + uint8_t* pub_key = &cmd_frame[4]; + ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); + if (result == MSG_SEND_FAILED) { + writeErrFrame(ERR_CODE_TABLE_FULL); + } else { + pending_status = pending_login = 0; + pending_telemetry = tag; // match this in onContactResponse() + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &tag, 4); memcpy(&out_frame[6], &est_timeout, 4); _serial->writeFrame(out_frame, 10); } @@ -1383,9 +1491,45 @@ public: writeErrFrame(ERR_CODE_TABLE_FULL); } } else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) { - memcpy(&_prefs.ble_pin, &cmd_frame[1], 4); - savePrefs(); - writeOKFrame(); + + // get pin from command frame + uint32_t pin; + memcpy(&pin, &cmd_frame[1], 4); + + // ensure pin is zero, or a valid 6 digit pin + if(pin == 0 || (pin >= 100000 && pin <= 999999)){ + _prefs.ble_pin = pin; + savePrefs(); + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + + } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { + out_frame[0] = RESP_CODE_CUSTOM_VARS; + char* dp = (char *) &out_frame[1]; + for (int i = 0; i < sensors.getNumSettings() && dp - (char *) &out_frame[1] < 140; i++) { + if (i > 0) { *dp++ = ','; } + strcpy(dp, sensors.getSettingName(i)); dp = strchr(dp, 0); + *dp++ = ':'; + strcpy(dp, sensors.getSettingValue(i)); dp = strchr(dp, 0); + } + _serial->writeFrame(out_frame, dp - (char *)out_frame); + } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { + cmd_frame[len] = 0; + char* sp = (char *) &cmd_frame[1]; + char* np = strchr(sp, ':'); // look for separator char + if (np) { + *np++ = 0; // modify 'cmd_frame', replace ':' with null + bool success = sensors.setSettingValue(sp, np); + if (success) { + writeOKFrame(); + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } + } else { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); @@ -1419,7 +1563,7 @@ public: checkConnections(); } - #ifdef HAS_UI + #ifdef DISPLAY_CLASS ui_task.setHasConnection(_serial->isConnected()); ui_task.loop(); #endif @@ -1444,6 +1588,24 @@ public: #include ArduinoSerialInterface serial_interface; #endif +#elif defined(RP2040_PLATFORM) + //#ifdef WIFI_SSID + // #include + // SerialWifiInterface serial_interface; + // #ifndef TCP_PORT + // #define TCP_PORT 5000 + // #endif + // #elif defined(BLE_PIN_CODE) + // #include + // SerialBLEInterface serial_interface; + #if defined(SERIAL_RX) + #include + ArduinoSerialInterface serial_interface; + HardwareSerial companion_serial(1); + #else + #include + ArduinoSerialInterface serial_interface; + #endif #elif defined(NRF52_PLATFORM) #ifdef BLE_PIN_CODE #include @@ -1452,6 +1614,9 @@ public: #include ArduinoSerialInterface serial_interface; #endif +#elif defined(STM32_PLATFORM) + #include + ArduinoSerialInterface serial_interface; #else #error "need to define a serial interface" #endif @@ -1469,26 +1634,24 @@ void setup() { board.begin(); -#ifdef HAS_UI +#ifdef DISPLAY_CLASS DisplayDriver* disp = NULL; - #ifdef DISPLAY_CLASS if (display.begin()) { disp = &display; disp->startFrame(); disp->print("Please wait..."); disp->endFrame(); } - #endif #endif if (!radio_init()) { halt(); } fast_rng.begin(radio_get_rng_seed()); -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) InternalFS.begin(); the_mesh.begin(InternalFS, - #ifdef HAS_UI + #ifdef DISPLAY_CLASS disp != NULL #else false @@ -1503,10 +1666,35 @@ void setup() { serial_interface.begin(Serial); #endif the_mesh.startInterface(serial_interface); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + the_mesh.begin(LittleFS, + #ifdef DISPLAY_CLASS + disp != NULL + #else + false + #endif + ); + + //#ifdef WIFI_SSID + // WiFi.begin(WIFI_SSID, WIFI_PWD); + // serial_interface.begin(TCP_PORT); + // #elif defined(BLE_PIN_CODE) + // char dev_name[32+16]; + // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); + // serial_interface.begin(dev_name, the_mesh.getBLEPin()); + #if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + serial_interface.begin(companion_serial); + #else + serial_interface.begin(Serial); + #endif + the_mesh.startInterface(serial_interface); #elif defined(ESP32) SPIFFS.begin(true); the_mesh.begin(SPIFFS, - #ifdef HAS_UI + #ifdef DISPLAY_CLASS disp != NULL #else false @@ -1532,11 +1720,14 @@ void setup() { #error "need to define filesystem" #endif -#ifdef HAS_UI - ui_task.begin(disp, the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION, the_mesh.getBLEPin()); + sensors.begin(); + +#ifdef DISPLAY_CLASS + ui_task.begin(disp, the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION, the_mesh.getBLEPin()); #endif } void loop() { the_mesh.loop(); + sensors.loop(); } diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 6fff675e..d096d14b 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -1,7 +1,9 @@ #include "UITask.h" #include +#include -#define AUTO_OFF_MILLIS 20000 // 20 seconds +#define AUTO_OFF_MILLIS 20000 // 20 seconds +#define BOOT_SCREEN_MILLIS 4000 // 4 seconds // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { @@ -20,10 +22,10 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(const char* node_name, const char* build_date, const char* firmware_version) { +void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; - _node_name = node_name; + _node_prefs = node_prefs; _display->turnOn(); // strip off dash and commit hash by changing dash to null terminator @@ -39,18 +41,43 @@ void UITask::begin(const char* node_name, const char* build_date, const char* fi } void UITask::renderCurrScreen() { - // render 'home' screen - _display->drawXbm(0, 0, meshcore_logo, 128, 13); - _display->setCursor(0, 20); - _display->setTextSize(1); - _display->print(_node_name); + char tmp[80]; + if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); - _display->setCursor(0, 32); - _display->print(_version_info); - _display->setCursor(0, 43); - _display->print("< Repeater >"); - //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); - //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); + // version info + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + uint16_t versionWidth = _display->getTextWidth(_version_info); + _display->setCursor((_display->width() - versionWidth) / 2, 22); + _display->print(_version_info); + + // node type + const char* node_type = "< Repeater >"; + uint16_t typeWidth = _display->getTextWidth(node_type); + _display->setCursor((_display->width() - typeWidth) / 2, 35); + _display->print(node_type); + } else { // home screen + // node name + _display->setCursor(0, 0); + _display->setTextSize(1); + _display->setColor(DisplayDriver::GREEN); + _display->print(_node_prefs->node_name); + + // freq / sf + _display->setCursor(0, 20); + _display->setColor(DisplayDriver::YELLOW); + sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); + _display->print(tmp); + + // bw / cr + _display->setCursor(0, 30); + sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); + _display->print(tmp); + } } void UITask::loop() { diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index 13005957..a27259f1 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -1,18 +1,19 @@ #pragma once #include +#include class UITask { DisplayDriver* _display; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; - const char* _node_name; + NodePrefs* _node_prefs; char _version_info[32]; void renderCurrScreen(); public: UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } - void begin(const char* node_name, const char* build_date, const char* firmware_version); + void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); }; \ No newline at end of file diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7ce05b64..12c843b7 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -1,8 +1,10 @@ #include // needed for PlatformIO #include -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include +#elif defined(RP2040_PLATFORM) + #include #elif defined(ESP32) #include #endif @@ -20,11 +22,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "21 Apr 2025" + #define FIRMWARE_BUILD_DATE "24 May 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.5.1" + #define FIRMWARE_VERSION "v1.6.2" #endif #ifndef LORA_FREQ @@ -58,10 +60,6 @@ #endif #ifdef DISPLAY_CLASS - #include - - static DISPLAY_CLASS display; - #include "UITask.h" static UITask ui_task(display); #endif @@ -72,7 +70,9 @@ /* ------------------------------ Code -------------------------------- */ -#define CMD_GET_STATUS 0x01 +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -87,7 +87,7 @@ struct RepeaterStats { uint32_t total_up_time_secs; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; - uint16_t n_full_events; + uint16_t err_events; // was 'n_full_events' int16_t last_snr; // x 4 uint16_t n_direct_dups, n_flood_dups; }; @@ -101,7 +101,16 @@ struct ClientInfo { uint8_t out_path[MAX_PATH_SIZE]; }; -#define MAX_CLIENTS 4 +#ifndef MAX_CLIENTS + #define MAX_CLIENTS 32 +#endif + +struct NeighbourInfo { + mesh::Identity id; + uint32_t advert_timestamp; + uint32_t heard_timestamp; + int8_t snr; // multiplied by 4, user should divide to get float value +}; // NOTE: need to space the ACK and the reply text apart (in CLI) #define CLI_REPLY_DELAY_MILLIS 1500 @@ -114,6 +123,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; ClientInfo known_clients[MAX_CLIENTS]; +#if MAX_NEIGHBOURS + NeighbourInfo neighbours[MAX_NEIGHBOURS]; +#endif + CayenneLPP telemetry; ClientInfo* putClient(const mesh::Identity& id) { uint32_t min_time = 0xFFFFFFFF; @@ -133,15 +146,43 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { return oldest; } - int handleRequest(ClientInfo* sender, uint8_t* payload, size_t payload_len) { - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) { + #if MAX_NEIGHBOURS // check if neighbours enabled + // find existing neighbour, else use least recently updated + uint32_t oldest_timestamp = 0xFFFFFFFF; + NeighbourInfo* neighbour = &neighbours[0]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + // if neighbour already known, we should update it + if (id.matches(neighbours[i].id)) { + neighbour = &neighbours[i]; + break; + } + + // otherwise we should update the least recently updated neighbour + if (neighbours[i].heard_timestamp < oldest_timestamp) { + neighbour = &neighbours[i]; + oldest_timestamp = neighbour->heard_timestamp; + } + } + + // update neighbour info + neighbour->id = id; + neighbour->advert_timestamp = timestamp; + neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); + neighbour->snr = (int8_t) (snr * 4); + #endif + } + + int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) { + // uint32_t now = getRTCClock()->getCurrentTimeUnique(); + // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') switch (payload[0]) { - case CMD_GET_STATUS: { // guests can also access this now + case REQ_TYPE_GET_STATUS: { // guests can also access this now RepeaterStats stats; stats.batt_milli_volts = board.getBattMilliVolts(); - stats.curr_tx_queue_len = _mgr->getOutboundCount(); + stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); stats.curr_free_queue_len = _mgr->getFreeCount(); stats.last_rssi = (int16_t) radio_driver.getLastRSSI(); stats.n_packets_recv = radio_driver.getPacketsRecv(); @@ -152,7 +193,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { stats.n_sent_direct = getNumSentDirect(); stats.n_recv_flood = getNumRecvFlood(); stats.n_recv_direct = getNumRecvDirect(); - stats.n_full_events = getNumFullEvents(); + stats.err_events = _err_flags; stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); @@ -161,9 +202,18 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { return 4 + sizeof(stats); // reply_len } + case REQ_TYPE_GET_TELEMETRY_DATA: { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(sender->is_admin ? 0xFF : 0x00, telemetry); + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } } - // unknown command - return 0; // reply_len + return 0; // unknown command } mesh::Packet* createSelfAdvert() { @@ -178,8 +228,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { } File openAppend(const char* fname) { - #if defined(NRF52_PLATFORM) + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) return _fs->open(fname, FILE_O_WRITE); + #elif defined(RP2040_PLATFORM) + return _fs->open(fname, "a"); #else return _fs->open(fname, "a", true); #endif @@ -357,6 +409,18 @@ protected: } } + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl + + // if this a zero hop advert, add it to neighbours + if (packet->path_len == 0) { + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters + putNeighbour(id, timestamp, packet->getSNR()); + } + } + } + void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override { int i = matching_peer_indexes[sender_idx]; if (i < 0 || i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) @@ -369,7 +433,7 @@ protected: memcpy(×tamp, data, 4); if (timestamp > client->last_timestamp) { // prevent replay attacks - int reply_len = handleRequest(client, &data[4], len - 4); + int reply_len = handleRequest(client, timestamp, &data[4], len - 4); if (reply_len == 0) return; // invalid command client->last_timestamp = timestamp; @@ -423,12 +487,14 @@ protected: } uint8_t temp[166]; + const char *command = (const char *) &data[5]; + char *reply = (char *) &temp[5]; if (is_retry) { - temp[0] = 0; + *reply = 0; } else { - _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); + _cli.handleCommand(sender_timestamp, command, reply); } - int text_len = strlen((char *) &temp[5]); + int text_len = strlen(reply); if (text_len > 0) { uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); if (timestamp == sender_timestamp) { @@ -472,12 +538,16 @@ protected: public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - _cli(board, this, &_prefs, this) + _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { memset(known_clients, 0, sizeof(known_clients)); next_local_advert = next_flood_advert = 0; _logging = false; + #if MAX_NEIGHBOURS + memset(neighbours, 0, sizeof(neighbours)); + #endif + // defaults memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; // one half @@ -516,18 +586,23 @@ public: const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } const char* getRole() override { return FIRMWARE_ROLE; } const char* getNodeName() { return _prefs.node_name; } + NodePrefs* getNodePrefs() { + return &_prefs; + } void savePrefs() override { _cli.savePrefs(_fs); } bool formatFileSystem() override { -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) return InternalFS.format(); +#elif defined(RP2040_PLATFORM) + return LittleFS.format(); #elif defined(ESP32) return SPIFFS.format(); #else - #error "need to implement file system erase" + #error "need to implement file system erase" return false; #endif } @@ -563,7 +638,11 @@ public: } void dumpLogFile() override { +#if defined(RP2040_PLATFORM) + File f = _fs->open(PACKET_LOG_FILE, "r"); +#else File f = _fs->open(PACKET_LOG_FILE); +#endif if (f) { while (f.available()) { int c = f.read(); @@ -578,6 +657,41 @@ public: radio_set_tx_power(power_dbm); } + void formatNeighborsReply(char *reply) override { + char *dp = reply; + +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS && dp - reply < 134; i++) { + NeighbourInfo* neighbour = &neighbours[i]; + if (neighbour->heard_timestamp == 0) continue; // skip empty slots + + // add new line if not first item + if (i > 0) *dp++ = '\n'; + + char hex[10]; + // get 4 bytes of neighbour id as hex + mesh::Utils::toHex(hex, neighbour->id.pub_key, 4); + + // add next neighbour + uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr); + while (*dp) dp++; // find end of string + } +#endif + if (dp == reply) { // no neighbours, need empty response + strcpy(dp, "-none-"); dp += 6; + } + *dp = 0; // null terminator + } + + const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; } + + void clearStats() override { + radio_driver.resetStats(); + resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); + } + void loop() { mesh::Mesh::loop(); @@ -617,7 +731,7 @@ void setup() { board.begin(); #ifdef DISPLAY_CLASS - if(display.begin()){ + if (display.begin()) { display.startFrame(); display.print("Please wait..."); display.endFrame(); @@ -629,7 +743,7 @@ void setup() { fast_rng.begin(radio_get_rng_seed()); FILESYSTEM* fs; -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) InternalFS.begin(); fs = &InternalFS; IdentityStore store(InternalFS, ""); @@ -637,6 +751,11 @@ void setup() { SPIFFS.begin(true); fs = &SPIFFS; IdentityStore store(SPIFFS, "/identity"); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + fs = &LittleFS; + IdentityStore store(LittleFS, "/identity"); + store.begin(); #else #error "need to define filesystem" #endif @@ -655,10 +774,12 @@ void setup() { command[0] = 0; + sensors.begin(); + the_mesh.begin(fs); #ifdef DISPLAY_CLASS - ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); + ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif // send out initial Advertisement to the mesh @@ -691,4 +812,5 @@ void loop() { } the_mesh.loop(); + sensors.loop(); } diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index eb7be78e..46311c5e 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -1,7 +1,9 @@ #include "UITask.h" #include +#include -#define AUTO_OFF_MILLIS 20000 // 20 seconds +#define AUTO_OFF_MILLIS 20000 // 20 seconds +#define BOOT_SCREEN_MILLIS 4000 // 4 seconds // 'meshcore', 128x13px static const uint8_t meshcore_logo [] PROGMEM = { @@ -20,10 +22,10 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(const char* node_name, const char* build_date, const char* firmware_version) { +void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; - _node_name = node_name; + _node_prefs = node_prefs; _display->turnOn(); // strip off dash and commit hash by changing dash to null terminator @@ -39,18 +41,43 @@ void UITask::begin(const char* node_name, const char* build_date, const char* fi } void UITask::renderCurrScreen() { - // render 'home' screen - _display->drawXbm(0, 0, meshcore_logo, 128, 13); - _display->setCursor(0, 20); - _display->setTextSize(1); - _display->print(_node_name); + char tmp[80]; + if (millis() < BOOT_SCREEN_MILLIS) { // boot screen + // meshcore logo + _display->setColor(DisplayDriver::BLUE); + int logoWidth = 128; + _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); - _display->setCursor(0, 32); - _display->print(_version_info); - _display->setCursor(0, 43); - _display->print("< Room Server >"); - //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); - //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); + // version info + _display->setColor(DisplayDriver::LIGHT); + _display->setTextSize(1); + uint16_t versionWidth = _display->getTextWidth(_version_info); + _display->setCursor((_display->width() - versionWidth) / 2, 22); + _display->print(_version_info); + + // node type + const char* node_type = "< Room Server >"; + uint16_t typeWidth = _display->getTextWidth(node_type); + _display->setCursor((_display->width() - typeWidth) / 2, 35); + _display->print(node_type); + } else { // home screen + // node name + _display->setCursor(0, 0); + _display->setTextSize(1); + _display->setColor(DisplayDriver::GREEN); + _display->print(_node_prefs->node_name); + + // freq / sf + _display->setCursor(0, 20); + _display->setColor(DisplayDriver::YELLOW); + sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); + _display->print(tmp); + + // bw / cr + _display->setCursor(0, 30); + sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); + _display->print(tmp); + } } void UITask::loop() { diff --git a/examples/simple_room_server/UITask.h b/examples/simple_room_server/UITask.h index 13005957..a27259f1 100644 --- a/examples/simple_room_server/UITask.h +++ b/examples/simple_room_server/UITask.h @@ -1,18 +1,19 @@ #pragma once #include +#include class UITask { DisplayDriver* _display; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; - const char* _node_name; + NodePrefs* _node_prefs; char _version_info[32]; void renderCurrScreen(); public: UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } - void begin(const char* node_name, const char* build_date, const char* firmware_version); + void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); }; \ No newline at end of file diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 9d07e7f4..dad7ce78 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -3,6 +3,8 @@ #if defined(NRF52_PLATFORM) #include +#elif defined(RP2040_PLATFORM) + #include #elif defined(ESP32) #include #endif @@ -20,11 +22,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "21 Apr 2025" + #define FIRMWARE_BUILD_DATE "24 May 2025" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.5.1" + #define FIRMWARE_VERSION "v1.6.2" #endif #ifndef LORA_FREQ @@ -66,10 +68,6 @@ #endif #ifdef DISPLAY_CLASS - #include - - static DISPLAY_CLASS display; - #include "UITask.h" static UITask ui_task(display); #endif @@ -119,8 +117,9 @@ struct PostInfo { #define CLIENT_KEEP_ALIVE_SECS 128 -#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS -#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -135,7 +134,7 @@ struct ServerStats { uint32_t total_up_time_secs; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; - uint16_t n_full_events; + uint16_t err_events; // was 'n_full_events' int16_t last_snr; // x 4 uint16_t n_direct_dups, n_flood_dups; uint16_t n_posted, n_post_push; @@ -155,6 +154,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { int next_client_idx; // for round-robin polling int next_post_idx; PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue + CayenneLPP telemetry; ClientInfo* putClient(const mesh::Identity& id) { for (int i = 0; i < num_clients; i++) { @@ -270,11 +270,58 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { File openAppend(const char* fname) { #if defined(NRF52_PLATFORM) return _fs->open(fname, FILE_O_WRITE); + #elif defined(RP2040_PLATFORM) + return _fs->open(fname, "a"); #else return _fs->open(fname, "a", true); #endif } + int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) { + // uint32_t now = getRTCClock()->getCurrentTimeUnique(); + // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + switch (payload[0]) { + case REQ_TYPE_GET_STATUS: { + ServerStats stats; + stats.batt_milli_volts = board.getBattMilliVolts(); + stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); + stats.curr_free_queue_len = _mgr->getFreeCount(); + stats.last_rssi = (int16_t) radio_driver.getLastRSSI(); + stats.n_packets_recv = radio_driver.getPacketsRecv(); + stats.n_packets_sent = radio_driver.getPacketsSent(); + stats.total_air_time_secs = getTotalAirTime() / 1000; + stats.total_up_time_secs = _ms->getMillis() / 1000; + stats.n_sent_flood = getNumSentFlood(); + stats.n_sent_direct = getNumSentDirect(); + stats.n_recv_flood = getNumRecvFlood(); + stats.n_recv_direct = getNumRecvDirect(); + stats.err_events = _err_flags; + stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.n_posted = _num_posted; + stats.n_post_push = _num_post_pushes; + + memcpy(&reply_data[4], &stats, sizeof(stats)); + return 4 + sizeof(stats); + } + + case REQ_TYPE_GET_TELEMETRY_DATA: { + telemetry.reset(); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + // query other sensors -- target specific + sensors.querySensors(sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00, telemetry); + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + } + return 0; // unknown command + } + protected: float getAirtimeBudgetFactor() const override { return _prefs.airtime_factor; @@ -587,44 +634,22 @@ protected: sendDirect(reply, client->out_path, client->out_path_len); } } - } else if (data[4] == REQ_TYPE_GET_STATUS) { - ServerStats stats; - stats.batt_milli_volts = board.getBattMilliVolts(); - stats.curr_tx_queue_len = _mgr->getOutboundCount(); - stats.curr_free_queue_len = _mgr->getFreeCount(); - stats.last_rssi = (int16_t) radio_driver.getLastRSSI(); - stats.n_packets_recv = radio_driver.getPacketsRecv(); - stats.n_packets_sent = radio_driver.getPacketsSent(); - stats.total_air_time_secs = getTotalAirTime() / 1000; - stats.total_up_time_secs = _ms->getMillis() / 1000; - stats.n_sent_flood = getNumSentFlood(); - stats.n_sent_direct = getNumSentDirect(); - stats.n_recv_flood = getNumRecvFlood(); - stats.n_recv_direct = getNumRecvDirect(); - stats.n_full_events = getNumFullEvents(); - stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); - stats.n_posted = _num_posted; - stats.n_post_push = _num_post_pushes; - - now = getRTCClock()->getCurrentTimeUnique(); - memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - memcpy(&reply_data[4], &stats, sizeof(stats)); - uint8_t reply_len = 4 + sizeof(stats); - - if (packet->isRouteFlood()) { - // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response - mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path); - } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len); - } else { - sendFlood(reply); + } else { + int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4); + if (reply_len > 0) { // valid command + if (packet->isRouteFlood()) { + // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response + mesh::Packet* path = createPathReturn(client->id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len); + } else { + sendFlood(reply); + } } } } @@ -663,7 +688,7 @@ protected: public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), - _cli(board, this, &_prefs, this) + _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { next_local_advert = next_flood_advert = 0; _logging = false; @@ -717,6 +742,9 @@ public: const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } const char* getRole() override { return FIRMWARE_ROLE; } const char* getNodeName() { return _prefs.node_name; } + NodePrefs* getNodePrefs() { + return &_prefs; + } void savePrefs() override { _cli.savePrefs(_fs); @@ -725,6 +753,8 @@ public: bool formatFileSystem() override { #if defined(NRF52_PLATFORM) return InternalFS.format(); + #elif defined(RP2040_PLATFORM) + return LittleFS.format(); #elif defined(ESP32) return SPIFFS.format(); #else @@ -764,7 +794,11 @@ public: } void dumpLogFile() override { + #if defined(RP2040_PLATFORM) + File f = _fs->open(PACKET_LOG_FILE, "r"); + #else File f = _fs->open(PACKET_LOG_FILE); + #endif if (f) { while (f.available()) { int c = f.read(); @@ -779,6 +813,18 @@ public: radio_set_tx_power(power_dbm); } + void formatNeighborsReply(char *reply) override { + strcpy(reply, "not supported"); + } + + const uint8_t* getSelfIdPubKey() override { return self_id.pub_key; } + + void clearStats() override { + radio_driver.resetStats(); + resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); + } + void loop() { mesh::Mesh::loop(); @@ -859,7 +905,7 @@ void setup() { board.begin(); #ifdef DISPLAY_CLASS - if(display.begin()){ + if (display.begin()) { display.startFrame(); display.print("Please wait..."); display.endFrame(); @@ -875,6 +921,11 @@ void setup() { InternalFS.begin(); fs = &InternalFS; IdentityStore store(InternalFS, ""); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + fs = &LittleFS; + IdentityStore store(LittleFS, "/identity"); + store.begin(); #elif defined(ESP32) SPIFFS.begin(true); fs = &SPIFFS; @@ -896,10 +947,12 @@ void setup() { command[0] = 0; + sensors.begin(); + the_mesh.begin(fs); #ifdef DISPLAY_CLASS - ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); + ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif // send out initial Advertisement to the mesh @@ -932,4 +985,5 @@ void loop() { } the_mesh.loop(); + sensors.loop(); } diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index f5cbc743..63ff20da 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -3,6 +3,8 @@ #if defined(NRF52_PLATFORM) #include +#elif defined(RP2040_PLATFORM) + #include #elif defined(ESP32) #include #endif @@ -88,7 +90,11 @@ class MyMesh : public BaseChatMesh, ContactVisitor { void loadContacts() { if (_fs->exists("/contacts")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/contacts", "r"); + #else File file = _fs->open("/contacts"); + #endif if (file) { bool full = false; while (!full) { @@ -121,8 +127,10 @@ class MyMesh : public BaseChatMesh, ContactVisitor { void saveContacts() { #if defined(NRF52_PLATFORM) + _fs->remove("/contacts"); File file = _fs->open("/contacts", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = _fs->open("/contacts", "w"); #else File file = _fs->open("/contacts", "w", true); #endif @@ -246,6 +254,10 @@ protected: Serial.printf(" %s\n", text); } + uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) override { + return 0; // unknown + } + void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) override { // not supported } @@ -287,6 +299,9 @@ public: #if defined(NRF52_PLATFORM) IdentityStore store(fs, ""); + #elif defined(RP2040_PLATFORM) + IdentityStore store(fs, "/identity"); + store.begin(); #else IdentityStore store(fs, "/identity"); #endif @@ -309,7 +324,11 @@ public: // load persisted prefs if (_fs->exists("/node_prefs")) { + #if defined(RP2040_PLATFORM) + File file = _fs->open("/node_prefs", "r"); + #else File file = _fs->open("/node_prefs"); + #endif if (file) { file.read((uint8_t *) &_prefs, sizeof(_prefs)); file.close(); @@ -322,8 +341,10 @@ public: void savePrefs() { #if defined(NRF52_PLATFORM) + _fs->remove("/node_prefs"); File file = _fs->open("/node_prefs", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = _fs->open("/node_prefs", "w"); #else File file = _fs->open("/node_prefs", "w", true); #endif @@ -545,6 +566,9 @@ void setup() { #if defined(NRF52_PLATFORM) InternalFS.begin(); the_mesh.begin(InternalFS); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + the_mesh.begin(LittleFS); #elif defined(ESP32) SPIFFS.begin(true); the_mesh.begin(SPIFFS); diff --git a/platformio.ini b/platformio.ini index 985943c7..80f850ea 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,6 +22,7 @@ lib_deps = rweather/Crypto @ ^0.4.0 adafruit/RTClib @ ^2.1.3 melopero/Melopero RV3028 @ ^1.1.0 + electroniccats/CayenneLPP @ 1.4.0 build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 -D LORA_FREQ=869.525 -D LORA_BW=250 @@ -47,6 +48,7 @@ lib_deps = file://arch/esp32/AsyncElegantOTA ; ----------------- NRF52 --------------------- + [nrf52_base] extends = arduino_base platform = nordicnrf52 @@ -59,4 +61,26 @@ build_flags = ${nrf52_base.build_flags} lib_deps = ${nrf52_base.lib_deps} rweather/Crypto @ ^0.4.0 - https://github.com/adafruit/Adafruit_nRF52_Arduino \ No newline at end of file + https://github.com/adafruit/Adafruit_nRF52_Arduino + +; ----------------- RP2040 --------------------- + +[rp2040_base] +extends = arduino_base +build_flags = ${arduino_base.build_flags} + -D RP2040_PLATFORM + +; ----------------- STM32 ---------------------- + +[stm32_base] +extends = arduino_base +platform = platformio/ststm32@19.1.0 +platform_packages = platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip +extra_scripts = post:arch/stm32/build_hex.py +build_flags = ${arduino_base.build_flags} + -D STM32_PLATFORM + -I src/helpers/stm32 +build_src_filter = ${arduino_base.build_src_filter} + + +lib_deps = ${arduino_base.lib_deps} + file://arch/stm32/Adafruit_LittleFS_stm32 \ No newline at end of file diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 7a415f07..3d5b04fc 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -13,9 +13,11 @@ namespace mesh { void Dispatcher::begin() { n_sent_flood = n_sent_direct = 0; n_recv_flood = n_recv_direct = 0; - n_full_events = 0; + _err_flags = 0; + radio_nonrx_start = _ms->getMillis(); _radio->begin(); + prev_isrecv_mode = _radio->isInRecvMode(); } float Dispatcher::getAirtimeBudgetFactor() const { @@ -34,6 +36,18 @@ uint32_t Dispatcher::getCADFailMaxDuration() const { } void Dispatcher::loop() { + // check for radio 'stuck' in mode other than Rx + bool is_recv = _radio->isInRecvMode(); + if (is_recv != prev_isrecv_mode) { + prev_isrecv_mode = is_recv; + if (!is_recv) { + radio_nonrx_start = _ms->getMillis(); + } + } + if (!is_recv && _ms->getMillis() - radio_nonrx_start > 8000) { // radio has not been in Rx mode for 8 seconds! + _err_flags |= ERR_EVENT_STARTRX_TIMEOUT; + } + if (outbound) { // waiting for outbound send to be completed if (_radio->isSendComplete()) { long t = _ms->getMillis() - outbound_start; @@ -191,7 +205,7 @@ void Dispatcher::processRecvPacket(Packet* pkt) { } void Dispatcher::checkSend() { - if (_mgr->getOutboundCount() == 0) return; // nothing waiting to send + if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return; // nothing waiting to send if (!millisHasNowPassed(next_tx_time)) return; // still in 'radio silence' phase (from airtime budget setting) if (_radio->isReceiving()) { // LBT - check if radio is currently mid-receive, or if channel activity if (cad_busy_start == 0) { @@ -199,6 +213,8 @@ void Dispatcher::checkSend() { } if (_ms->getMillis() - cad_busy_start > getCADFailMaxDuration()) { + _err_flags |= ERR_EVENT_CAD_TIMEOUT; + MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): CAD busy max duration reached!", getLogDateTime()); // channel activity has gone on too long... (Radio might be in a bad state) // force the pending transmit below... @@ -234,7 +250,16 @@ void Dispatcher::checkSend() { uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2; outbound_start = _ms->getMillis(); - _radio->startSendRaw(raw, len); + bool success = _radio->startSendRaw(raw, len); + if (!success) { + MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime()); + + logTxFail(outbound, outbound->getRawLength()); + + releasePacket(outbound); // return to pool + outbound = NULL; + return; + } outbound_expiry = futureMillis(max_airtime); #if MESH_PACKET_LOGGING @@ -255,7 +280,7 @@ void Dispatcher::checkSend() { Packet* Dispatcher::obtainNewPacket() { auto pkt = _mgr->allocNew(); // TODO: zero out all fields if (pkt == NULL) { - n_full_events++; + _err_flags |= ERR_EVENT_FULL; } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 393d0856..d03c9f73 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -42,8 +42,9 @@ public: * \brief starts the raw packet send. (no wait) * \param bytes the raw packet data * \param len the length in bytes + * \returns true if successfully started */ - virtual void startSendRaw(const uint8_t* bytes, int len) = 0; + virtual bool startSendRaw(const uint8_t* bytes, int len) = 0; /** * \returns true if the previous 'startSendRaw()' completed successfully. @@ -55,6 +56,8 @@ public: */ virtual void onSendFinished() = 0; + virtual bool isInRecvMode() const = 0; + /** * \returns true if the radio is currently mid-receive of a packet. */ @@ -75,7 +78,7 @@ public: virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0; virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority - virtual int getOutboundCount() const = 0; + virtual int getOutboundCount(uint32_t now) const = 0; virtual int getFreeCount() const = 0; virtual Packet* getOutboundByIdx(int i) = 0; virtual Packet* removeOutboundByIdx(int i) = 0; @@ -90,6 +93,10 @@ typedef uint32_t DispatcherAction; #define ACTION_RETRANSMIT(pri) (((uint32_t)1 + (pri))<<24) #define ACTION_RETRANSMIT_DELAYED(pri, _delay) ((((uint32_t)1 + (pri))<<24) | (_delay)) +#define ERR_EVENT_FULL (1 << 0) +#define ERR_EVENT_CAD_TIMEOUT (1 << 1) +#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2) + /** * \brief The low-level task that manages detecting incoming Packets, and the queueing * and scheduling of outbound Packets. @@ -99,9 +106,10 @@ class Dispatcher { unsigned long outbound_expiry, outbound_start, total_air_time; unsigned long next_tx_time; unsigned long cad_busy_start; + unsigned long radio_nonrx_start; + bool prev_isrecv_mode; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; - uint32_t n_full_events; void processRecvPacket(Packet* pkt); @@ -109,12 +117,16 @@ protected: PacketManager* _mgr; Radio* _radio; MillisecondClock* _ms; + uint16_t _err_flags; Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr) : _radio(&radio), _ms(&ms), _mgr(&mgr) { outbound = NULL; total_air_time = 0; next_tx_time = 0; cad_busy_start = 0; + _err_flags = 0; + radio_nonrx_start = 0; + prev_isrecv_mode = true; } virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; @@ -144,7 +156,10 @@ public: uint32_t getNumSentDirect() const { return n_sent_direct; } uint32_t getNumRecvFlood() const { return n_recv_flood; } uint32_t getNumRecvDirect() const { return n_recv_direct; } - uint32_t getNumFullEvents() const { return n_full_events; } + void resetStats() { + n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0; + _err_flags = 0; + } // helper methods bool millisHasNowPassed(unsigned long timestamp) const; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index fe3b2473..6029c192 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -71,7 +71,15 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { // remove our hash from 'path', then re-broadcast pkt->path_len -= PATH_HASH_SIZE; + #if 0 memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len); + #elif PATH_HASH_SIZE == 1 + for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1 + pkt->path[k] = pkt->path[k + 1]; + } + #else + #error "need path remove impl" + #endif uint32_t d = getDirectRetransmitDelay(pkt); return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority diff --git a/src/Mesh.h b/src/Mesh.h index cb81f8de..9649187c 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -16,6 +16,7 @@ public: class MeshTables { public: virtual bool hasSeen(const Packet* packet) = 0; + virtual void clear(const Packet* packet) = 0; // remove this packet hash from table }; /** diff --git a/src/Packet.cpp b/src/Packet.cpp index cb31638c..2d54ca45 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -52,6 +52,7 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { memcpy(path, &src[i], path_len); i += path_len; if (i >= len) return false; // bad encoding payload_len = len - i; + if (payload_len > sizeof(payload)) return false; // bad encoding memcpy(payload, &src[i], payload_len); //i += payload_len; return true; // success } diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 5972bce2..88253f61 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -8,8 +8,14 @@ memcpy(&app_data[i], &_lat, 4); i += 4; memcpy(&app_data[i], &_lon, 4); i += 4; } - // TODO: BATTERY encoding - // TODO: TEMPERATURE encoding + if (_extra1) { + app_data[0] |= ADV_FEAT1_MASK; + memcpy(&app_data[i], &_extra1, 2); i += 2; + } + if (_extra2) { + app_data[0] |= ADV_FEAT2_MASK; + memcpy(&app_data[i], &_extra2, 2); i += 2; + } if (_name && *_name != 0) { app_data[0] |= ADV_NAME_MASK; const char* sp = _name; @@ -25,17 +31,18 @@ _lat = _lon = 0; _flags = app_data[0]; _valid = false; - + _extra1 = _extra2 = 0; + int i = 1; if (_flags & ADV_LATLON_MASK) { memcpy(&_lat, &app_data[i], 4); i += 4; memcpy(&_lon, &app_data[i], 4); i += 4; } - if (_flags & ADV_BATTERY_MASK) { - /* TODO: somewhere to store battery volts? */ i += 2; + if (_flags & ADV_FEAT1_MASK) { + memcpy(&_extra1, &app_data[i], 2); i += 2; } - if (_flags & ADV_TEMPERATURE_MASK) { - /* TODO: somewhere to store temperature? */ i += 2; + if (_flags & ADV_FEAT2_MASK) { + memcpy(&_extra2, &app_data[i], 2); i += 2; } if (app_data_len >= i) { diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index a8a24a0a..14815eac 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -11,20 +11,25 @@ //FUTURE: 4..15 #define ADV_LATLON_MASK 0x10 -#define ADV_BATTERY_MASK 0x20 -#define ADV_TEMPERATURE_MASK 0x40 +#define ADV_FEAT1_MASK 0x20 // FUTURE +#define ADV_FEAT2_MASK 0x40 // FUTURE #define ADV_NAME_MASK 0x80 class AdvertDataBuilder { uint8_t _type; const char* _name; int32_t _lat, _lon; + uint16_t _extra1 = 0; + uint16_t _extra2 = 0; public: AdvertDataBuilder(uint8_t adv_type) : _type(adv_type), _name(NULL), _lat(0), _lon(0) { } AdvertDataBuilder(uint8_t adv_type, const char* name) : _type(adv_type), _name(name), _lat(0), _lon(0) { } AdvertDataBuilder(uint8_t adv_type, const char* name, double lat, double lon) : _type(adv_type), _name(name), _lat(lat * 1E6), _lon(lon * 1E6) { } + void setFeat1(uint16_t extra) { _extra1 = extra; } + void setFeat2(uint16_t extra) { _extra2 = extra; } + /** * \brief encode the given advertisement data. * \param app_data dest array, must be MAX_ADVERT_DATA_SIZE @@ -38,11 +43,15 @@ class AdvertDataParser { bool _valid; char _name[MAX_ADVERT_DATA_SIZE]; int32_t _lat, _lon; + uint16_t _extra1; + uint16_t _extra2; public: AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len); bool isValid() const { return _valid; } uint8_t getType() const { return _flags & 0x0F; } + uint16_t getFeat1() const { return _extra1; } + uint16_t getFeat2() const { return _extra2; } bool hasName() const { return _name[0] != 0; } const char* getName() const { return _name; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index e84f6d37..c4086353 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -8,30 +8,18 @@ class ArduinoSerialInterface : public BaseSerialInterface { uint8_t _state; uint16_t _frame_len; uint16_t rx_len; -#ifdef LILYGO_T3S3 - HWCDC* _serial; -#elif defined(NRF52_PLATFORM) - Adafruit_USBD_CDC* _serial; -#else - HardwareSerial* _serial; -#endif + Stream* _serial; uint8_t rx_buf[MAX_FRAME_SIZE]; public: ArduinoSerialInterface() { _isEnabled = false; _state = 0; } -#ifdef LILYGO_T3S3 - void begin(HWCDC& serial) { _serial = &serial; } -#elif defined(NRF52_PLATFORM) - void begin(Adafruit_USBD_CDC& serial) { - _serial = &serial; + void begin(Stream& serial) { + _serial = &serial; #ifdef RAK_4631 pinMode(WB_IO2, OUTPUT); - #endif + #endif } -#else - void begin(HardwareSerial& serial) { _serial = &serial; } -#endif // BaseSerialInterface methods void enable() override; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 97aefff4..36ddcbb4 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -178,6 +178,27 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } else { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags); } + } else if (type == PAYLOAD_TYPE_REQ && len > 4) { + uint32_t sender_timestamp; + memcpy(&sender_timestamp, data, 4); + uint8_t reply_len = onContactRequest(from, sender_timestamp, &data[4], len - 4, temp_buf); + if (reply_len > 0) { + if (packet->isRouteFlood()) { + // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response + mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len); + if (path) sendFlood(path); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); + if (reply) { + if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, from.out_path, from.out_path_len); + } else { + sendFlood(reply); + } + } + } + } } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { onContactResponse(from, data, len); } @@ -352,6 +373,7 @@ bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) { if (pkt) { if (pkt->readFrom(src_buf, len) && pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT) { pkt->header |= ROUTE_TYPE_FLOOD; // simulate it being received flood-mode + getTables()->clear(pkt); // remove packet hash from table, so we can receive/process it again _pendingLoopback = pkt; // loop-back, as if received over radio return true; // success } else { @@ -393,11 +415,11 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, return MSG_SEND_FAILED; } -int BaseChatMesh::sendStatusRequest(const ContactInfo& recipient, uint32_t& est_timeout) { +int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) { uint8_t temp[13]; - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = REQ_TYPE_GET_STATUS; + tag = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique + temp[4] = req_type; memset(&temp[5], 0, 4); // reserved (possibly for 'since' param) getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index ed6e1c3a..2222bf4e 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -114,6 +114,7 @@ protected: virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0; virtual void onSendTimeout() = 0; virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0; + virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0; virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0; // storage concepts, for sub-classes to override/implement @@ -146,7 +147,7 @@ public: int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout); bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); - int sendStatusRequest(const ContactInfo& recipient, uint32_t& est_timeout); + int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout); bool shareContactZeroHop(const ContactInfo& contact); uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]); bool importContact(const uint8_t src_buf[], uint8_t len); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2bafd5f9..8b8296f5 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -24,7 +24,11 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { } void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { +#if defined(RP2040_PLATFORM) + File file = fs->open(filename, "r"); +#else File file = fs->open(filename); +#endif if (file) { uint8_t pad[8]; @@ -69,9 +73,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } void CommonCLI::savePrefs(FILESYSTEM* fs) { -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + fs->remove("/com_prefs"); File file = fs->open("/com_prefs", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = fs->open("/com_prefs", "w"); #else File file = fs->open("/com_prefs", "w", true); #endif @@ -155,12 +161,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "(ERR: clock cannot go backwards)"); } + } else if (memcmp(command, "neighbors", 9) == 0) { + _callbacks->formatNeighborsReply(reply); } else if (memcmp(command, "password ", 9) == 0) { // change admin password StrHelper::strncpy(_prefs->password, &command[9], sizeof(_prefs->password)); checkAdvertInterval(); savePrefs(); sprintf(reply, "password now: %s", _prefs->password); // echo back just to let admin know for sure!! + } else if (memcmp(command, "clear stats", 11) == 0) { + _callbacks->clearStats(); + strcpy(reply, "(OK - stats reset)"); } else if (memcmp(command, "get ", 4) == 0) { const char* config = &command[4]; if (memcmp(config, "af", 2) == 0) { @@ -200,7 +211,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", StrHelper::ftoa(_prefs->freq)); } else if (memcmp(config, "public.key", 10) == 0) { strcpy(reply, "> "); - mesh::Utils::toHex(&reply[2], _mesh->self_id.pub_key, PUB_KEY_SIZE); + mesh::Utils::toHex(&reply[2], _callbacks->getSelfIdPubKey(), PUB_KEY_SIZE); } else if (memcmp(config, "role", 4) == 0) { sprintf(reply, "> %s", _callbacks->getRole()); } else { @@ -347,6 +358,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _callbacks->dumpLogFile(); strcpy(reply, " EOF"); } else { - sprintf(reply, "Unknown: %s", command); + strcpy(reply, "Unknown command"); } } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 50e5f8d6..0e88c266 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -40,16 +40,19 @@ public: virtual void eraseLogFile() = 0; virtual void dumpLogFile() = 0; virtual void setTxPower(uint8_t power_dbm) = 0; + virtual void formatNeighborsReply(char *reply) = 0; + virtual const uint8_t* getSelfIdPubKey() = 0; + virtual void clearStats() = 0; }; class CommonCLI { - mesh::Mesh* _mesh; + mesh::RTCClock* _rtc; NodePrefs* _prefs; CommonCLICallbacks* _callbacks; mesh::MainBoard* _board; char tmp[80]; - mesh::RTCClock* getRTCClock() { return _mesh->getRTCClock(); } + mesh::RTCClock* getRTCClock() { return _rtc; } void savePrefs() { _callbacks->savePrefs(); } void checkAdvertInterval(); @@ -57,8 +60,8 @@ class CommonCLI { void loadPrefsInt(FILESYSTEM* _fs, const char* filename); public: - CommonCLI(mesh::MainBoard& board, mesh::Mesh* mesh, NodePrefs* prefs, CommonCLICallbacks* callbacks) - : _board(&board), _mesh(mesh), _prefs(prefs), _callbacks(callbacks) { } + CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, NodePrefs* prefs, CommonCLICallbacks* callbacks) + : _board(&board), _rtc(&rtc), _prefs(prefs), _callbacks(callbacks) { } void loadPrefs(FILESYSTEM* _fs); void savePrefs(FILESYSTEM* _fs); diff --git a/src/helpers/CustomSTM32WLx.h b/src/helpers/CustomSTM32WLx.h new file mode 100644 index 00000000..cbdd5c8c --- /dev/null +++ b/src/helpers/CustomSTM32WLx.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received +#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 + +class CustomSTM32WLx : public STM32WLx { + public: + CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { } + + bool isReceiving() { + uint16_t irq = getIrqFlags(); + bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + return detected; + } +}; \ No newline at end of file diff --git a/src/helpers/CustomSTM32WLxWrapper.h b/src/helpers/CustomSTM32WLxWrapper.h new file mode 100644 index 00000000..84f78376 --- /dev/null +++ b/src/helpers/CustomSTM32WLxWrapper.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CustomSTM32WLx.h" +#include "RadioLibWrappers.h" +#include + +class CustomSTM32WLxWrapper : public RadioLibWrapper { +public: + CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } + bool isReceiving() override { + if (((CustomSTM32WLx *)_radio)->isReceiving()) return true; + + idle(); // put sx126x into standby + // do some basic CAD (blocks for ~12780 micros (on SF 10)!) + bool activity = (((CustomSTM32WLx *)_radio)->scanChannel() == RADIOLIB_LORA_DETECTED); + if (activity) { + startRecv(); + } else { + idle(); + } + return activity; + } + float getLastRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); } + float getLastSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); } + + float packetScore(float snr, int packet_len) override { + int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; + return packetScoreInt(snr, sf, packet_len); + } +}; diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index 57838d32..dd1f1801 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -8,6 +8,8 @@ #include #include +#include + bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { WiFi.softAP("MeshCore-OTA", NULL); @@ -24,6 +26,9 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) { server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/html", home_buf); }); + server->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(SPIFFS, "/packet_log", "text/plain"); + }); AsyncElegantOTA.setID(id_buf); AsyncElegantOTA.begin(server); // Start ElegantOTA diff --git a/src/helpers/HeltecV3Board.h b/src/helpers/HeltecV3Board.h index 605bfe21..988e66d9 100644 --- a/src/helpers/HeltecV3Board.h +++ b/src/helpers/HeltecV3Board.h @@ -1,8 +1,10 @@ #pragma once #include +#include // LoRa radio module pins for Heltec V3 +// Also for Heltec Wireless Tracker #define P_LORA_DIO_1 14 #define P_LORA_NSS 8 #define P_LORA_RESET RADIOLIB_NC @@ -13,11 +15,12 @@ // built-ins #define PIN_VBAT_READ 1 -#define PIN_ADC_CTRL 37 +#ifndef PIN_ADC_CTRL // set in platformio.ini for Heltec Wireless Tracker (2) + #define PIN_ADC_CTRL 37 +#endif #define PIN_ADC_CTRL_ACTIVE LOW #define PIN_ADC_CTRL_INACTIVE HIGH -#define PIN_LED_BUILTIN 35 -#define PIN_VEXT_EN 36 +//#define PIN_LED_BUILTIN 35 #include "ESP32Board.h" @@ -28,6 +31,10 @@ private: bool adc_active_state; public: + RefCountedDigitalPin periph_power; + + HeltecV3Board() : periph_power(PIN_VEXT_EN) { } + void begin() { ESP32Board::begin(); @@ -38,8 +45,7 @@ public: pinMode(PIN_ADC_CTRL, OUTPUT); digitalWrite(PIN_ADC_CTRL, !adc_active_state); // Initially inactive - pinMode(PIN_VEXT_EN, OUTPUT); - digitalWrite(PIN_VEXT_EN, LOW); // for V3.2 boards + periph_power.begin(); esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { diff --git a/src/helpers/IdentityStore.cpp b/src/helpers/IdentityStore.cpp index 3b44eb4a..dc85d69c 100644 --- a/src/helpers/IdentityStore.cpp +++ b/src/helpers/IdentityStore.cpp @@ -5,7 +5,11 @@ bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) { char filename[40]; sprintf(filename, "%s/%s.id", _dir, name); if (_fs->exists(filename)) { +#if defined(RP2040_PLATFORM) + File file = _fs->open(filename, "r"); +#else File file = _fs->open(filename); +#endif if (file) { loaded = id.readFrom(file); file.close(); @@ -19,7 +23,11 @@ bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display char filename[40]; sprintf(filename, "%s/%s.id", _dir, name); if (_fs->exists(filename)) { +#if defined(RP2040_PLATFORM) + File file = _fs->open(filename, "r"); +#else File file = _fs->open(filename); +#endif if (file) { loaded = id.readFrom(file); @@ -38,9 +46,11 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) { char filename[40]; sprintf(filename, "%s/%s.id", _dir, name); -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(filename); File file = _fs->open(filename, FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = _fs->open(filename, "w"); #else File file = _fs->open(filename, "w", true); #endif @@ -58,9 +68,11 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char filename[40]; sprintf(filename, "%s/%s.id", _dir, name); -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _fs->remove(filename); File file = _fs->open(filename, FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } +#elif defined(RP2040_PLATFORM) + File file = _fs->open(filename, "w"); #else File file = _fs->open(filename, "w", true); #endif diff --git a/src/helpers/IdentityStore.h b/src/helpers/IdentityStore.h index 2a5363ea..e81290e7 100644 --- a/src/helpers/IdentityStore.h +++ b/src/helpers/IdentityStore.h @@ -1,9 +1,9 @@ #pragma once -#if defined(ESP32) +#if defined(ESP32) || defined(RP2040_PLATFORM) #include #define FILESYSTEM fs::FS -#elif defined(NRF52_PLATFORM) +#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #define FILESYSTEM Adafruit_LittleFS @@ -18,7 +18,7 @@ class IdentityStore { public: IdentityStore(FILESYSTEM& fs, const char* dir): _fs(&fs), _dir(dir) { } - void begin() { _fs->mkdir(_dir); } + void begin() { if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } } bool load(const char *name, mesh::LocalIdentity& id); bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz); bool save(const char *name, const mesh::LocalIdentity& id); diff --git a/src/helpers/RadioLibWrappers.cpp b/src/helpers/RadioLibWrappers.cpp index 03f8f3ce..39fb340e 100644 --- a/src/helpers/RadioLibWrappers.cpp +++ b/src/helpers/RadioLibWrappers.cpp @@ -44,6 +44,10 @@ void RadioLibWrapper::startRecv() { } } +bool RadioLibWrapper::isInRecvMode() const { + return (state & ~STATE_INT_READY) == STATE_RX; +} + int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { if (state & STATE_INT_READY) { int len = _radio->getPacketLength(); @@ -77,13 +81,16 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { return _radio->getTimeOnAir(len_bytes) / 1000; } -void RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { - state = STATE_TX_WAIT; +bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { _board->onBeforeTransmit(); int err = _radio->startTransmit((uint8_t *) bytes, len); - if (err != RADIOLIB_ERR_NONE) { - MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startTransmit(%d)", err); + if (err == RADIOLIB_ERR_NONE) { + state = STATE_TX_WAIT; + return true; } + MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startTransmit(%d)", err); + idle(); // trigger another startRecv() + return false; } bool RadioLibWrapper::isSendComplete() { diff --git a/src/helpers/RadioLibWrappers.h b/src/helpers/RadioLibWrappers.h index c9621cf6..bdbadb19 100644 --- a/src/helpers/RadioLibWrappers.h +++ b/src/helpers/RadioLibWrappers.h @@ -19,12 +19,15 @@ public: void begin() override; int recvRaw(uint8_t* bytes, int sz) override; uint32_t getEstAirtimeFor(int len_bytes) override; - void startSendRaw(const uint8_t* bytes, int len) override; + bool startSendRaw(const uint8_t* bytes, int len) override; bool isSendComplete() override; void onSendFinished() override; + bool isInRecvMode() const override; uint32_t getPacketsRecv() const { return n_recv; } uint32_t getPacketsSent() const { return n_sent; } + void resetStats() { n_recv = n_sent = 0; } + virtual float getLastRSSI() const override; virtual float getLastSNR() const override; diff --git a/src/helpers/RefCountedDigitalPin.h b/src/helpers/RefCountedDigitalPin.h new file mode 100644 index 00000000..14b67fb1 --- /dev/null +++ b/src/helpers/RefCountedDigitalPin.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +class RefCountedDigitalPin { + uint8_t _pin; + int8_t _claims = 0; + +public: + RefCountedDigitalPin(uint8_t pin): _pin(pin) { } + + void begin() { + pinMode(_pin, OUTPUT); + digitalWrite(_pin, LOW); // initial state + } + + void claim() { + _claims++; + if (_claims > 0) { + digitalWrite(_pin, HIGH); + } + } + void release() { + _claims--; + if (_claims == 0) { + digitalWrite(_pin, LOW); + } + } +}; diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h new file mode 100644 index 00000000..0e4bc27d --- /dev/null +++ b/src/helpers/SensorManager.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#define TELEM_PERM_BASE 0x01 // 'base' permission includes battery +#define TELEM_PERM_LOCATION 0x02 +#define TELEM_PERM_ENVIRONMENT 0x04 // permission to access environment sensors + +#define TELEM_CHANNEL_SELF 1 // LPP data channel for 'self' device + +class SensorManager { +public: + double node_lat, node_lon; // modify these, if you want to affect Advert location + double node_altitude; // altitude in meters + + SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } + virtual bool begin() { return false; } + virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } + virtual void loop() { } + virtual int getNumSettings() const { return 0; } + virtual const char* getSettingName(int i) const { return NULL; } + virtual const char* getSettingValue(int i) const { return NULL; } + virtual bool setSettingValue(const char* name, const char* value) { return false; } +}; diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 0dd6032a..2f8af52a 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -80,7 +80,32 @@ public: return false; } + void clear(const mesh::Packet* packet) override { + if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) { + uint32_t ack; + memcpy(&ack, packet->payload, 4); + for (int i = 0; i < MAX_PACKET_ACKS; i++) { + if (ack == _acks[i]) { + _acks[i] = 0; + break; + } + } + } else { + uint8_t hash[MAX_HASH_SIZE]; + packet->calculatePacketHash(hash); + + uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + memset(sp, 0, MAX_HASH_SIZE); + break; + } + } + } + } + uint32_t getNumDirectDups() const { return _direct_dups; } uint32_t getNumFloodDups() const { return _flood_dups; } + void resetStats() { _direct_dups = _flood_dups = 0; } }; diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index 07bc8f2e..4f28eac6 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -8,6 +8,15 @@ PacketQueue::PacketQueue(int max_entries) { _num = 0; } +int PacketQueue::countBefore(uint32_t now) const { + int n = 0; + for (int j = 0; j < _num; j++) { + if (_schedule_table[j] > now) continue; // scheduled for future... ignore for now + n++; + } + return n; +} + mesh::Packet* PacketQueue::get(uint32_t now) { uint8_t min_pri = 0xFF; int best_idx = -1; @@ -81,8 +90,8 @@ mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { return send_queue.get(now); } -int StaticPoolPacketManager::getOutboundCount() const { - return send_queue.count(); +int StaticPoolPacketManager::getOutboundCount(uint32_t now) const { + return send_queue.countBefore(now); } int StaticPoolPacketManager::getFreeCount() const { diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index 09c2fdec..bbf4b193 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -13,6 +13,7 @@ public: mesh::Packet* get(uint32_t now); void add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); int count() const { return _num; } + int countBefore(uint32_t now) const; mesh::Packet* itemAt(int i) const { return _table[i]; } mesh::Packet* removeByIdx(int i); }; @@ -27,7 +28,7 @@ public: void free(mesh::Packet* packet) override; void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override; mesh::Packet* getNextOutbound(uint32_t now) override; - int getOutboundCount() const override; + int getOutboundCount(uint32_t now) const override; int getFreeCount() const override; mesh::Packet* getOutboundByIdx(int i) override; mesh::Packet* removeOutboundByIdx(int i) override; diff --git a/src/helpers/TBeamBoardSX1262.h b/src/helpers/TBeamBoardSX1262.h new file mode 100644 index 00000000..f47df28e --- /dev/null +++ b/src/helpers/TBeamBoardSX1262.h @@ -0,0 +1,79 @@ +#pragma once + + +#include +#include +#include "XPowersLib.h" + +#define XPOWERS_CHIP_AXP192 + +// LoRa radio module pins for TBeam +#define P_LORA_DIO_1 33 // SX1262 IRQ pin +#define P_LORA_NSS 18 +#define P_LORA_RESET 23 +#define P_LORA_BUSY 32 // SX1262 Busy pin +#define P_LORA_SCLK 5 +#define P_LORA_MISO 19 +#define P_LORA_MOSI 27 + +#include "ESP32Board.h" + +#include + +class TBeamBoardSX1262 : public ESP32Board { + XPowersAXP192 power; + +public: + void begin() { + ESP32Board::begin(); + + power.setLDO2Voltage(3300); + power.enableLDO2(); + + power.enableBattVoltageMeasure(); + + pinMode(38, INPUT_PULLUP); + + esp_reset_reason_t reason = esp_reset_reason(); + if (reason == ESP_RST_DEEPSLEEP) { + long wakeup_source = esp_sleep_get_ext1_wakeup_status(); + if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) + startup_reason = BD_STARTUP_RX_PACKET; + } + + rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS); + rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1); + } + } + + void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) { + esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); + + // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep + rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); + rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); + + rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); + + if (pin_wake_btn < 0) { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet + } else { + esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn + } + + if (secs > 0) { + esp_sleep_enable_timer_wakeup(secs * 1000000); + } + + // Finally set ESP32 into sleep + esp_deep_sleep_start(); // CPU halts here and never returns! + } + + uint16_t getBattMilliVolts() override { + return power.getBattVoltage(); + } + + const char* getManufacturerName() const override { + return "LilyGo T-Beam SX1262"; + } +}; diff --git a/src/helpers/TBeamS3SupremeBoard.h b/src/helpers/TBeamS3SupremeBoard.h index 2b8232d8..74d9ca2e 100644 --- a/src/helpers/TBeamS3SupremeBoard.h +++ b/src/helpers/TBeamS3SupremeBoard.h @@ -15,8 +15,8 @@ #define P_LORA_MISO 13 //SX1262 MISO pin #define P_LORA_MOSI 11 //SX1262 MOSI pin -#define PIN_BOARD_SDA 17 //SDA for OLED, BME280, and QMC6310U (0x1C) -#define PIN_BOARD_SCL 18 //SCL for OLED, BME280, and QMC6310U (0x1C) +//#define PIN_BOARD_SDA 17 //SDA for OLED, BME280, and QMC6310U (0x1C) +//#define PIN_BOARD_SCL 18 //SCL for OLED, BME280, and QMC6310U (0x1C) #define PIN_BOARD_SDA1 42 //SDA for PMU and PFC8563 (RTC) #define PIN_BOARD_SCL1 41 //SCL for PMU and PFC8563 (RTC) @@ -27,7 +27,7 @@ #define P_BOARD_SPI_MOSI 35 //SPI for SD Card and QMI8653 (IMU) #define P_BOARD_SPI_MISO 37 //SPI for SD Card and QMI8653 (IMU) #define P_BOARD_SPI_SCK 36 //SPI for SD Card and QMI8653 (IMU) -#define P_BPARD_SPI_CS 47 //SPI for SD Card and QMI8653 (IMU) +#define P_BPARD_SPI_CS 47 //Pin for SD Card CS #define P_BOARD_IMU_CS 34 //Pin for QMI8653 (IMU) CS #define P_BOARD_IMU_INT 33 //IMU Int pin @@ -36,7 +36,8 @@ #define P_GPS_RX 9 //GPS RX pin #define P_GPS_TX 8 //GPS TX pin #define P_GPS_WAKE 7 //GPS Wakeup pin -#define P_GPS_1PPS 6 //GPS 1PPS pin +//#define P_GPS_1PPS 6 //GPS 1PPS pin - repurposed for lora tx led +#define GPS_BAUD_RATE 9600 //I2C Wire addresses #define I2C_BME280_ADD 0x76 //BME280 sensor I2C address on Wire @@ -47,15 +48,21 @@ #define I2C_RTC_ADD 0x51 //RTC I2C address on Wire1 #define I2C_PMU_ADD 0x34 //AXP2101 I2C address on Wire1 - +#define PMU_WIRE_PORT Wire1 +#define XPOWERS_CHIP_AXP2101 class TBeamS3SupremeBoard : public ESP32Board { - + XPowersAXP2101 PMU; public: +#ifdef MESH_DEBUG + void printPMU(); +#endif + bool power_init(); + void begin() { - - bool power_init(); - + + power_init(); + ESP32Board::begin(); esp_reset_reason_t reason = esp_reset_reason(); @@ -94,12 +101,14 @@ public: } uint16_t getBattMilliVolts() override { - - return 0; + return PMU.getBattVoltage(); } - uint16_t getBattPercent(); - + uint16_t getBattPercent() { + //Read the PMU fuel guage for battery % + uint16_t battPercent = PMU.getBatteryPercent(); + return battPercent; + } const char* getManufacturerName() const override { return "LilyGo T-Beam S3 Supreme SX1262"; } diff --git a/src/helpers/esp32/ESPNOWRadio.cpp b/src/helpers/esp32/ESPNOWRadio.cpp index 7f8f4f71..ced19f91 100644 --- a/src/helpers/esp32/ESPNOWRadio.cpp +++ b/src/helpers/esp32/ESPNOWRadio.cpp @@ -5,7 +5,7 @@ static uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; static esp_now_peer_info_t peerInfo; -static bool is_send_complete = false; +static volatile bool is_send_complete = false; static esp_err_t last_send_result; static uint8_t rx_buf[256]; static uint8_t last_rx_len = 0; @@ -22,7 +22,7 @@ static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) { last_rx_len = len; } -void ESPNOWRadio::begin() { +void ESPNOWRadio::init() { // Set device as a Wi-Fi Station WiFi.mode(WIFI_STA); // Long Range mode @@ -44,6 +44,8 @@ void ESPNOWRadio::begin() { peerInfo.channel = 0; peerInfo.encrypt = false; + is_send_complete = true; + // Add peer if (esp_now_add_peer(&peerInfo) == ESP_OK) { ESPNOW_DEBUG_PRINTLN("init success"); @@ -67,23 +69,30 @@ uint32_t ESPNOWRadio::intID() { return n + m; } -void ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) { +bool ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) { // Send message via ESP-NOW is_send_complete = false; esp_err_t result = esp_now_send(broadcastAddress, bytes, len); if (result == ESP_OK) { + n_sent++; ESPNOW_DEBUG_PRINTLN("Send success"); - } else { - last_send_result = result; - is_send_complete = true; - ESPNOW_DEBUG_PRINTLN("Send failed: %d", result); + return true; } + last_send_result = result; + is_send_complete = true; + ESPNOW_DEBUG_PRINTLN("Send failed: %d", result); + return false; } bool ESPNOWRadio::isSendComplete() { return is_send_complete; } void ESPNOWRadio::onSendFinished() { + is_send_complete = true; +} + +bool ESPNOWRadio::isInRecvMode() const { + return is_send_complete; // if NO send in progress, then we're in Rx mode } float ESPNOWRadio::getLastRSSI() const { return 0; } @@ -94,6 +103,7 @@ int ESPNOWRadio::recvRaw(uint8_t* bytes, int sz) { if (last_rx_len > 0) { memcpy(bytes, rx_buf, last_rx_len); last_rx_len = 0; + n_recv++; } return len; } diff --git a/src/helpers/esp32/ESPNOWRadio.h b/src/helpers/esp32/ESPNOWRadio.h index 7e628344..c696da3a 100644 --- a/src/helpers/esp32/ESPNOWRadio.h +++ b/src/helpers/esp32/ESPNOWRadio.h @@ -9,15 +9,18 @@ protected: public: ESPNOWRadio() { n_recv = n_sent = 0; } - void begin() override; + void init(); int recvRaw(uint8_t* bytes, int sz) override; uint32_t getEstAirtimeFor(int len_bytes) override; - void startSendRaw(const uint8_t* bytes, int len) override; + bool startSendRaw(const uint8_t* bytes, int len) override; bool isSendComplete() override; void onSendFinished() override; + bool isInRecvMode() const override; uint32_t getPacketsRecv() const { return n_recv; } uint32_t getPacketsSent() const { return n_sent; } + void resetStats() { n_recv = n_sent = 0; } + virtual float getLastRSSI() const override; virtual float getLastSNR() const override; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index eb98b58b..8a8710a7 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -169,7 +169,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } -#define BLE_WRITE_MIN_INTERVAL 20 +#define BLE_WRITE_MIN_INTERVAL 60 bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? diff --git a/src/helpers/nrf52/RAK4631Board.cpp b/src/helpers/nrf52/RAK4631Board.cpp index 8b368734..eb1a42c5 100644 --- a/src/helpers/nrf52/RAK4631Board.cpp +++ b/src/helpers/nrf52/RAK4631Board.cpp @@ -26,6 +26,10 @@ void RAK4631Board::begin() { pinMode(PIN_USER_BTN, INPUT_PULLUP); #endif +#ifdef PIN_USER_BTN_ANA + pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); +#endif + #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); #endif diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 61b570eb..f43a3767 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -94,7 +94,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } -#define BLE_WRITE_MIN_INTERVAL 20 +#define BLE_WRITE_MIN_INTERVAL 60 bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? diff --git a/src/helpers/nrf52/ThinkNodeM1Board.cpp b/src/helpers/nrf52/ThinkNodeM1Board.cpp new file mode 100644 index 00000000..ef1cf111 --- /dev/null +++ b/src/helpers/nrf52/ThinkNodeM1Board.cpp @@ -0,0 +1,90 @@ +#include +#include "ThinkNodeM1Board.h" + +#ifdef THINKNODE_M1 + +#include +#include + +static BLEDfu bledfu; + +static void connect_callback(uint16_t conn_handle) { + (void)conn_handle; + MESH_DEBUG_PRINTLN("BLE client connected"); +} + +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { + (void)conn_handle; + (void)reason; + + MESH_DEBUG_PRINTLN("BLE client disconnected"); +} + +void ThinkNodeM1Board::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + + Wire.begin(); + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} + +uint16_t ThinkNodeM1Board::getBattMilliVolts() { + int adcvalue = 0; + + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); + delay(10); + + // ADC range is 0..3000mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); +} + +bool ThinkNodeM1Board::startOTAUpdate(const char* id, char reply[]) { + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("THINKNODE_M1_OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + strcpy(reply, "OK - started"); + return true; +} +#endif diff --git a/src/helpers/nrf52/ThinkNodeM1Board.h b/src/helpers/nrf52/ThinkNodeM1Board.h new file mode 100644 index 00000000..cc87c96d --- /dev/null +++ b/src/helpers/nrf52/ThinkNodeM1Board.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +// LoRa radio module pins for Elecrow ThinkNode M1 +#define P_LORA_DIO_1 20 +#define P_LORA_NSS 24 +#define P_LORA_RESET 25 +#define P_LORA_BUSY 17 +#define P_LORA_SCLK 19 +#define P_LORA_MISO 23 +#define P_LORA_MOSI 22 +#define SX126X_POWER_EN 37 + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// built-ins +#define VBAT_MV_PER_LSB (0.73242188F) // 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 + +#define VBAT_DIVIDER (0.5F) // 150K + 150K voltage divider on VBAT +#define VBAT_DIVIDER_COMP (2.0F) // Compensation factor for the VBAT divider + +#define PIN_VBAT_READ (4) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) + +class ThinkNodeM1Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + + void begin(); + uint16_t getBattMilliVolts() override; + bool startOTAUpdate(const char* id, char reply[]) override; + + uint8_t getStartupReason() const override { + return startup_reason; + } + + const char* getManufacturerName() const override { + return "Elecrow ThinkNode-M1"; + } + + void reboot() override { + NVIC_SystemReset(); + } +}; diff --git a/src/helpers/nrf52/XiaoNrf52Board.h b/src/helpers/nrf52/XiaoNrf52Board.h index 386001a2..e6c8e7f7 100644 --- a/src/helpers/nrf52/XiaoNrf52Board.h +++ b/src/helpers/nrf52/XiaoNrf52Board.h @@ -7,25 +7,17 @@ // LoRa radio module pins for Seeed Xiao-nrf52 #ifdef SX1262_XIAO_S3_VARIANT + #undef P_LORA_DIO_1 + #undef P_LORA_BUSY + #undef P_LORA_RESET + #undef P_LORA_NSS #define P_LORA_DIO_1 D0 #define P_LORA_BUSY D1 #define P_LORA_RESET D2 #define P_LORA_NSS D3 - #define LORA_TX_BOOST_PIN D4 -#else - #define P_LORA_DIO_1 D1 - #define P_LORA_BUSY D3 - #define P_LORA_RESET D2 - #define P_LORA_NSS D4 - #define LORA_TX_BOOST_PIN D5 #endif -#define P_LORA_SCLK PIN_SPI_SCK -#define P_LORA_MISO PIN_SPI_MISO -#define P_LORA_MOSI PIN_SPI_MOSI //#define SX126X_POWER_EN 37 -#define SX126X_DIO2_AS_RF_SWITCH true -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 class XiaoNrf52Board : public mesh::MainBoard { diff --git a/src/helpers/rp2040/PicoWBoard.cpp b/src/helpers/rp2040/PicoWBoard.cpp new file mode 100644 index 00000000..f345f96d --- /dev/null +++ b/src/helpers/rp2040/PicoWBoard.cpp @@ -0,0 +1,42 @@ +#include +#include "PicoWBoard.h" + +//#include +#include + +//static BLEDfu bledfu; + +static void connect_callback(uint16_t conn_handle) { + (void)conn_handle; + MESH_DEBUG_PRINTLN("BLE client connected"); +} + +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { + (void)conn_handle; + (void)reason; + + MESH_DEBUG_PRINTLN("BLE client disconnected"); +} + +void PicoWBoard::begin() { + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + pinMode(PIN_VBAT_READ, INPUT); +#ifdef PIN_USER_BTN + pinMode(PIN_USER_BTN, INPUT_PULLUP); +#endif + +#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + + Wire.begin(); + + //pinMode(SX126X_POWER_EN, OUTPUT); + //digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up +} + +bool PicoWBoard::startOTAUpdate(const char* id, char reply[]) { + return false; +} diff --git a/src/helpers/rp2040/PicoWBoard.h b/src/helpers/rp2040/PicoWBoard.h new file mode 100644 index 00000000..cdc9c9f7 --- /dev/null +++ b/src/helpers/rp2040/PicoWBoard.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +// LoRa radio module pins for PicoW +#define P_LORA_DIO_1 20 +#define P_LORA_NSS 3 +#define P_LORA_RESET 15 +#define P_LORA_BUSY 2 +#define P_LORA_SCLK 10 +#define P_LORA_MISO 12 +#define P_LORA_MOSI 11 +//#define SX126X_POWER_EN ??? // Not Sure + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// built-ins +#define PIN_VBAT_READ 26 +#define ADC_MULTIPLIER (3.1 * 3.3 * 1000) // MT Uses 3.1 +#define PIN_LED_BUILTIN LED_BUILTIN + +class PicoWBoard : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + void begin(); + uint8_t getStartupReason() const override { return startup_reason; } + + void onBeforeTransmit() override { + digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on + } + + void onAfterTransmit() override { + digitalWrite(LED_BUILTIN, LOW); // turn TX LED off + } + + #define BATTERY_SAMPLES 8 + + uint16_t getBattMilliVolts() override { + analogReadResolution(12); + + uint32_t raw = 0; + for (int i = 0; i < BATTERY_SAMPLES; i++) { + raw += analogRead(PIN_VBAT_READ); + } + raw = raw / BATTERY_SAMPLES; + + return (ADC_MULTIPLIER * raw) / 4096; + } + + const char* getManufacturerName() const override { + return "Pico W"; + } + + void reboot() override { + //NVIC_SystemReset(); + rp2040.reboot(); + } + + bool startOTAUpdate(const char* id, char reply[]) override; +}; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp new file mode 100644 index 00000000..dda1fd55 --- /dev/null +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -0,0 +1,231 @@ +#include "EnvironmentSensorManager.h" + +#if ENV_INCLUDE_AHTX0 +#define TELEM_AHTX_ADDRESS 0x38 // AHT10, AHT20 temperature and humidity sensor I2C address +#include +static Adafruit_AHTX0 AHTX0; +#endif + +#if ENV_INCLUDE_BME280 +#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address +#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level +#include +static Adafruit_BME280 BME280; +#endif + +#if ENV_INCLUDE_INA3221 +#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address +#define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts +#define TELEM_INA3221_NUM_CHANNELS 3 +#include +static Adafruit_INA3221 INA3221; +#endif + +#if ENV_INCLUDE_INA219 +#define TELEM_INA219_ADDRESS 0x40 // INA219 single channel current sensor I2C address +#include +static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS); +#endif + +bool EnvironmentSensorManager::begin() { + #if ENV_INCLUDE_GPS + initBasicGPS(); + #endif + + #if ENV_INCLUDE_AHTX0 + if (AHTX0.begin(&Wire, 0, TELEM_AHTX_ADDRESS)) { + MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS); + AHTX0_initialized = true; + } else { + AHTX0_initialized = false; + MESH_DEBUG_PRINTLN("AHT10/AHT20 was not found at I2C address %02X", TELEM_AHTX_ADDRESS); + } + #endif + + #if ENV_INCLUDE_BME280 + if (BME280.begin(TELEM_BME280_ADDRESS, &Wire)) { + MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS); + MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID()); + BME280_initialized = true; + } else { + BME280_initialized = false; + MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS); + } + #endif + + #if ENV_INCLUDE_INA3221 + if (INA3221.begin(TELEM_INA3221_ADDRESS, &Wire)) { + MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS); + MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID()); + + for(int i = 0; i < 3; i++) { + INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE); + } + INA3221_initialized = true; + } else { + INA3221_initialized = false; + MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS); + } + #endif + + #if ENV_INCLUDE_INA219 + if (INA219.begin(&Wire)) { + MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS); + INA219_initialized = true; + } else { + INA219_initialized = false; + MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS); + } + #endif + + return true; +} + +bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + next_available_channel = TELEM_CHANNEL_SELF + 1; + + if (requester_permissions & TELEM_PERM_LOCATION) { + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, 0.0f); // allow lat/lon via telemetry even if no GPS is detected + } + + if (requester_permissions & TELEM_PERM_ENVIRONMENT) { + + #if ENV_INCLUDE_AHTX0 + if (AHTX0_initialized) { + sensors_event_t humidity, temp; + AHTX0.getEvent(&humidity, &temp); + telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature); + telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity); + } + #endif + + #if ENV_INCLUDE_BME280 + if (BME280_initialized) { + telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature()); + telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity()); + telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()); + telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA)); + } + #endif + + #if ENV_INCLUDE_INA3221 + if (INA3221_initialized) { + for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) { + // add only enabled INA3221 channels to telemetry + if (INA3221.isChannelEnabled(i)) { + float voltage = INA3221.getBusVoltage(i); + float current = INA3221.getCurrentAmps(i); + telemetry.addVoltage(next_available_channel, voltage); + telemetry.addCurrent(next_available_channel, current); + telemetry.addPower(next_available_channel, voltage * current); + next_available_channel++; + } + } + } + #endif + + #if ENV_INCLUDE_INA219 + if (INA219_initialized) { + telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V()); + telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000); + telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000); + next_available_channel++; + } + #endif + + } + + return true; +} + + +int EnvironmentSensorManager::getNumSettings() const { + #if ENV_INCLUDE_GPS + return gps_detected ? 1 : 0; // only show GPS setting if GPS is detected + #else + return 0; + #endif +} + +const char* EnvironmentSensorManager::getSettingName(int i) const { + #if ENV_INCLUDE_GPS + return (gps_detected && i == 0) ? "gps" : NULL; + #else + return NULL; + #endif +} + +const char* EnvironmentSensorManager::getSettingValue(int i) const { + #if ENV_INCLUDE_GPS + if (gps_detected && i == 0) { + return gps_active ? "1" : "0"; + } + #endif + return NULL; +} + +bool EnvironmentSensorManager::setSettingValue(const char* name, const char* value) { + #if ENV_INCLUDE_GPS + if (gps_detected && strcmp(name, "gps") == 0) { + if (strcmp(value, "0") == 0) { + stop_gps(); + } else { + start_gps(); + } + return true; + } + #endif + return false; // not supported +} + +#if ENV_INCLUDE_GPS +void EnvironmentSensorManager::initBasicGPS() { + Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); + Serial1.begin(9600); + + // Try to detect if GPS is physically connected to determine if we should expose the setting + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, HIGH); // Power on GPS + + // Give GPS a moment to power up and send data + delay(1000); + + // We'll consider GPS detected if we see any data on Serial1 + gps_detected = (Serial1.available() > 0); + + if (gps_detected) { + MESH_DEBUG_PRINTLN("GPS detected"); + digitalWrite(PIN_GPS_EN, LOW); // Power off GPS until the setting is changed + } else { + MESH_DEBUG_PRINTLN("No GPS detected"); + digitalWrite(PIN_GPS_EN, LOW); + } +} + +void EnvironmentSensorManager::start_gps() { + gps_active = true; + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, HIGH); +} + +void EnvironmentSensorManager::stop_gps() { + gps_active = false; + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); +} + +void EnvironmentSensorManager::loop() { + static long next_gps_update = 0; + + _location->loop(); + + if (millis() > next_gps_update) { + if (_location->isValid()) { + node_lat = ((double)_location->getLatitude())/1000000.; + node_lon = ((double)_location->getLongitude())/1000000.; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + } + next_gps_update = millis() + 1000; + } +} +#endif \ No newline at end of file diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h new file mode 100644 index 00000000..c9ec6870 --- /dev/null +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +class EnvironmentSensorManager : public SensorManager { +protected: + int next_available_channel = TELEM_CHANNEL_SELF + 1; + + bool AHTX0_initialized = false; + bool BME280_initialized = false; + bool INA3221_initialized = false; + bool INA219_initialized = false; + + bool gps_detected = false; + bool gps_active = false; + + #if ENV_INCLUDE_GPS + LocationProvider* _location; + void start_gps(); + void stop_gps(); + void initBasicGPS(); + #endif + + +public: + #if ENV_INCLUDE_GPS + EnvironmentSensorManager(LocationProvider &location): _location(&location){}; + #else + EnvironmentSensorManager(){}; + #endif + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + #if ENV_INCLUDE_GPS + void loop() override; + #endif + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; +}; diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h new file mode 100644 index 00000000..056e61e0 --- /dev/null +++ b/src/helpers/sensors/LocationProvider.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Mesh.h" + + +class LocationProvider { + +public: + virtual long getLatitude() = 0; + virtual long getLongitude() = 0; + virtual long getAltitude() = 0; + virtual bool isValid() = 0; + virtual long getTimestamp() = 0; + virtual void reset(); + virtual void begin(); + virtual void stop(); + virtual void loop(); +}; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h new file mode 100644 index 00000000..9f439e25 --- /dev/null +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -0,0 +1,85 @@ +#pragma once + +#include "LocationProvider.h" +#include +#include + +#ifndef GPS_EN +#define GPS_EN (-1) +#endif + +#ifndef GPS_RESET +#define GPS_RESET (-1) +#endif + +#ifndef GPS_RESET_FORCE +#define GPS_RESET_FORCE LOW +#endif + +class MicroNMEALocationProvider : public LocationProvider { + char _nmeaBuffer[100]; + MicroNMEA nmea; + Stream* _gps_serial; + int _pin_reset; + int _pin_en; + +public : + MicroNMEALocationProvider(Stream& ser, int pin_reset = GPS_RESET, int pin_en = GPS_EN) : + _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en) { + if (_pin_reset != -1) { + pinMode(_pin_reset, OUTPUT); + digitalWrite(_pin_reset, GPS_RESET_FORCE); + } + if (_pin_en != -1) { + pinMode(_pin_en, OUTPUT); + digitalWrite(_pin_en, LOW); + } + } + + void begin() override { + if (_pin_reset != -1) { + digitalWrite(_pin_reset, !GPS_RESET_FORCE); + } + if (_pin_en != -1) { + digitalWrite(_pin_en, HIGH); + } + } + + void reset() override { + if (_pin_reset != -1) { + digitalWrite(_pin_reset, GPS_RESET_FORCE); + delay(100); + digitalWrite(_pin_reset, !GPS_RESET_FORCE); + } + } + + void stop() override { + if (_pin_en != -1) { + digitalWrite(_pin_en, LOW); + } + } + + long getLatitude() override { return nmea.getLatitude(); } + long getLongitude() override { return nmea.getLongitude(); } + long getAltitude() override { + long alt = 0; + nmea.getAltitude(alt); + return alt; + } + bool isValid() override { return nmea.isValid(); } + + long getTimestamp() override { + DateTime dt(nmea.getYear(), nmea.getMonth(),nmea.getDay(),nmea.getHour(),nmea.getMinute(),nmea.getSecond()); + return dt.unixtime(); + } + + void loop() override { + while (_gps_serial->available()) { + char c = _gps_serial->read(); + #ifdef GPS_NMEA_DEBUG + Serial.print(c); + #endif + nmea.process(c); + } + } +}; \ No newline at end of file diff --git a/src/helpers/stm32/InternalFileSystem.cpp b/src/helpers/stm32/InternalFileSystem.cpp new file mode 100644 index 00000000..2714ec6b --- /dev/null +++ b/src/helpers/stm32/InternalFileSystem.cpp @@ -0,0 +1,142 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 hathach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include "InternalFileSystem.h" + +//--------------------------------------------------------------------+ +// LFS Disk IO +//--------------------------------------------------------------------+ +static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) +{ + if (!buffer || !size) return LFS_ERR_INVAL; + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * FLASH_PAGE_SIZE + off); + memcpy(buffer, (void *)address, size); + return LFS_ERR_OK; +} + +// Program a region in a block. The block must have previously +// been erased. Negative error codes are propogated to the user. +// May return LFS_ERR_CORRUPT if the block should be considered bad. +static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) +{ + HAL_StatusTypeDef hal_rc = HAL_OK; + lfs_block_t addr = LFS_FLASH_ADDR_BASE + (block * FLASH_PAGE_SIZE + off); + uint64_t *bufp = (uint64_t *) buffer; + + if (HAL_FLASH_Unlock() != HAL_OK) return LFS_ERR_IO; + for (uint32_t i = 0; i < size/8; i++) { + if ((addr < LFS_FLASH_ADDR_BASE) || (addr > FLASH_END_ADDR)) { + HAL_FLASH_Lock(); + return LFS_ERR_INVAL; + } + hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, *bufp); + addr += 8; + bufp += 1; + } + if (HAL_FLASH_Lock() != HAL_OK) return LFS_ERR_IO; + + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; +} + +// Erase a block. A block must be erased before being programmed. +// The state of an erased block is undefined. Negative error codes +// are propogated to the user. +// May return LFS_ERR_CORRUPT if the block should be considered bad. +static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) +{ + HAL_StatusTypeDef hal_rc; + lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * FLASH_PAGE_SIZE); + uint32_t pageError = 0; + FLASH_EraseInitTypeDef EraseInitStruct = { + .TypeErase = FLASH_TYPEERASE_PAGES, + .Page = 0, + .NbPages = 1 + }; + + if ((address < LFS_FLASH_ADDR_BASE) || (address > FLASH_END_ADDR)) { + return LFS_ERR_INVAL; + } + EraseInitStruct.Page = (address - FLASH_BASE) / FLASH_PAGE_SIZE; + HAL_FLASH_Unlock(); + hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &pageError); + HAL_FLASH_Lock(); + + return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; +} + +// Sync the state of the underlying block device. Negative error codes +// are propogated to the user. +static int _internal_flash_sync(const struct lfs_config *c) +{ + return LFS_ERR_OK; // don't need sync +} + +struct lfs_config _InternalFSConfig = { + .context = NULL, + .read = _internal_flash_read, + .prog = _internal_flash_prog, + .erase = _internal_flash_erase, + .sync = _internal_flash_sync, + + .read_size = LFS_BLOCK_SIZE, + .prog_size = LFS_BLOCK_SIZE, + .block_size = LFS_BLOCK_SIZE, + .block_count = LFS_FLASH_TOTAL_SIZE / LFS_BLOCK_SIZE, + .lookahead = 128, + + .read_buffer = NULL, + .prog_buffer = NULL, + .lookahead_buffer = NULL, + .file_buffer = NULL +}; + +InternalFileSystem InternalFS; + +//--------------------------------------------------------------------+ +// +//--------------------------------------------------------------------+ + +InternalFileSystem::InternalFileSystem(void) + : Adafruit_LittleFS(&_InternalFSConfig) +{ + +} + +bool InternalFileSystem::begin(void) +{ + #ifdef FORMAT_FS + this->format(); + #endif + // failed to mount, erase all sector then format and mount again + if ( !Adafruit_LittleFS::begin() ) + { + // lfs format + this->format(); + // mount again if still failed, give up + if ( !Adafruit_LittleFS::begin() ) return false; + } + + return true; +} diff --git a/src/helpers/stm32/InternalFileSystem.h b/src/helpers/stm32/InternalFileSystem.h new file mode 100644 index 00000000..1e3a4b4f --- /dev/null +++ b/src/helpers/stm32/InternalFileSystem.h @@ -0,0 +1,48 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 hathach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + + #ifndef INTERNALFILESYSTEM_H_ + #define INTERNALFILESYSTEM_H_ + + #include "Adafruit_LittleFS.h" + +#ifndef LFS_FLASH_TOTAL_SIZE /* Flash size can be configured in platformio.ini */ + #define LFS_FLASH_TOTAL_SIZE (16 * 2048) /* defaults to 32k flash */ +#endif +#define LFS_BLOCK_SIZE (2048) +#define LFS_FLASH_ADDR_BASE (FLASH_END_ADDR - LFS_FLASH_TOTAL_SIZE + 1) + + class InternalFileSystem : public Adafruit_LittleFS + { + public: + InternalFileSystem(void); + + // overwrite to also perform low level format (sector erase of whole flash region) + bool begin(void); + }; + + extern InternalFileSystem InternalFS; + + #endif /* INTERNALFILESYSTEM_H_ */ + \ No newline at end of file diff --git a/src/helpers/stm32/STM32Board.h b/src/helpers/stm32/STM32Board.h new file mode 100644 index 00000000..21cbdcdb --- /dev/null +++ b/src/helpers/stm32/STM32Board.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +class STM32Board : public mesh::MainBoard { +protected: + uint8_t startup_reason; + +public: + void begin() { + startup_reason = BD_STARTUP_NORMAL; + } + + uint8_t getStartupReason() const override { return startup_reason; } + + uint16_t getBattMilliVolts() override { + return 0; // not supported + } + + const char* getManufacturerName() const override { + return "Generic STM32"; + } + + void reboot() override { + } + + bool startOTAUpdate(const char* id, char reply[]) override { return false; }; +}; \ No newline at end of file diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 57aed85c..2d8b69c1 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -24,5 +24,6 @@ public: virtual void fillRect(int x, int y, int w, int h) = 0; virtual void drawRect(int x, int y, int w, int h) = 0; virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0; + virtual uint16_t getTextWidth(const char* str) = 0; virtual void endFrame() = 0; }; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index e7b70b49..7faaf2c3 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -1,11 +1,23 @@ #include "GxEPDDisplay.h" +#ifndef DISPLAY_ROTATION + #define DISPLAY_ROTATION 3 +#endif + +#ifdef TECHO_ZOOM + #define SCALE_X (1.5625f * 1.5f) // 200 / 128 (with 1.5 scale) + #define SCALE_Y (1.5625f * 1.5f) // 200 / 128 (with 1.5 scale) +#else + #define SCALE_X 1.5625f // 200 / 128 + #define SCALE_Y 1.5625f // 200 / 128 +#endif + bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); SPI1.begin(); display.init(115200, true, 2, false); - display.setRotation(3); + display.setRotation(DISPLAY_ROTATION); #ifdef TECHO_ZOOM display.setFont(&FreeMono9pt7b); #endif @@ -53,11 +65,7 @@ void GxEPDDisplay::setColor(Color c) { } void GxEPDDisplay::setCursor(int x, int y) { -#ifdef TECHO_ZOOM - x = x + (x >> 1); - y = y + (y >> 1); -#endif - display.setCursor(x, (y+10)); + display.setCursor(x*SCALE_X, (y+10)*SCALE_Y); } void GxEPDDisplay::print(const char* str) { @@ -65,33 +73,22 @@ void GxEPDDisplay::print(const char* str) { } void GxEPDDisplay::fillRect(int x, int y, int w, int h) { -#ifdef TECHO_ZOOM - x = x + (x >> 1); - y = y + (y >> 1); - w = w + (w >> 1); - h = h + (h >> 1); -#endif - display.fillRect(x, y, w, h, GxEPD_BLACK); + display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, GxEPD_BLACK); } void GxEPDDisplay::drawRect(int x, int y, int w, int h) { -#ifdef TECHO_ZOOM - x = x + (x >> 1); - y = y + (y >> 1); - w = w + (w >> 1); - h = h + (h >> 1); -#endif - display.drawRect(x, y, w, h, GxEPD_BLACK); + display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, GxEPD_BLACK); } void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { -#ifdef TECHO_ZOOM - x = x + (x >> 1); - y = y + (y >> 1); - w = w + (w >> 1); - h = h + (h >> 1); -#endif - display.drawBitmap(x*1.5, (y*1.5) + 10, bits, w, h, GxEPD_BLACK); + display.drawBitmap(x*SCALE_X, (y*SCALE_Y) + 10, bits, w, h, GxEPD_BLACK); +} + +uint16_t GxEPDDisplay::getTextWidth(const char* str) { + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w / SCALE_X; } void GxEPDDisplay::endFrame() { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index ecadc50c..01c900ac 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -14,11 +14,11 @@ #define GxEPD2_DISPLAY_CLASS GxEPD2_BW #define GxEPD2_DRIVER_CLASS GxEPD2_150_BN // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1 -#include // 1.54" b/w +#include // 1.54" b/w #include "DisplayDriver.h" -//GxEPD2_BW display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)); // DEPG0150BN 200x200, SSD1681, TTGO T5 V2.4.1 +//GxEPD2_BW display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)); // DEPG0150BN 200x200, SSD1681, TTGO T5 V2.4.1 class GxEPDDisplay : public DisplayDriver { @@ -29,13 +29,12 @@ class GxEPDDisplay : public DisplayDriver { public: // there is a margin in y... - GxEPDDisplay() : DisplayDriver(200, 200-10), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) { - + GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) { } bool begin(); - bool isOn() override {return _isOn;}; + bool isOn() override {return _isOn;}; void turnOn() override; void turnOff() override; void clear() override; @@ -47,5 +46,6 @@ public: void fillRect(int x, int y, int w, int h) override; void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; void endFrame() override; }; diff --git a/src/helpers/ui/OLEDDisplay.cpp b/src/helpers/ui/OLEDDisplay.cpp new file mode 100644 index 00000000..dca2ad1b --- /dev/null +++ b/src/helpers/ui/OLEDDisplay.cpp @@ -0,0 +1,1176 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2019 by Helmut Tschemernjak - www.radioshuttle.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + + /* + * TODO Helmut + * - test/finish dislplay.printf() on mbed-os + * - Finish _putc with drawLogBuffer when running display + */ + +#include "OLEDDisplay.h" + +OLEDDisplay::OLEDDisplay() { + + displayWidth = 128; + displayHeight = 64; + displayBufferSize = displayWidth * displayHeight / 8; + color = WHITE; + geometry = GEOMETRY_128_64; + textAlignment = TEXT_ALIGN_LEFT; + fontData = ArialMT_Plain_10; + fontTableLookupFunction = DefaultFontTableLookup; + buffer = NULL; +#ifdef OLEDDISPLAY_DOUBLE_BUFFER + buffer_back = NULL; +#endif +} + +OLEDDisplay::~OLEDDisplay() { + end(); +} + +bool OLEDDisplay::allocateBuffer() { + + logBufferSize = 0; + logBufferFilled = 0; + logBufferLine = 0; + logBufferMaxLines = 0; + logBuffer = NULL; + + if (!connect()) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Can't establish connection to display\n"); + return false; + } + + if(this->buffer==NULL) { + this->buffer = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + BufferOffset); + this->buffer += BufferOffset; + + if(!this->buffer) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not enough memory to create display\n"); + return false; + } + } + + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + if(this->buffer_back==NULL) { + this->buffer_back = (uint8_t*) malloc((sizeof(uint8_t) * displayBufferSize) + BufferOffset); + this->buffer_back += BufferOffset; + + if(!this->buffer_back) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][init] Not enough memory to create back buffer\n"); + free(this->buffer - BufferOffset); + return false; + } + } + #endif + + return true; +} + +bool OLEDDisplay::init() { + + BufferOffset = getBufferOffset(); + + if(!allocateBuffer()) { + return false; + } + + sendInitCommands(); + resetDisplay(); + + return true; +} + +void OLEDDisplay::end() { + if (this->buffer) { free(this->buffer - BufferOffset); this->buffer = NULL; } + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + if (this->buffer_back) { free(this->buffer_back - BufferOffset); this->buffer_back = NULL; } + #endif + if (this->logBuffer != NULL) { free(this->logBuffer); this->logBuffer = NULL; } +} + +void OLEDDisplay::resetDisplay(void) { + clear(); + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + memset(buffer_back, 1, displayBufferSize); + #endif + display(); +} + +void OLEDDisplay::setColor(OLEDDISPLAY_COLOR color) { + this->color = color; +} + +OLEDDISPLAY_COLOR OLEDDisplay::getColor() { + return this->color; +} + +void OLEDDisplay::setPixel(int16_t x, int16_t y) { + if (x >= 0 && x < this->width() && y >= 0 && y < this->height()) { + switch (color) { + case WHITE: buffer[x + (y / 8) * this->width()] |= (1 << (y & 7)); break; + case BLACK: buffer[x + (y / 8) * this->width()] &= ~(1 << (y & 7)); break; + case INVERSE: buffer[x + (y / 8) * this->width()] ^= (1 << (y & 7)); break; + } + } +} + +void OLEDDisplay::setPixelColor(int16_t x, int16_t y, OLEDDISPLAY_COLOR color) { + if (x >= 0 && x < this->width() && y >= 0 && y < this->height()) { + switch (color) { + case WHITE: buffer[x + (y / 8) * this->width()] |= (1 << (y & 7)); break; + case BLACK: buffer[x + (y / 8) * this->width()] &= ~(1 << (y & 7)); break; + case INVERSE: buffer[x + (y / 8) * this->width()] ^= (1 << (y & 7)); break; + } + } +} + +void OLEDDisplay::clearPixel(int16_t x, int16_t y) { + if (x >= 0 && x < this->width() && y >= 0 && y < this->height()) { + switch (color) { + case BLACK: buffer[x + (y >> 3) * this->width()] |= (1 << (y & 7)); break; + case WHITE: buffer[x + (y >> 3) * this->width()] &= ~(1 << (y & 7)); break; + case INVERSE: buffer[x + (y >> 3) * this->width()] ^= (1 << (y & 7)); break; + } + } +} + + +// Bresenham's algorithm - thx wikipedia and Adafruit_GFX +void OLEDDisplay::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1) { + int16_t steep = abs(y1 - y0) > abs(x1 - x0); + if (steep) { + _swap_int16_t(x0, y0); + _swap_int16_t(x1, y1); + } + + if (x0 > x1) { + _swap_int16_t(x0, x1); + _swap_int16_t(y0, y1); + } + + int16_t dx, dy; + dx = x1 - x0; + dy = abs(y1 - y0); + + int16_t err = dx / 2; + int16_t ystep; + + if (y0 < y1) { + ystep = 1; + } else { + ystep = -1; + } + + for (; x0<=x1; x0++) { + if (steep) { + setPixel(y0, x0); + } else { + setPixel(x0, y0); + } + err -= dy; + if (err < 0) { + y0 += ystep; + err += dx; + } + } +} + +void OLEDDisplay::drawRect(int16_t x, int16_t y, int16_t width, int16_t height) { + drawHorizontalLine(x, y, width); + drawVerticalLine(x, y, height); + drawVerticalLine(x + width - 1, y, height); + drawHorizontalLine(x, y + height - 1, width); +} + +void OLEDDisplay::fillRect(int16_t xMove, int16_t yMove, int16_t width, int16_t height) { + for (int16_t x = xMove; x < xMove + width; x++) { + drawVerticalLine(x, yMove, height); + } +} + +void OLEDDisplay::drawCircle(int16_t x0, int16_t y0, int16_t radius) { + int16_t x = 0, y = radius; + int16_t dp = 1 - radius; + do { + if (dp < 0) + dp = dp + (x++) * 2 + 3; + else + dp = dp + (x++) * 2 - (y--) * 2 + 5; + + setPixel(x0 + x, y0 + y); //For the 8 octants + setPixel(x0 - x, y0 + y); + setPixel(x0 + x, y0 - y); + setPixel(x0 - x, y0 - y); + setPixel(x0 + y, y0 + x); + setPixel(x0 - y, y0 + x); + setPixel(x0 + y, y0 - x); + setPixel(x0 - y, y0 - x); + + } while (x < y); + + setPixel(x0 + radius, y0); + setPixel(x0, y0 + radius); + setPixel(x0 - radius, y0); + setPixel(x0, y0 - radius); +} + +void OLEDDisplay::drawCircleQuads(int16_t x0, int16_t y0, int16_t radius, uint8_t quads) { + int16_t x = 0, y = radius; + int16_t dp = 1 - radius; + while (x < y) { + if (dp < 0) + dp = dp + (x++) * 2 + 3; + else + dp = dp + (x++) * 2 - (y--) * 2 + 5; + if (quads & 0x1) { + setPixel(x0 + x, y0 - y); + setPixel(x0 + y, y0 - x); + } + if (quads & 0x2) { + setPixel(x0 - y, y0 - x); + setPixel(x0 - x, y0 - y); + } + if (quads & 0x4) { + setPixel(x0 - y, y0 + x); + setPixel(x0 - x, y0 + y); + } + if (quads & 0x8) { + setPixel(x0 + x, y0 + y); + setPixel(x0 + y, y0 + x); + } + } + if (quads & 0x1 && quads & 0x8) { + setPixel(x0 + radius, y0); + } + if (quads & 0x4 && quads & 0x8) { + setPixel(x0, y0 + radius); + } + if (quads & 0x2 && quads & 0x4) { + setPixel(x0 - radius, y0); + } + if (quads & 0x1 && quads & 0x2) { + setPixel(x0, y0 - radius); + } +} + + +void OLEDDisplay::fillCircle(int16_t x0, int16_t y0, int16_t radius) { + int16_t x = 0, y = radius; + int16_t dp = 1 - radius; + do { + if (dp < 0) + dp = dp + (x++) * 2 + 3; + else + dp = dp + (x++) * 2 - (y--) * 2 + 5; + + drawHorizontalLine(x0 - x, y0 - y, 2*x); + drawHorizontalLine(x0 - x, y0 + y, 2*x); + drawHorizontalLine(x0 - y, y0 - x, 2*y); + drawHorizontalLine(x0 - y, y0 + x, 2*y); + + + } while (x < y); + drawHorizontalLine(x0 - radius, y0, 2 * radius); + +} + +void OLEDDisplay::drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, + int16_t x2, int16_t y2) { + drawLine(x0, y0, x1, y1); + drawLine(x1, y1, x2, y2); + drawLine(x2, y2, x0, y0); +} + +void OLEDDisplay::fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, + int16_t x2, int16_t y2) { + int16_t a, b, y, last; + + if (y0 > y1) { + _swap_int16_t(y0, y1); + _swap_int16_t(x0, x1); + } + if (y1 > y2) { + _swap_int16_t(y2, y1); + _swap_int16_t(x2, x1); + } + if (y0 > y1) { + _swap_int16_t(y0, y1); + _swap_int16_t(x0, x1); + } + + if (y0 == y2) { + a = b = x0; + if (x1 < a) { + a = x1; + } else if (x1 > b) { + b = x1; + } + if (x2 < a) { + a = x2; + } else if (x2 > b) { + b = x2; + } + drawHorizontalLine(a, y0, b - a + 1); + return; + } + + int16_t + dx01 = x1 - x0, + dy01 = y1 - y0, + dx02 = x2 - x0, + dy02 = y2 - y0, + dx12 = x2 - x1, + dy12 = y2 - y1; + int32_t + sa = 0, + sb = 0; + + if (y1 == y2) { + last = y1; // Include y1 scanline + } else { + last = y1 - 1; // Skip it + } + + for (y = y0; y <= last; y++) { + a = x0 + sa / dy01; + b = x0 + sb / dy02; + sa += dx01; + sb += dx02; + + if (a > b) { + _swap_int16_t(a, b); + } + drawHorizontalLine(a, y, b - a + 1); + } + + sa = dx12 * (y - y1); + sb = dx02 * (y - y0); + for (; y <= y2; y++) { + a = x1 + sa / dy12; + b = x0 + sb / dy02; + sa += dx12; + sb += dx02; + + if (a > b) { + _swap_int16_t(a, b); + } + drawHorizontalLine(a, y, b - a + 1); + } +} + +void OLEDDisplay::drawHorizontalLine(int16_t x, int16_t y, int16_t length) { + if (y < 0 || y >= this->height()) { return; } + + if (x < 0) { + length += x; + x = 0; + } + + if ( (x + length) > this->width()) { + length = (this->width() - x); + } + + if (length <= 0) { return; } + + uint8_t * bufferPtr = buffer; + bufferPtr += (y >> 3) * this->width(); + bufferPtr += x; + + uint8_t drawBit = 1 << (y & 7); + + switch (color) { + case WHITE: while (length--) { + *bufferPtr++ |= drawBit; + }; break; + case BLACK: drawBit = ~drawBit; while (length--) { + *bufferPtr++ &= drawBit; + }; break; + case INVERSE: while (length--) { + *bufferPtr++ ^= drawBit; + }; break; + } +} + +void OLEDDisplay::drawVerticalLine(int16_t x, int16_t y, int16_t length) { + if (x < 0 || x >= this->width()) return; + + if (y < 0) { + length += y; + y = 0; + } + + if ( (y + length) > this->height()) { + length = (this->height() - y); + } + + if (length <= 0) return; + + + uint8_t yOffset = y & 7; + uint8_t drawBit; + uint8_t *bufferPtr = buffer; + + bufferPtr += (y >> 3) * this->width(); + bufferPtr += x; + + if (yOffset) { + yOffset = 8 - yOffset; + drawBit = ~(0xFF >> (yOffset)); + + if (length < yOffset) { + drawBit &= (0xFF >> (yOffset - length)); + } + + switch (color) { + case WHITE: *bufferPtr |= drawBit; break; + case BLACK: *bufferPtr &= ~drawBit; break; + case INVERSE: *bufferPtr ^= drawBit; break; + } + + if (length < yOffset) return; + + length -= yOffset; + bufferPtr += this->width(); + } + + if (length >= 8) { + switch (color) { + case WHITE: + case BLACK: + drawBit = (color == WHITE) ? 0xFF : 0x00; + do { + *bufferPtr = drawBit; + bufferPtr += this->width(); + length -= 8; + } while (length >= 8); + break; + case INVERSE: + do { + *bufferPtr = ~(*bufferPtr); + bufferPtr += this->width(); + length -= 8; + } while (length >= 8); + break; + } + } + + if (length > 0) { + drawBit = (1 << (length & 7)) - 1; + switch (color) { + case WHITE: *bufferPtr |= drawBit; break; + case BLACK: *bufferPtr &= ~drawBit; break; + case INVERSE: *bufferPtr ^= drawBit; break; + } + } +} + +void OLEDDisplay::drawProgressBar(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t progress) { + uint16_t radius = height / 2; + uint16_t xRadius = x + radius; + uint16_t yRadius = y + radius; + uint16_t doubleRadius = 2 * radius; + uint16_t innerRadius = radius - 2; + + setColor(WHITE); + drawCircleQuads(xRadius, yRadius, radius, 0b00000110); + drawHorizontalLine(xRadius, y, width - doubleRadius + 1); + drawHorizontalLine(xRadius, y + height, width - doubleRadius + 1); + drawCircleQuads(x + width - radius, yRadius, radius, 0b00001001); + + uint16_t maxProgressWidth = (width - doubleRadius + 1) * progress / 100; + + fillCircle(xRadius, yRadius, innerRadius); + fillRect(xRadius + 1, y + 2, maxProgressWidth, height - 3); + fillCircle(xRadius + maxProgressWidth, yRadius, innerRadius); +} + +void OLEDDisplay::drawFastImage(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *image) { + drawInternal(xMove, yMove, width, height, image, 0, 0); +} + +void OLEDDisplay::drawXbm(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *xbm) { + int16_t widthInXbm = (width + 7) / 8; + uint8_t data = 0; + + for(int16_t y = 0; y < height; y++) { + for(int16_t x = 0; x < width; x++ ) { + if (x & 7) { + data >>= 1; // Move a bit + } else { // Read new data every 8 bit + data = pgm_read_byte(xbm + (x / 8) + y * widthInXbm); + } + // if there is a bit draw it + if (data & 0x01) { + setPixel(xMove + x, yMove + y); + } + } + } +} + +void OLEDDisplay::drawIco16x16(int16_t xMove, int16_t yMove, const uint8_t *ico, bool inverse) { + uint16_t data; + + for(int16_t y = 0; y < 16; y++) { + data = pgm_read_byte(ico + (y << 1)) + (pgm_read_byte(ico + (y << 1) + 1) << 8); + for(int16_t x = 0; x < 16; x++ ) { + if ((data & 0x01) ^ inverse) { + setPixelColor(xMove + x, yMove + y, WHITE); + } else { + setPixelColor(xMove + x, yMove + y, BLACK); + } + data >>= 1; // Move a bit + } + } +} + +uint16_t OLEDDisplay::drawStringInternal(int16_t xMove, int16_t yMove, const char* text, uint16_t textLength, uint16_t textWidth, bool utf8) { + uint8_t textHeight = pgm_read_byte(fontData + HEIGHT_POS); + uint8_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + uint16_t sizeOfJumpTable = pgm_read_byte(fontData + CHAR_NUM_POS) * JUMPTABLE_BYTES; + + uint16_t cursorX = 0; + uint16_t cursorY = 0; + uint16_t charCount = 0; + + switch (textAlignment) { + case TEXT_ALIGN_CENTER_BOTH: + yMove -= textHeight >> 1; + // Fallthrough + case TEXT_ALIGN_CENTER: + xMove -= textWidth >> 1; // divide by 2 + break; + case TEXT_ALIGN_RIGHT: + xMove -= textWidth; + break; + case TEXT_ALIGN_LEFT: + break; + } + + // Don't draw anything if it is not on the screen. + if (xMove + textWidth < 0 || xMove >= this->width() ) {return 0;} + if (yMove + textHeight < 0 || yMove >= this->height()) {return 0;} + + for (uint16_t j = 0; j < textLength; j++) { + int16_t xPos = xMove + cursorX; + int16_t yPos = yMove + cursorY; + if (xPos > this->width()) + break; // no need to continue + charCount++; + + uint8_t code; + if (utf8) { + code = (this->fontTableLookupFunction)(text[j]); + if (code == 0) + continue; + } else + code = text[j]; + if (code >= firstChar) { + uint8_t charCode = code - firstChar; + + // 4 Bytes per char code + uint8_t msbJumpToChar = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES ); // MSB \ JumpAddress + uint8_t lsbJumpToChar = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_LSB); // LSB / + uint8_t charByteSize = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_SIZE); // Size + uint8_t currentCharWidth = pgm_read_byte( fontData + JUMPTABLE_START + charCode * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); // Width + + // Test if the char is drawable + if (!(msbJumpToChar == 255 && lsbJumpToChar == 255)) { + // Get the position of the char data + uint16_t charDataPosition = JUMPTABLE_START + sizeOfJumpTable + ((msbJumpToChar << 8) + lsbJumpToChar); + drawInternal(xPos, yPos, currentCharWidth, textHeight, fontData, charDataPosition, charByteSize); + } + + cursorX += currentCharWidth; + } + } + return charCount; +} + + +uint16_t OLEDDisplay::drawString(int16_t xMove, int16_t yMove, const String &strUser) { + uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS); + + // char* text must be freed! + char* text = strdup(strUser.c_str()); + if (!text) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][drawString] Can't allocate char array.\n"); + return 0; + } + + uint16_t yOffset = 0; + // If the string should be centered vertically too + // we need to now how heigh the string is. + if (textAlignment == TEXT_ALIGN_CENTER_BOTH) { + uint16_t lb = 0; + // Find number of linebreaks in text + for (uint16_t i=0;text[i] != 0; i++) { + lb += (text[i] == 10); + } + // Calculate center + yOffset = (lb * lineHeight) / 2; + } + + uint16_t charDrawn = 0; + uint16_t line = 0; + char* textPart = strtok(text,"\n"); + while (textPart != NULL) { + uint16_t length = strlen(textPart); + charDrawn += drawStringInternal(xMove, yMove - yOffset + (line++) * lineHeight, textPart, length, getStringWidth(textPart, length, true), true); + textPart = strtok(NULL, "\n"); + } + free(text); + return charDrawn; +} + +void OLEDDisplay::drawStringf( int16_t x, int16_t y, char* buffer, String format, ... ) +{ + va_list myargs; + va_start(myargs, format); + vsprintf(buffer, format.c_str(), myargs); + va_end(myargs); + drawString( x, y, buffer ); +} + +uint16_t OLEDDisplay::drawStringMaxWidth(int16_t xMove, int16_t yMove, uint16_t maxLineWidth, const String &strUser) { + uint16_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS); + + const char* text = strUser.c_str(); + + uint16_t length = strlen(text); + uint16_t lastDrawnPos = 0; + uint16_t lineNumber = 0; + uint16_t strWidth = 0; + + uint16_t preferredBreakpoint = 0; + uint16_t widthAtBreakpoint = 0; + uint16_t firstLineChars = 0; + uint16_t drawStringResult = 1; // later tested for 0 == error, so initialize to 1 + + for (uint16_t i = 0; i < length; i++) { + char c = (this->fontTableLookupFunction)(text[i]); + if (c == 0) + continue; + strWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); + + // Always break on newline + if (text[i] == '\n') { + drawStringResult = drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], i - lastDrawnPos, strWidth, true); + if (firstLineChars == 0) + firstLineChars = i; + + lastDrawnPos = i + 1; + strWidth = 0; + if (drawStringResult == 0) // we are past the display already? + break; + } + + // Always try to break on a space, dash or slash + if (text[i] == ' ' || text[i]== '-' || text[i] == '/') { + preferredBreakpoint = i + 1; + widthAtBreakpoint = strWidth; + } + + if (strWidth >= maxLineWidth) { + if (preferredBreakpoint == 0) { + preferredBreakpoint = i; + widthAtBreakpoint = strWidth; + } + drawStringResult = drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], preferredBreakpoint - lastDrawnPos, widthAtBreakpoint, true); + if (firstLineChars == 0) + firstLineChars = preferredBreakpoint; + lastDrawnPos = preferredBreakpoint; + // It is possible that we did not draw all letters to i so we need + // to account for the width of the chars from `i - preferredBreakpoint` + // by calculating the width we did not draw yet. + strWidth = strWidth - widthAtBreakpoint; + preferredBreakpoint = 0; + if (drawStringResult == 0) // we are past the display already? + break; + } + } + + // Draw last part if needed + if (drawStringResult != 0 && lastDrawnPos < length) { + drawStringResult = drawStringInternal(xMove, yMove + (lineNumber++) * lineHeight , &text[lastDrawnPos], length - lastDrawnPos, getStringWidth(&text[lastDrawnPos], length - lastDrawnPos, true), true); + } + + if (drawStringResult == 0 || (yMove + lineNumber * lineHeight) >= this->height()) // text did not fit on screen + return firstLineChars; + return 0; // everything was drawn +} + +uint16_t OLEDDisplay::getStringWidth(const char* text, uint16_t length, bool utf8) { + uint16_t firstChar = pgm_read_byte(fontData + FIRST_CHAR_POS); + + uint16_t stringWidth = 0; + uint16_t maxWidth = 0; + + for (uint16_t i = 0; i < length; i++) { + char c = text[i]; + if (utf8) { + c = (this->fontTableLookupFunction)(c); + if (c == 0) + continue; + } + stringWidth += pgm_read_byte(fontData + JUMPTABLE_START + (c - firstChar) * JUMPTABLE_BYTES + JUMPTABLE_WIDTH); + if (c == 10) { + maxWidth = max(maxWidth, stringWidth); + stringWidth = 0; + } + } + + return max(maxWidth, stringWidth); +} + +uint16_t OLEDDisplay::getStringWidth(const String &strUser) { + uint16_t width = getStringWidth(strUser.c_str(), strUser.length()); + return width; +} + +void OLEDDisplay::setTextAlignment(OLEDDISPLAY_TEXT_ALIGNMENT textAlignment) { + this->textAlignment = textAlignment; +} + +void OLEDDisplay::setFont(const uint8_t *fontData) { + this->fontData = fontData; +} + +void OLEDDisplay::displayOn(void) { + sendCommand(DISPLAYON); +} + +void OLEDDisplay::displayOff(void) { + sendCommand(DISPLAYOFF); +} + +void OLEDDisplay::invertDisplay(void) { + sendCommand(INVERTDISPLAY); +} + +void OLEDDisplay::normalDisplay(void) { + sendCommand(NORMALDISPLAY); +} + +void OLEDDisplay::setContrast(uint8_t contrast, uint8_t precharge, uint8_t comdetect) { + sendCommand(SETPRECHARGE); //0xD9 + sendCommand(precharge); //0xF1 default, to lower the contrast, put 1-1F + sendCommand(SETCONTRAST); + sendCommand(contrast); // 0-255 + sendCommand(SETVCOMDETECT); //0xDB, (additionally needed to lower the contrast) + sendCommand(comdetect); //0x40 default, to lower the contrast, put 0 + sendCommand(DISPLAYALLON_RESUME); + sendCommand(NORMALDISPLAY); + sendCommand(DISPLAYON); +} + +void OLEDDisplay::setBrightness(uint8_t brightness) { + uint8_t contrast = brightness; + if (brightness < 128) { + // Magic values to get a smooth/ step-free transition + contrast = brightness * 1.171; + } else { + contrast = brightness * 1.171 - 43; + } + + uint8_t precharge = 241; + if (brightness == 0) { + precharge = 0; + } + uint8_t comdetect = brightness / 8; + + setContrast(contrast, precharge, comdetect); +} + +void OLEDDisplay::resetOrientation() { + sendCommand(SEGREMAP); + sendCommand(COMSCANINC); //Reset screen rotation or mirroring +} + +void OLEDDisplay::flipScreenVertically() { + sendCommand(SEGREMAP | 0x01); + sendCommand(COMSCANDEC); //Rotate screen 180 Deg +} + +void OLEDDisplay::mirrorScreen() { + sendCommand(SEGREMAP); + sendCommand(COMSCANDEC); //Mirror screen +} + +void OLEDDisplay::clear(void) { + memset(buffer, 0, displayBufferSize); +} + +void OLEDDisplay::drawLogBuffer(uint16_t xMove, uint16_t yMove) { + uint16_t lineHeight = pgm_read_byte(fontData + HEIGHT_POS); + // Always align left + setTextAlignment(TEXT_ALIGN_LEFT); + + // State values + uint16_t length = 0; + uint16_t line = 0; + uint16_t lastPos = 0; + + for (uint16_t i=0;ilogBufferFilled;i++){ + // Everytime we have a \n print + if (this->logBuffer[i] == 10) { + length++; + // Draw string on line `line` from lastPos to length + // Passing 0 as the lenght because we are in TEXT_ALIGN_LEFT + drawStringInternal(xMove, yMove + (line++) * lineHeight, &this->logBuffer[lastPos], length, 0, false); + // Remember last pos + lastPos = i; + // Reset length + length = 0; + } else { + // Count chars until next linebreak + length++; + } + } + // Draw the remaining string + if (length > 0) { + drawStringInternal(xMove, yMove + line * lineHeight, &this->logBuffer[lastPos], length, 0, false); + } +} + +uint16_t OLEDDisplay::getWidth(void) { + return displayWidth; +} + +uint16_t OLEDDisplay::getHeight(void) { + return displayHeight; +} + +bool OLEDDisplay::setLogBuffer(uint16_t lines, uint16_t chars){ + if (logBuffer != NULL) free(logBuffer); + uint16_t size = lines * chars; + if (size > 0) { + this->logBufferLine = 0; // Lines printed + this->logBufferFilled = 0; // Nothing stored yet + this->logBufferMaxLines = lines; // Lines max printable + this->logBufferSize = size; // Total number of characters the buffer can hold + this->logBuffer = (char *) malloc(size * sizeof(uint8_t)); + if(!this->logBuffer) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][setLogBuffer] Not enough memory to create log buffer\n"); + return false; + } + } + return true; +} + +size_t OLEDDisplay::write(uint8_t c) { + if (this->logBufferSize > 0) { + // Don't waste space on \r\n line endings, dropping \r + if (c == 13) return 1; + + // convert UTF-8 character to font table index + c = (this->fontTableLookupFunction)(c); + // drop unknown character + if (c == 0) return 1; + + bool maxLineNotReached = this->logBufferLine < this->logBufferMaxLines; + bool bufferNotFull = this->logBufferFilled < this->logBufferSize; + + // Can we write to the buffer? + if (bufferNotFull && maxLineNotReached) { + this->logBuffer[logBufferFilled] = c; + this->logBufferFilled++; + // Keep track of lines written + if (c == 10) this->logBufferLine++; + } else { + // Max line number is reached + if (!maxLineNotReached) this->logBufferLine--; + + // Find the end of the first line + uint16_t firstLineEnd = 0; + for (uint16_t i=0;ilogBufferFilled;i++) { + if (this->logBuffer[i] == 10){ + // Include last char too + firstLineEnd = i + 1; + break; + } + } + // If there was a line ending + if (firstLineEnd > 0) { + // Calculate the new logBufferFilled value + this->logBufferFilled = logBufferFilled - firstLineEnd; + // Now we move the lines infront of the buffer + memcpy(this->logBuffer, &this->logBuffer[firstLineEnd], logBufferFilled); + } else { + // Let's reuse the buffer if it was full + if (!bufferNotFull) { + this->logBufferFilled = 0; + }// else { + // Nothing to do here + //} + } + write(c); + } + } + // We are always writing all uint8_t to the buffer + return 1; +} + +size_t OLEDDisplay::write(const char* str) { + if (str == NULL) return 0; + size_t length = strlen(str); + for (size_t i = 0; i < length; i++) { + write(str[i]); + } + return length; +} + +#ifdef __MBED__ +int OLEDDisplay::_putc(int c) { + + if (!fontData) + return 1; + if (!logBufferSize) { + uint8_t textHeight = pgm_read_byte(fontData + HEIGHT_POS); + uint16_t lines = this->displayHeight / textHeight; + uint16_t chars = 2 * (this->displayWidth / textHeight); + + if (this->displayHeight % textHeight) + lines++; + if (this->displayWidth % textHeight) + chars++; + setLogBuffer(lines, chars); + } + + return this->write((uint8_t)c); +} +#endif + +// Private functions +void OLEDDisplay::setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width, uint16_t height) { + this->geometry = g; + + switch (g) { + case GEOMETRY_128_128: + this->displayWidth = 128; + this->displayHeight = 128; + break; + case GEOMETRY_128_64: + this->displayWidth = 128; + this->displayHeight = 64; + break; + case GEOMETRY_128_32: + this->displayWidth = 128; + this->displayHeight = 32; + break; + case GEOMETRY_64_48: + this->displayWidth = 64; + this->displayHeight = 48; + break; + case GEOMETRY_64_32: + this->displayWidth = 64; + this->displayHeight = 32; + break; + case GEOMETRY_RAWMODE: + this->displayWidth = width > 0 ? width : 128; + this->displayHeight = height > 0 ? height : 64; + break; + } + this->displayBufferSize = displayWidth * displayHeight / 8; +} + +void OLEDDisplay::sendInitCommands(void) { + if (geometry == GEOMETRY_RAWMODE) + return; + sendCommand(DISPLAYOFF); + sendCommand(SETDISPLAYCLOCKDIV); + sendCommand(0xF0); // Increase speed of the display max ~96Hz + sendCommand(SETMULTIPLEX); + sendCommand(this->height() - 1); + sendCommand(SETDISPLAYOFFSET); + sendCommand(0x00); + if(geometry == GEOMETRY_64_32) + sendCommand(0x00); + else + sendCommand(SETSTARTLINE); + sendCommand(CHARGEPUMP); + sendCommand(0x14); + sendCommand(MEMORYMODE); + sendCommand(0x00); + sendCommand(SEGREMAP); + sendCommand(COMSCANINC); + sendCommand(SETCOMPINS); + + if (geometry == GEOMETRY_128_128 || geometry == GEOMETRY_128_64 || geometry == GEOMETRY_64_48 || geometry == GEOMETRY_64_32) { + sendCommand(0x12); + } else if (geometry == GEOMETRY_128_32) { + sendCommand(0x02); + } + + sendCommand(SETCONTRAST); + + if (geometry == GEOMETRY_128_128 || geometry == GEOMETRY_128_64 || geometry == GEOMETRY_64_48 || geometry == GEOMETRY_64_32) { + sendCommand(0xCF); + } else if (geometry == GEOMETRY_128_32) { + sendCommand(0x8F); + } + + sendCommand(SETPRECHARGE); + sendCommand(0xF1); + sendCommand(SETVCOMDETECT); //0xDB, (additionally needed to lower the contrast) + sendCommand(0x40); //0x40 default, to lower the contrast, put 0 + sendCommand(DISPLAYALLON_RESUME); + sendCommand(NORMALDISPLAY); + sendCommand(0x2e); // stop scroll + sendCommand(DISPLAYON); +} + +void inline OLEDDisplay::drawInternal(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *data, uint16_t offset, uint16_t bytesInData) { + if (width < 0 || height < 0) return; + if (yMove + height < 0 || yMove > this->height()) return; + if (xMove + width < 0 || xMove > this->width()) return; + + uint8_t rasterHeight = 1 + ((height - 1) >> 3); // fast ceil(height / 8.0) + int8_t yOffset = yMove & 7; + + bytesInData = bytesInData == 0 ? width * rasterHeight : bytesInData; + + int16_t initYMove = yMove; + int8_t initYOffset = yOffset; + + + for (uint16_t i = 0; i < bytesInData; i++) { + + // Reset if next horizontal drawing phase is started. + if ( i % rasterHeight == 0) { + yMove = initYMove; + yOffset = initYOffset; + } + + uint8_t currentByte = pgm_read_byte(data + offset + i); + + int16_t xPos = xMove + (i / rasterHeight); + int16_t yPos = ((yMove >> 3) + (i % rasterHeight)) * this->width(); + +// int16_t yScreenPos = yMove + yOffset; + int16_t dataPos = xPos + yPos; + + if (dataPos >= 0 && dataPos < displayBufferSize && + xPos >= 0 && xPos < this->width() ) { + + if (yOffset >= 0) { + switch (this->color) { + case WHITE: buffer[dataPos] |= currentByte << yOffset; break; + case BLACK: buffer[dataPos] &= ~(currentByte << yOffset); break; + case INVERSE: buffer[dataPos] ^= currentByte << yOffset; break; + } + + if (dataPos < (displayBufferSize - this->width())) { + switch (this->color) { + case WHITE: buffer[dataPos + this->width()] |= currentByte >> (8 - yOffset); break; + case BLACK: buffer[dataPos + this->width()] &= ~(currentByte >> (8 - yOffset)); break; + case INVERSE: buffer[dataPos + this->width()] ^= currentByte >> (8 - yOffset); break; + } + } + } else { + // Make new offset position + yOffset = -yOffset; + + switch (this->color) { + case WHITE: buffer[dataPos] |= currentByte >> yOffset; break; + case BLACK: buffer[dataPos] &= ~(currentByte >> yOffset); break; + case INVERSE: buffer[dataPos] ^= currentByte >> yOffset; break; + } + + // Prepare for next iteration by moving one block up + yMove -= 8; + + // and setting the new yOffset + yOffset = 8 - yOffset; + } +#ifndef __MBED__ + yield(); +#endif + } + } +} + +// You need to free the char! +char* OLEDDisplay::utf8ascii(const String &str) { + uint16_t k = 0; + uint16_t length = str.length() + 1; + + // Copy the string into a char array + char* s = (char*) malloc(length * sizeof(char)); + if(!s) { + DEBUG_OLEDDISPLAY("[OLEDDISPLAY][utf8ascii] Can't allocate another char array. Drop support for UTF-8.\n"); + return (char*) str.c_str(); + } + str.toCharArray(s, length); + + length--; + + for (uint16_t i=0; i < length; i++) { + char c = (this->fontTableLookupFunction)(s[i]); + if (c!=0) { + s[k++]=c; + } + } + + s[k]=0; + + // This will leak 's' be sure to free it in the calling function. + return s; +} + +void OLEDDisplay::setFontTableLookupFunction(FontTableLookupFunction function) { + this->fontTableLookupFunction = function; +} + + +char DefaultFontTableLookup(const uint8_t ch) { + // UTF-8 to font table index converter + // Code form http://playground.arduino.cc/Main/Utf8ascii + static uint8_t LASTCHAR; + + if (ch < 128) { // Standard ASCII-set 0..0x7F handling + LASTCHAR = 0; + return ch; + } + + uint8_t last = LASTCHAR; // get last char + LASTCHAR = ch; + + switch (last) { // conversion depnding on first UTF8-character + case 0xC2: return (uint8_t) ch; + case 0xC3: return (uint8_t) (ch | 0xC0); + case 0x82: if (ch == 0xAC) return (uint8_t) 0x80; // special case Euro-symbol + } + + return (uint8_t) 0; // otherwise: return zero, if character has to be ignored +} diff --git a/src/helpers/ui/OLEDDisplay.h b/src/helpers/ui/OLEDDisplay.h new file mode 100644 index 00000000..23e68455 --- /dev/null +++ b/src/helpers/ui/OLEDDisplay.h @@ -0,0 +1,395 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2019 by Helmut Tschemernjak - www.radioshuttle.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef OLEDDISPLAY_h +#define OLEDDISPLAY_h + +#include + +#ifdef ARDUINO +#include +#elif __MBED__ +#define pgm_read_byte(addr) (*(const unsigned char *)(addr)) + +#include +#define delay(x) wait_ms(x) +#define yield() void() + +/* + * This is a little Arduino String emulation to keep the OLEDDisplay + * library code in common between Arduino and mbed-os + */ +class String { +public: + String(const char *s) { _str = s; }; + int length() { return strlen(_str); }; + const char *c_str() { return _str; }; + void toCharArray(char *buf, unsigned int bufsize, unsigned int index = 0) const { + memcpy(buf, _str + index, std::min(bufsize, strlen(_str))); + }; +private: + const char *_str; +}; + +#else +#error "Unkown operating system" +#endif + +#include "OLEDDisplayFonts.h" + +//#define DEBUG_OLEDDISPLAY(...) Serial.printf( __VA_ARGS__ ) +//#define DEBUG_OLEDDISPLAY(...) dprintf("%s", __VA_ARGS__ ) + +#ifndef DEBUG_OLEDDISPLAY +#define DEBUG_OLEDDISPLAY(...) +#endif + +// Use DOUBLE BUFFERING by default +#ifndef OLEDDISPLAY_REDUCE_MEMORY +#define OLEDDISPLAY_DOUBLE_BUFFER +#endif + +// Header Values +#define JUMPTABLE_BYTES 4 + +#define JUMPTABLE_LSB 1 +#define JUMPTABLE_SIZE 2 +#define JUMPTABLE_WIDTH 3 +#define JUMPTABLE_START 4 + +#define WIDTH_POS 0 +#define HEIGHT_POS 1 +#define FIRST_CHAR_POS 2 +#define CHAR_NUM_POS 3 + + +// Display commands +#define CHARGEPUMP 0x8D +#define COLUMNADDR 0x21 +#define COMSCANDEC 0xC8 +#define COMSCANINC 0xC0 +#define DISPLAYALLON 0xA5 +#define DISPLAYALLON_RESUME 0xA4 +#define DISPLAYOFF 0xAE +#define DISPLAYON 0xAF +#define EXTERNALVCC 0x1 +#define INVERTDISPLAY 0xA7 +#define MEMORYMODE 0x20 +#define NORMALDISPLAY 0xA6 +#define PAGEADDR 0x22 +#define SEGREMAP 0xA0 +#define SETCOMPINS 0xDA +#define SETCONTRAST 0x81 +#define SETDISPLAYCLOCKDIV 0xD5 +#define SETDISPLAYOFFSET 0xD3 +#define SETHIGHCOLUMN 0x10 +#define SETLOWCOLUMN 0x00 +#define SETMULTIPLEX 0xA8 +#define SETPRECHARGE 0xD9 +#define SETSEGMENTREMAP 0xA1 +#define SETSTARTLINE 0x40 +#define SETVCOMDETECT 0xDB +#define SWITCHCAPVCC 0x2 + +#ifndef _swap_int16_t +#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; } +#endif + +enum OLEDDISPLAY_COLOR { + BLACK = 0, + WHITE = 1, + INVERSE = 2 +}; + +enum OLEDDISPLAY_TEXT_ALIGNMENT { + TEXT_ALIGN_LEFT = 0, + TEXT_ALIGN_RIGHT = 1, + TEXT_ALIGN_CENTER = 2, + TEXT_ALIGN_CENTER_BOTH = 3 +}; + + +enum OLEDDISPLAY_GEOMETRY { + GEOMETRY_128_64 = 0, + GEOMETRY_128_32 = 1, + GEOMETRY_64_48 = 2, + GEOMETRY_64_32 = 3, + GEOMETRY_RAWMODE = 4, + GEOMETRY_128_128 = 5 +}; + +enum HW_I2C { + I2C_ONE, + I2C_TWO +}; + +typedef char (*FontTableLookupFunction)(const uint8_t ch); +char DefaultFontTableLookup(const uint8_t ch); + + +#ifdef ARDUINO +class OLEDDisplay : public Print { +#elif __MBED__ +class OLEDDisplay : public Stream { +#else +#error "Unkown operating system" +#endif + + public: + OLEDDisplay(); + virtual ~OLEDDisplay(); + + uint16_t width(void) const { return displayWidth; }; + uint16_t height(void) const { return displayHeight; }; + + // Use this to resume after a deep sleep without resetting the display (what init() would do). + // Returns true if connection to the display was established and the buffer allocated, false otherwise. + bool allocateBuffer(); + + // Allocates the buffer and initializes the driver & display. Resets the display! + // Returns false if buffer allocation failed, true otherwise. + bool init(); + + // Free the memory used by the display + void end(); + + // Cycle through the initialization + void resetDisplay(void); + + /* Drawing functions */ + // Sets the color of all pixel operations + void setColor(OLEDDISPLAY_COLOR color); + + // Returns the current color. + OLEDDISPLAY_COLOR getColor(); + + // Draw a pixel at given position + void setPixel(int16_t x, int16_t y); + + // Draw a pixel at given position and color + void setPixelColor(int16_t x, int16_t y, OLEDDISPLAY_COLOR color); + + // Clear a pixel at given position FIXME: INVERSE is untested with this function + void clearPixel(int16_t x, int16_t y); + + // Draw a line from position 0 to position 1 + void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1); + + // Draw the border of a rectangle at the given location + void drawRect(int16_t x, int16_t y, int16_t width, int16_t height); + + // Fill the rectangle + void fillRect(int16_t x, int16_t y, int16_t width, int16_t height); + + // Draw the border of a circle + void drawCircle(int16_t x, int16_t y, int16_t radius); + + // Draw all Quadrants specified in the quads bit mask + void drawCircleQuads(int16_t x0, int16_t y0, int16_t radius, uint8_t quads); + + // Fill circle + void fillCircle(int16_t x, int16_t y, int16_t radius); + + // Draw an empty triangle i.e. only the outline + void drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2); + + // Draw a solid triangle i.e. filled + void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2); + + // Draw a line horizontally + void drawHorizontalLine(int16_t x, int16_t y, int16_t length); + + // Draw a line vertically + void drawVerticalLine(int16_t x, int16_t y, int16_t length); + + // Draws a rounded progress bar with the outer dimensions given by width and height. Progress is + // a unsigned byte value between 0 and 100 + void drawProgressBar(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t progress); + + // Draw a bitmap in the internal image format + void drawFastImage(int16_t x, int16_t y, int16_t width, int16_t height, const uint8_t *image); + + // Draw a XBM + void drawXbm(int16_t x, int16_t y, int16_t width, int16_t height, const uint8_t *xbm); + + // Draw icon 16x16 xbm format + void drawIco16x16(int16_t x, int16_t y, const uint8_t *ico, bool inverse = false); + + /* Text functions */ + + // Draws a string at the given location, returns how many chars have been written + uint16_t drawString(int16_t x, int16_t y, const String &text); + + // Draws a formatted string (like printf) at the given location + void drawStringf(int16_t x, int16_t y, char* buffer, String format, ... ); + + // Draws a String with a maximum width at the given location. + // If the given String is wider than the specified width + // The text will be wrapped to the next line at a space or dash + // returns 0 if everything fits on the screen or the numbers of characters in the + // first line if not + uint16_t drawStringMaxWidth(int16_t x, int16_t y, uint16_t maxLineWidth, const String &text); + + // Returns the width of the const char* with the current + // font settings + uint16_t getStringWidth(const char* text, uint16_t length, bool utf8 = false); + + // Convencience method for the const char version + uint16_t getStringWidth(const String &text); + + // Specifies relative to which anchor point + // the text is rendered. Available constants: + // TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER_BOTH + void setTextAlignment(OLEDDISPLAY_TEXT_ALIGNMENT textAlignment); + + // Sets the current font. Available default fonts + // ArialMT_Plain_10, ArialMT_Plain_16, ArialMT_Plain_24 + void setFont(const uint8_t *fontData); + + // Set the function that will convert utf-8 to font table index + void setFontTableLookupFunction(FontTableLookupFunction function); + + /* Display functions */ + + // Turn the display on + void displayOn(void); + + // Turn the display offs + void displayOff(void); + + // Inverted display mode + void invertDisplay(void); + + // Normal display mode + void normalDisplay(void); + + // Set display contrast + // really low brightness & contrast: contrast = 10, precharge = 5, comdetect = 0 + // normal brightness & contrast: contrast = 100 + void setContrast(uint8_t contrast, uint8_t precharge = 241, uint8_t comdetect = 64); + + // Convenience method to access + virtual void setBrightness(uint8_t); + + // Reset display rotation or mirroring + void resetOrientation(); + + // Turn the display upside down + void flipScreenVertically(); + + // Mirror the display (to be used in a mirror or as a projector) + void mirrorScreen(); + + // Write the buffer to the display memory + virtual void display(void) = 0; + + // Clear the local pixel buffer + void clear(void); + + // Log buffer implementation + + // This will define the lines and characters you can + // print to the screen. When you exeed the buffer size (lines * chars) + // the output may be truncated due to the size constraint. + bool setLogBuffer(uint16_t lines, uint16_t chars); + + // Draw the log buffer at position (x, y) + void drawLogBuffer(uint16_t x, uint16_t y); + + // Get screen geometry + uint16_t getWidth(void); + uint16_t getHeight(void); + + // Implement needed function to be compatible with Print class + size_t write(uint8_t c); + size_t write(const char* s); + + // Implement needed function to be compatible with Stream class +#ifdef __MBED__ + int _putc(int c); + int _getc() { return -1; }; +#endif + + + uint8_t *buffer; + + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + uint8_t *buffer_back; + #endif + + // Set the correct height, width and buffer for the geometry + void setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width = 0, uint16_t height = 0); + + protected: + + OLEDDISPLAY_GEOMETRY geometry; + + uint16_t displayWidth; + uint16_t displayHeight; + uint16_t displayBufferSize; + + OLEDDISPLAY_TEXT_ALIGNMENT textAlignment; + OLEDDISPLAY_COLOR color; + + const uint8_t *fontData; + + // State values for logBuffer + uint16_t logBufferSize; + uint16_t logBufferFilled; + uint16_t logBufferLine; + uint16_t logBufferMaxLines; + char *logBuffer; + + + // the header size of the buffer used, e.g. for the SPI command header + int BufferOffset; + virtual int getBufferOffset(void) = 0; + + // Send a command to the display (low level function) + virtual void sendCommand(uint8_t com) {(void)com;}; + + // Connect to the display + virtual bool connect() { return false; }; + + // Send all the init commands + virtual void sendInitCommands(); + + // converts utf8 characters to extended ascii + char* utf8ascii(const String &s); + + void inline drawInternal(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *data, uint16_t offset, uint16_t bytesInData) __attribute__((always_inline)); + + uint16_t drawStringInternal(int16_t xMove, int16_t yMove, const char* text, uint16_t textLength, uint16_t textWidth, bool utf8); + + FontTableLookupFunction fontTableLookupFunction; +}; + +#endif diff --git a/src/helpers/ui/OLEDDisplayFonts.cpp b/src/helpers/ui/OLEDDisplayFonts.cpp new file mode 100644 index 00000000..660eb6a3 --- /dev/null +++ b/src/helpers/ui/OLEDDisplayFonts.cpp @@ -0,0 +1,1273 @@ +#include "OLEDDisplayFonts.h" + +const uint8_t ArialMT_Plain_10[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x20, // First Char: 32 + 0xE0, // Numbers of Chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x03, // 32:65535 + 0x00, 0x00, 0x04, 0x03, // 33:0 + 0x00, 0x04, 0x05, 0x04, // 34:4 + 0x00, 0x09, 0x09, 0x06, // 35:9 + 0x00, 0x12, 0x0A, 0x06, // 36:18 + 0x00, 0x1C, 0x10, 0x09, // 37:28 + 0x00, 0x2C, 0x0E, 0x07, // 38:44 + 0x00, 0x3A, 0x01, 0x02, // 39:58 + 0x00, 0x3B, 0x06, 0x03, // 40:59 + 0x00, 0x41, 0x06, 0x03, // 41:65 + 0x00, 0x47, 0x05, 0x04, // 42:71 + 0x00, 0x4C, 0x09, 0x06, // 43:76 + 0x00, 0x55, 0x04, 0x03, // 44:85 + 0x00, 0x59, 0x03, 0x03, // 45:89 + 0x00, 0x5C, 0x04, 0x03, // 46:92 + 0x00, 0x60, 0x05, 0x03, // 47:96 + 0x00, 0x65, 0x0A, 0x06, // 48:101 + 0x00, 0x6F, 0x08, 0x06, // 49:111 + 0x00, 0x77, 0x0A, 0x06, // 50:119 + 0x00, 0x81, 0x0A, 0x06, // 51:129 + 0x00, 0x8B, 0x0B, 0x06, // 52:139 + 0x00, 0x96, 0x0A, 0x06, // 53:150 + 0x00, 0xA0, 0x0A, 0x06, // 54:160 + 0x00, 0xAA, 0x09, 0x06, // 55:170 + 0x00, 0xB3, 0x0A, 0x06, // 56:179 + 0x00, 0xBD, 0x0A, 0x06, // 57:189 + 0x00, 0xC7, 0x04, 0x03, // 58:199 + 0x00, 0xCB, 0x04, 0x03, // 59:203 + 0x00, 0xCF, 0x0A, 0x06, // 60:207 + 0x00, 0xD9, 0x09, 0x06, // 61:217 + 0x00, 0xE2, 0x09, 0x06, // 62:226 + 0x00, 0xEB, 0x0B, 0x06, // 63:235 + 0x00, 0xF6, 0x14, 0x0A, // 64:246 + 0x01, 0x0A, 0x0E, 0x07, // 65:266 + 0x01, 0x18, 0x0C, 0x07, // 66:280 + 0x01, 0x24, 0x0C, 0x07, // 67:292 + 0x01, 0x30, 0x0B, 0x07, // 68:304 + 0x01, 0x3B, 0x0C, 0x07, // 69:315 + 0x01, 0x47, 0x09, 0x06, // 70:327 + 0x01, 0x50, 0x0D, 0x08, // 71:336 + 0x01, 0x5D, 0x0C, 0x07, // 72:349 + 0x01, 0x69, 0x04, 0x03, // 73:361 + 0x01, 0x6D, 0x08, 0x05, // 74:365 + 0x01, 0x75, 0x0E, 0x07, // 75:373 + 0x01, 0x83, 0x0C, 0x06, // 76:387 + 0x01, 0x8F, 0x10, 0x08, // 77:399 + 0x01, 0x9F, 0x0C, 0x07, // 78:415 + 0x01, 0xAB, 0x0E, 0x08, // 79:427 + 0x01, 0xB9, 0x0B, 0x07, // 80:441 + 0x01, 0xC4, 0x0E, 0x08, // 81:452 + 0x01, 0xD2, 0x0C, 0x07, // 82:466 + 0x01, 0xDE, 0x0C, 0x07, // 83:478 + 0x01, 0xEA, 0x0B, 0x06, // 84:490 + 0x01, 0xF5, 0x0C, 0x07, // 85:501 + 0x02, 0x01, 0x0D, 0x07, // 86:513 + 0x02, 0x0E, 0x11, 0x09, // 87:526 + 0x02, 0x1F, 0x0E, 0x07, // 88:543 + 0x02, 0x2D, 0x0D, 0x07, // 89:557 + 0x02, 0x3A, 0x0C, 0x06, // 90:570 + 0x02, 0x46, 0x06, 0x03, // 91:582 + 0x02, 0x4C, 0x06, 0x03, // 92:588 + 0x02, 0x52, 0x04, 0x03, // 93:594 + 0x02, 0x56, 0x09, 0x05, // 94:598 + 0x02, 0x5F, 0x0C, 0x06, // 95:607 + 0x02, 0x6B, 0x03, 0x03, // 96:619 + 0x02, 0x6E, 0x0A, 0x06, // 97:622 + 0x02, 0x78, 0x0A, 0x06, // 98:632 + 0x02, 0x82, 0x0A, 0x05, // 99:642 + 0x02, 0x8C, 0x0A, 0x06, // 100:652 + 0x02, 0x96, 0x0A, 0x06, // 101:662 + 0x02, 0xA0, 0x05, 0x03, // 102:672 + 0x02, 0xA5, 0x0A, 0x06, // 103:677 + 0x02, 0xAF, 0x0A, 0x06, // 104:687 + 0x02, 0xB9, 0x04, 0x02, // 105:697 + 0x02, 0xBD, 0x04, 0x02, // 106:701 + 0x02, 0xC1, 0x08, 0x05, // 107:705 + 0x02, 0xC9, 0x04, 0x02, // 108:713 + 0x02, 0xCD, 0x10, 0x08, // 109:717 + 0x02, 0xDD, 0x0A, 0x06, // 110:733 + 0x02, 0xE7, 0x0A, 0x06, // 111:743 + 0x02, 0xF1, 0x0A, 0x06, // 112:753 + 0x02, 0xFB, 0x0A, 0x06, // 113:763 + 0x03, 0x05, 0x05, 0x03, // 114:773 + 0x03, 0x0A, 0x08, 0x05, // 115:778 + 0x03, 0x12, 0x06, 0x03, // 116:786 + 0x03, 0x18, 0x0A, 0x06, // 117:792 + 0x03, 0x22, 0x09, 0x05, // 118:802 + 0x03, 0x2B, 0x0E, 0x07, // 119:811 + 0x03, 0x39, 0x0A, 0x05, // 120:825 + 0x03, 0x43, 0x09, 0x05, // 121:835 + 0x03, 0x4C, 0x0A, 0x05, // 122:844 + 0x03, 0x56, 0x06, 0x03, // 123:854 + 0x03, 0x5C, 0x04, 0x03, // 124:860 + 0x03, 0x60, 0x05, 0x03, // 125:864 + 0x03, 0x65, 0x09, 0x06, // 126:869 + 0xFF, 0xFF, 0x00, 0x00, // 127:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 128:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 129:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 130:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 131:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 132:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 133:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 134:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 135:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 136:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 137:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 138:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 139:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 140:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 141:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 142:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 143:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 144:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 145:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 146:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 147:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 148:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 149:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 150:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 151:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 152:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 153:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 154:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 155:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 156:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 157:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 158:65535 + 0xFF, 0xFF, 0x00, 0x0A, // 159:65535 + 0xFF, 0xFF, 0x00, 0x03, // 160:65535 + 0x03, 0x6E, 0x04, 0x03, // 161:878 + 0x03, 0x72, 0x0A, 0x06, // 162:882 + 0x03, 0x7C, 0x0C, 0x06, // 163:892 + 0x03, 0x88, 0x0A, 0x06, // 164:904 + 0x03, 0x92, 0x0A, 0x06, // 165:914 + 0x03, 0x9C, 0x04, 0x03, // 166:924 + 0x03, 0xA0, 0x0A, 0x06, // 167:928 + 0x03, 0xAA, 0x05, 0x03, // 168:938 + 0x03, 0xAF, 0x0D, 0x07, // 169:943 + 0x03, 0xBC, 0x07, 0x04, // 170:956 + 0x03, 0xC3, 0x0A, 0x06, // 171:963 + 0x03, 0xCD, 0x09, 0x06, // 172:973 + 0x03, 0xD6, 0x03, 0x03, // 173:982 + 0x03, 0xD9, 0x0D, 0x07, // 174:985 + 0x03, 0xE6, 0x0B, 0x06, // 175:998 + 0x03, 0xF1, 0x07, 0x04, // 176:1009 + 0x03, 0xF8, 0x0A, 0x05, // 177:1016 + 0x04, 0x02, 0x05, 0x03, // 178:1026 + 0x04, 0x07, 0x05, 0x03, // 179:1031 + 0x04, 0x0C, 0x05, 0x03, // 180:1036 + 0x04, 0x11, 0x0A, 0x06, // 181:1041 + 0x04, 0x1B, 0x09, 0x05, // 182:1051 + 0x04, 0x24, 0x03, 0x03, // 183:1060 + 0x04, 0x27, 0x06, 0x03, // 184:1063 + 0x04, 0x2D, 0x05, 0x03, // 185:1069 + 0x04, 0x32, 0x07, 0x04, // 186:1074 + 0x04, 0x39, 0x0A, 0x06, // 187:1081 + 0x04, 0x43, 0x10, 0x08, // 188:1091 + 0x04, 0x53, 0x10, 0x08, // 189:1107 + 0x04, 0x63, 0x10, 0x08, // 190:1123 + 0x04, 0x73, 0x0A, 0x06, // 191:1139 + 0x04, 0x7D, 0x0E, 0x07, // 192:1149 + 0x04, 0x8B, 0x0E, 0x07, // 193:1163 + 0x04, 0x99, 0x0E, 0x07, // 194:1177 + 0x04, 0xA7, 0x0E, 0x07, // 195:1191 + 0x04, 0xB5, 0x0E, 0x07, // 196:1205 + 0x04, 0xC3, 0x0E, 0x07, // 197:1219 + 0x04, 0xD1, 0x12, 0x0A, // 198:1233 + 0x04, 0xE3, 0x0C, 0x07, // 199:1251 + 0x04, 0xEF, 0x0C, 0x07, // 200:1263 + 0x04, 0xFB, 0x0C, 0x07, // 201:1275 + 0x05, 0x07, 0x0C, 0x07, // 202:1287 + 0x05, 0x13, 0x0C, 0x07, // 203:1299 + 0x05, 0x1F, 0x05, 0x03, // 204:1311 + 0x05, 0x24, 0x04, 0x03, // 205:1316 + 0x05, 0x28, 0x04, 0x03, // 206:1320 + 0x05, 0x2C, 0x05, 0x03, // 207:1324 + 0x05, 0x31, 0x0B, 0x07, // 208:1329 + 0x05, 0x3C, 0x0C, 0x07, // 209:1340 + 0x05, 0x48, 0x0E, 0x08, // 210:1352 + 0x05, 0x56, 0x0E, 0x08, // 211:1366 + 0x05, 0x64, 0x0E, 0x08, // 212:1380 + 0x05, 0x72, 0x0E, 0x08, // 213:1394 + 0x05, 0x80, 0x0E, 0x08, // 214:1408 + 0x05, 0x8E, 0x0A, 0x06, // 215:1422 + 0x05, 0x98, 0x0D, 0x08, // 216:1432 + 0x05, 0xA5, 0x0C, 0x07, // 217:1445 + 0x05, 0xB1, 0x0C, 0x07, // 218:1457 + 0x05, 0xBD, 0x0C, 0x07, // 219:1469 + 0x05, 0xC9, 0x0C, 0x07, // 220:1481 + 0x05, 0xD5, 0x0D, 0x07, // 221:1493 + 0x05, 0xE2, 0x0B, 0x07, // 222:1506 + 0x05, 0xED, 0x0C, 0x06, // 223:1517 + 0x05, 0xF9, 0x0A, 0x06, // 224:1529 + 0x06, 0x03, 0x0A, 0x06, // 225:1539 + 0x06, 0x0D, 0x0A, 0x06, // 226:1549 + 0x06, 0x17, 0x0A, 0x06, // 227:1559 + 0x06, 0x21, 0x0A, 0x06, // 228:1569 + 0x06, 0x2B, 0x0A, 0x06, // 229:1579 + 0x06, 0x35, 0x10, 0x09, // 230:1589 + 0x06, 0x45, 0x0A, 0x05, // 231:1605 + 0x06, 0x4F, 0x0A, 0x06, // 232:1615 + 0x06, 0x59, 0x0A, 0x06, // 233:1625 + 0x06, 0x63, 0x0A, 0x06, // 234:1635 + 0x06, 0x6D, 0x0A, 0x06, // 235:1645 + 0x06, 0x77, 0x05, 0x03, // 236:1655 + 0x06, 0x7C, 0x04, 0x03, // 237:1660 + 0x06, 0x80, 0x05, 0x03, // 238:1664 + 0x06, 0x85, 0x05, 0x03, // 239:1669 + 0x06, 0x8A, 0x0A, 0x06, // 240:1674 + 0x06, 0x94, 0x0A, 0x06, // 241:1684 + 0x06, 0x9E, 0x0A, 0x06, // 242:1694 + 0x06, 0xA8, 0x0A, 0x06, // 243:1704 + 0x06, 0xB2, 0x0A, 0x06, // 244:1714 + 0x06, 0xBC, 0x0A, 0x06, // 245:1724 + 0x06, 0xC6, 0x0A, 0x06, // 246:1734 + 0x06, 0xD0, 0x09, 0x05, // 247:1744 + 0x06, 0xD9, 0x0A, 0x06, // 248:1753 + 0x06, 0xE3, 0x0A, 0x06, // 249:1763 + 0x06, 0xED, 0x0A, 0x06, // 250:1773 + 0x06, 0xF7, 0x0A, 0x06, // 251:1783 + 0x07, 0x01, 0x0A, 0x06, // 252:1793 + 0x07, 0x0B, 0x09, 0x05, // 253:1803 + 0x07, 0x14, 0x0A, 0x06, // 254:1812 + 0x07, 0x1E, 0x09, 0x05, // 255:1822 + + // Font Data: + 0x00,0x00,0xF8,0x02, // 33 + 0x38,0x00,0x00,0x00,0x38, // 34 + 0xA0,0x03,0xE0,0x00,0xB8,0x03,0xE0,0x00,0xB8, // 35 + 0x30,0x01,0x28,0x02,0xF8,0x07,0x48,0x02,0x90,0x01, // 36 + 0x00,0x00,0x30,0x00,0x48,0x00,0x30,0x03,0xC0,0x00,0xB0,0x01,0x48,0x02,0x80,0x01, // 37 + 0x80,0x01,0x50,0x02,0x68,0x02,0xA8,0x02,0x18,0x01,0x80,0x03,0x80,0x02, // 38 + 0x38, // 39 + 0xE0,0x03,0x10,0x04,0x08,0x08, // 40 + 0x08,0x08,0x10,0x04,0xE0,0x03, // 41 + 0x28,0x00,0x18,0x00,0x28, // 42 + 0x40,0x00,0x40,0x00,0xF0,0x01,0x40,0x00,0x40, // 43 + 0x00,0x00,0x00,0x06, // 44 + 0x80,0x00,0x80, // 45 + 0x00,0x00,0x00,0x02, // 46 + 0x00,0x03,0xE0,0x00,0x18, // 47 + 0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x02,0xF0,0x01, // 48 + 0x00,0x00,0x20,0x00,0x10,0x00,0xF8,0x03, // 49 + 0x10,0x02,0x08,0x03,0x88,0x02,0x48,0x02,0x30,0x02, // 50 + 0x10,0x01,0x08,0x02,0x48,0x02,0x48,0x02,0xB0,0x01, // 51 + 0xC0,0x00,0xA0,0x00,0x90,0x00,0x88,0x00,0xF8,0x03,0x80, // 52 + 0x60,0x01,0x38,0x02,0x28,0x02,0x28,0x02,0xC8,0x01, // 53 + 0xF0,0x01,0x28,0x02,0x28,0x02,0x28,0x02,0xD0,0x01, // 54 + 0x08,0x00,0x08,0x03,0xC8,0x00,0x38,0x00,0x08, // 55 + 0xB0,0x01,0x48,0x02,0x48,0x02,0x48,0x02,0xB0,0x01, // 56 + 0x70,0x01,0x88,0x02,0x88,0x02,0x88,0x02,0xF0,0x01, // 57 + 0x00,0x00,0x20,0x02, // 58 + 0x00,0x00,0x20,0x06, // 59 + 0x00,0x00,0x40,0x00,0xA0,0x00,0xA0,0x00,0x10,0x01, // 60 + 0xA0,0x00,0xA0,0x00,0xA0,0x00,0xA0,0x00,0xA0, // 61 + 0x00,0x00,0x10,0x01,0xA0,0x00,0xA0,0x00,0x40, // 62 + 0x10,0x00,0x08,0x00,0x08,0x00,0xC8,0x02,0x48,0x00,0x30, // 63 + 0x00,0x00,0xC0,0x03,0x30,0x04,0xD0,0x09,0x28,0x0A,0x28,0x0A,0xC8,0x0B,0x68,0x0A,0x10,0x05,0xE0,0x04, // 64 + 0x00,0x02,0xC0,0x01,0xB0,0x00,0x88,0x00,0xB0,0x00,0xC0,0x01,0x00,0x02, // 65 + 0x00,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x48,0x02,0xF0,0x01, // 66 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x02,0x10,0x01, // 67 + 0x00,0x00,0xF8,0x03,0x08,0x02,0x08,0x02,0x10,0x01,0xE0, // 68 + 0x00,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x48,0x02,0x48,0x02, // 69 + 0x00,0x00,0xF8,0x03,0x48,0x00,0x48,0x00,0x08, // 70 + 0x00,0x00,0xE0,0x00,0x10,0x01,0x08,0x02,0x48,0x02,0x50,0x01,0xC0, // 71 + 0x00,0x00,0xF8,0x03,0x40,0x00,0x40,0x00,0x40,0x00,0xF8,0x03, // 72 + 0x00,0x00,0xF8,0x03, // 73 + 0x00,0x03,0x00,0x02,0x00,0x02,0xF8,0x01, // 74 + 0x00,0x00,0xF8,0x03,0x80,0x00,0x60,0x00,0x90,0x00,0x08,0x01,0x00,0x02, // 75 + 0x00,0x00,0xF8,0x03,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02, // 76 + 0x00,0x00,0xF8,0x03,0x30,0x00,0xC0,0x01,0x00,0x02,0xC0,0x01,0x30,0x00,0xF8,0x03, // 77 + 0x00,0x00,0xF8,0x03,0x30,0x00,0x40,0x00,0x80,0x01,0xF8,0x03, // 78 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x02,0x08,0x02,0xF0,0x01, // 79 + 0x00,0x00,0xF8,0x03,0x48,0x00,0x48,0x00,0x48,0x00,0x30, // 80 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x02,0x08,0x03,0x08,0x03,0xF0,0x02, // 81 + 0x00,0x00,0xF8,0x03,0x48,0x00,0x48,0x00,0xC8,0x00,0x30,0x03, // 82 + 0x00,0x00,0x30,0x01,0x48,0x02,0x48,0x02,0x48,0x02,0x90,0x01, // 83 + 0x00,0x00,0x08,0x00,0x08,0x00,0xF8,0x03,0x08,0x00,0x08, // 84 + 0x00,0x00,0xF8,0x01,0x00,0x02,0x00,0x02,0x00,0x02,0xF8,0x01, // 85 + 0x08,0x00,0x70,0x00,0x80,0x01,0x00,0x02,0x80,0x01,0x70,0x00,0x08, // 86 + 0x18,0x00,0xE0,0x01,0x00,0x02,0xF0,0x01,0x08,0x00,0xF0,0x01,0x00,0x02,0xE0,0x01,0x18, // 87 + 0x00,0x02,0x08,0x01,0x90,0x00,0x60,0x00,0x90,0x00,0x08,0x01,0x00,0x02, // 88 + 0x08,0x00,0x10,0x00,0x20,0x00,0xC0,0x03,0x20,0x00,0x10,0x00,0x08, // 89 + 0x08,0x03,0x88,0x02,0xC8,0x02,0x68,0x02,0x38,0x02,0x18,0x02, // 90 + 0x00,0x00,0xF8,0x0F,0x08,0x08, // 91 + 0x18,0x00,0xE0,0x00,0x00,0x03, // 92 + 0x08,0x08,0xF8,0x0F, // 93 + 0x40,0x00,0x30,0x00,0x08,0x00,0x30,0x00,0x40, // 94 + 0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0x08, // 95 + 0x08,0x00,0x10, // 96 + 0x00,0x00,0x00,0x03,0xA0,0x02,0xA0,0x02,0xE0,0x03, // 97 + 0x00,0x00,0xF8,0x03,0x20,0x02,0x20,0x02,0xC0,0x01, // 98 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0x40,0x01, // 99 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0xF8,0x03, // 100 + 0x00,0x00,0xC0,0x01,0xA0,0x02,0xA0,0x02,0xC0,0x02, // 101 + 0x20,0x00,0xF0,0x03,0x28, // 102 + 0x00,0x00,0xC0,0x05,0x20,0x0A,0x20,0x0A,0xE0,0x07, // 103 + 0x00,0x00,0xF8,0x03,0x20,0x00,0x20,0x00,0xC0,0x03, // 104 + 0x00,0x00,0xE8,0x03, // 105 + 0x00,0x08,0xE8,0x07, // 106 + 0xF8,0x03,0x80,0x00,0xC0,0x01,0x20,0x02, // 107 + 0x00,0x00,0xF8,0x03, // 108 + 0x00,0x00,0xE0,0x03,0x20,0x00,0x20,0x00,0xE0,0x03,0x20,0x00,0x20,0x00,0xC0,0x03, // 109 + 0x00,0x00,0xE0,0x03,0x20,0x00,0x20,0x00,0xC0,0x03, // 110 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0xC0,0x01, // 111 + 0x00,0x00,0xE0,0x0F,0x20,0x02,0x20,0x02,0xC0,0x01, // 112 + 0x00,0x00,0xC0,0x01,0x20,0x02,0x20,0x02,0xE0,0x0F, // 113 + 0x00,0x00,0xE0,0x03,0x20, // 114 + 0x40,0x02,0xA0,0x02,0xA0,0x02,0x20,0x01, // 115 + 0x20,0x00,0xF8,0x03,0x20,0x02, // 116 + 0x00,0x00,0xE0,0x01,0x00,0x02,0x00,0x02,0xE0,0x03, // 117 + 0x20,0x00,0xC0,0x01,0x00,0x02,0xC0,0x01,0x20, // 118 + 0xE0,0x01,0x00,0x02,0xC0,0x01,0x20,0x00,0xC0,0x01,0x00,0x02,0xE0,0x01, // 119 + 0x20,0x02,0x40,0x01,0x80,0x00,0x40,0x01,0x20,0x02, // 120 + 0x20,0x00,0xC0,0x09,0x00,0x06,0xC0,0x01,0x20, // 121 + 0x20,0x02,0x20,0x03,0xA0,0x02,0x60,0x02,0x20,0x02, // 122 + 0x80,0x00,0x78,0x0F,0x08,0x08, // 123 + 0x00,0x00,0xF8,0x0F, // 124 + 0x08,0x08,0x78,0x0F,0x80, // 125 + 0xC0,0x00,0x40,0x00,0xC0,0x00,0x80,0x00,0xC0, // 126 + 0x00,0x00,0xA0,0x0F, // 161 + 0x00,0x00,0xC0,0x01,0xA0,0x0F,0x78,0x02,0x40,0x01, // 162 + 0x40,0x02,0x70,0x03,0xC8,0x02,0x48,0x02,0x08,0x02,0x10,0x02, // 163 + 0x00,0x00,0xE0,0x01,0x20,0x01,0x20,0x01,0xE0,0x01, // 164 + 0x48,0x01,0x70,0x01,0xC0,0x03,0x70,0x01,0x48,0x01, // 165 + 0x00,0x00,0x38,0x0F, // 166 + 0xD0,0x04,0x28,0x09,0x48,0x09,0x48,0x0A,0x90,0x05, // 167 + 0x08,0x00,0x00,0x00,0x08, // 168 + 0xE0,0x00,0x10,0x01,0x48,0x02,0xA8,0x02,0xA8,0x02,0x10,0x01,0xE0, // 169 + 0x68,0x00,0x68,0x00,0x68,0x00,0x78, // 170 + 0x00,0x00,0x80,0x01,0x40,0x02,0x80,0x01,0x40,0x02, // 171 + 0x20,0x00,0x20,0x00,0x20,0x00,0x20,0x00,0xE0, // 172 + 0x80,0x00,0x80, // 173 + 0xE0,0x00,0x10,0x01,0xE8,0x02,0x68,0x02,0xC8,0x02,0x10,0x01,0xE0, // 174 + 0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02,0x00,0x02, // 175 + 0x00,0x00,0x38,0x00,0x28,0x00,0x38, // 176 + 0x40,0x02,0x40,0x02,0xF0,0x03,0x40,0x02,0x40,0x02, // 177 + 0x48,0x00,0x68,0x00,0x58, // 178 + 0x48,0x00,0x58,0x00,0x68, // 179 + 0x00,0x00,0x10,0x00,0x08, // 180 + 0x00,0x00,0xE0,0x0F,0x00,0x02,0x00,0x02,0xE0,0x03, // 181 + 0x70,0x00,0xF8,0x0F,0x08,0x00,0xF8,0x0F,0x08, // 182 + 0x00,0x00,0x40, // 183 + 0x00,0x00,0x00,0x14,0x00,0x18, // 184 + 0x00,0x00,0x10,0x00,0x78, // 185 + 0x30,0x00,0x48,0x00,0x48,0x00,0x30, // 186 + 0x00,0x00,0x40,0x02,0x80,0x01,0x40,0x02,0x80,0x01, // 187 + 0x00,0x00,0x10,0x02,0x78,0x01,0xC0,0x00,0x20,0x01,0x90,0x01,0xC8,0x03,0x00,0x01, // 188 + 0x00,0x00,0x10,0x02,0x78,0x01,0x80,0x00,0x60,0x00,0x50,0x02,0x48,0x03,0xC0,0x02, // 189 + 0x48,0x00,0x58,0x00,0x68,0x03,0x80,0x00,0x60,0x01,0x90,0x01,0xC8,0x03,0x00,0x01, // 190 + 0x00,0x00,0x00,0x06,0x00,0x09,0xA0,0x09,0x00,0x04, // 191 + 0x00,0x02,0xC0,0x01,0xB0,0x00,0x89,0x00,0xB2,0x00,0xC0,0x01,0x00,0x02, // 192 + 0x00,0x02,0xC0,0x01,0xB0,0x00,0x8A,0x00,0xB1,0x00,0xC0,0x01,0x00,0x02, // 193 + 0x00,0x02,0xC0,0x01,0xB2,0x00,0x89,0x00,0xB2,0x00,0xC0,0x01,0x00,0x02, // 194 + 0x00,0x02,0xC2,0x01,0xB1,0x00,0x8A,0x00,0xB1,0x00,0xC0,0x01,0x00,0x02, // 195 + 0x00,0x02,0xC0,0x01,0xB2,0x00,0x88,0x00,0xB2,0x00,0xC0,0x01,0x00,0x02, // 196 + 0x00,0x02,0xC0,0x01,0xBE,0x00,0x8A,0x00,0xBE,0x00,0xC0,0x01,0x00,0x02, // 197 + 0x00,0x03,0xC0,0x00,0xE0,0x00,0x98,0x00,0x88,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x48,0x02, // 198 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x08,0x16,0x08,0x1A,0x10,0x01, // 199 + 0x00,0x00,0xF8,0x03,0x49,0x02,0x4A,0x02,0x48,0x02,0x48,0x02, // 200 + 0x00,0x00,0xF8,0x03,0x48,0x02,0x4A,0x02,0x49,0x02,0x48,0x02, // 201 + 0x00,0x00,0xFA,0x03,0x49,0x02,0x4A,0x02,0x48,0x02,0x48,0x02, // 202 + 0x00,0x00,0xF8,0x03,0x4A,0x02,0x48,0x02,0x4A,0x02,0x48,0x02, // 203 + 0x00,0x00,0xF9,0x03,0x02, // 204 + 0x02,0x00,0xF9,0x03, // 205 + 0x01,0x00,0xFA,0x03, // 206 + 0x02,0x00,0xF8,0x03,0x02, // 207 + 0x40,0x00,0xF8,0x03,0x48,0x02,0x48,0x02,0x10,0x01,0xE0, // 208 + 0x00,0x00,0xFA,0x03,0x31,0x00,0x42,0x00,0x81,0x01,0xF8,0x03, // 209 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x09,0x02,0x0A,0x02,0x08,0x02,0xF0,0x01, // 210 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x0A,0x02,0x09,0x02,0x08,0x02,0xF0,0x01, // 211 + 0x00,0x00,0xF0,0x01,0x08,0x02,0x0A,0x02,0x09,0x02,0x0A,0x02,0xF0,0x01, // 212 + 0x00,0x00,0xF0,0x01,0x0A,0x02,0x09,0x02,0x0A,0x02,0x09,0x02,0xF0,0x01, // 213 + 0x00,0x00,0xF0,0x01,0x0A,0x02,0x08,0x02,0x0A,0x02,0x08,0x02,0xF0,0x01, // 214 + 0x10,0x01,0xA0,0x00,0xE0,0x00,0xA0,0x00,0x10,0x01, // 215 + 0x00,0x00,0xF0,0x02,0x08,0x03,0xC8,0x02,0x28,0x02,0x18,0x03,0xE8, // 216 + 0x00,0x00,0xF8,0x01,0x01,0x02,0x02,0x02,0x00,0x02,0xF8,0x01, // 217 + 0x00,0x00,0xF8,0x01,0x02,0x02,0x01,0x02,0x00,0x02,0xF8,0x01, // 218 + 0x00,0x00,0xF8,0x01,0x02,0x02,0x01,0x02,0x02,0x02,0xF8,0x01, // 219 + 0x00,0x00,0xF8,0x01,0x02,0x02,0x00,0x02,0x02,0x02,0xF8,0x01, // 220 + 0x08,0x00,0x10,0x00,0x20,0x00,0xC2,0x03,0x21,0x00,0x10,0x00,0x08, // 221 + 0x00,0x00,0xF8,0x03,0x10,0x01,0x10,0x01,0x10,0x01,0xE0, // 222 + 0x00,0x00,0xF0,0x03,0x08,0x01,0x48,0x02,0xB0,0x02,0x80,0x01, // 223 + 0x00,0x00,0x00,0x03,0xA4,0x02,0xA8,0x02,0xE0,0x03, // 224 + 0x00,0x00,0x00,0x03,0xA8,0x02,0xA4,0x02,0xE0,0x03, // 225 + 0x00,0x00,0x00,0x03,0xA8,0x02,0xA4,0x02,0xE8,0x03, // 226 + 0x00,0x00,0x08,0x03,0xA4,0x02,0xA8,0x02,0xE4,0x03, // 227 + 0x00,0x00,0x00,0x03,0xA8,0x02,0xA0,0x02,0xE8,0x03, // 228 + 0x00,0x00,0x00,0x03,0xAE,0x02,0xAA,0x02,0xEE,0x03, // 229 + 0x00,0x00,0x40,0x03,0xA0,0x02,0xA0,0x02,0xC0,0x01,0xA0,0x02,0xA0,0x02,0xC0,0x02, // 230 + 0x00,0x00,0xC0,0x01,0x20,0x16,0x20,0x1A,0x40,0x01, // 231 + 0x00,0x00,0xC0,0x01,0xA4,0x02,0xA8,0x02,0xC0,0x02, // 232 + 0x00,0x00,0xC0,0x01,0xA8,0x02,0xA4,0x02,0xC0,0x02, // 233 + 0x00,0x00,0xC0,0x01,0xA8,0x02,0xA4,0x02,0xC8,0x02, // 234 + 0x00,0x00,0xC0,0x01,0xA8,0x02,0xA0,0x02,0xC8,0x02, // 235 + 0x00,0x00,0xE4,0x03,0x08, // 236 + 0x08,0x00,0xE4,0x03, // 237 + 0x08,0x00,0xE4,0x03,0x08, // 238 + 0x08,0x00,0xE0,0x03,0x08, // 239 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x38,0x02,0xE0,0x01, // 240 + 0x00,0x00,0xE8,0x03,0x24,0x00,0x28,0x00,0xC4,0x03, // 241 + 0x00,0x00,0xC0,0x01,0x24,0x02,0x28,0x02,0xC0,0x01, // 242 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x24,0x02,0xC0,0x01, // 243 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x24,0x02,0xC8,0x01, // 244 + 0x00,0x00,0xC8,0x01,0x24,0x02,0x28,0x02,0xC4,0x01, // 245 + 0x00,0x00,0xC0,0x01,0x28,0x02,0x20,0x02,0xC8,0x01, // 246 + 0x40,0x00,0x40,0x00,0x50,0x01,0x40,0x00,0x40, // 247 + 0x00,0x00,0xC0,0x02,0xA0,0x03,0x60,0x02,0xA0,0x01, // 248 + 0x00,0x00,0xE0,0x01,0x04,0x02,0x08,0x02,0xE0,0x03, // 249 + 0x00,0x00,0xE0,0x01,0x08,0x02,0x04,0x02,0xE0,0x03, // 250 + 0x00,0x00,0xE8,0x01,0x04,0x02,0x08,0x02,0xE0,0x03, // 251 + 0x00,0x00,0xE0,0x01,0x08,0x02,0x00,0x02,0xE8,0x03, // 252 + 0x20,0x00,0xC0,0x09,0x08,0x06,0xC4,0x01,0x20, // 253 + 0x00,0x00,0xF8,0x0F,0x20,0x02,0x20,0x02,0xC0,0x01, // 254 + 0x20,0x00,0xC8,0x09,0x00,0x06,0xC8,0x01,0x20 // 255 +}; + +const uint8_t ArialMT_Plain_16[] PROGMEM = { + 0x10, // Width: 16 + 0x13, // Height: 19 + 0x20, // First Char: 32 + 0xE0, // Numbers of Chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x04, // 32:65535 + 0x00, 0x00, 0x08, 0x04, // 33:0 + 0x00, 0x08, 0x0D, 0x06, // 34:8 + 0x00, 0x15, 0x1A, 0x09, // 35:21 + 0x00, 0x2F, 0x17, 0x09, // 36:47 + 0x00, 0x46, 0x26, 0x0E, // 37:70 + 0x00, 0x6C, 0x1D, 0x0B, // 38:108 + 0x00, 0x89, 0x04, 0x03, // 39:137 + 0x00, 0x8D, 0x0C, 0x05, // 40:141 + 0x00, 0x99, 0x0B, 0x05, // 41:153 + 0x00, 0xA4, 0x0D, 0x06, // 42:164 + 0x00, 0xB1, 0x17, 0x09, // 43:177 + 0x00, 0xC8, 0x09, 0x04, // 44:200 + 0x00, 0xD1, 0x0B, 0x05, // 45:209 + 0x00, 0xDC, 0x08, 0x04, // 46:220 + 0x00, 0xE4, 0x0A, 0x04, // 47:228 + 0x00, 0xEE, 0x17, 0x09, // 48:238 + 0x01, 0x05, 0x11, 0x09, // 49:261 + 0x01, 0x16, 0x17, 0x09, // 50:278 + 0x01, 0x2D, 0x17, 0x09, // 51:301 + 0x01, 0x44, 0x17, 0x09, // 52:324 + 0x01, 0x5B, 0x17, 0x09, // 53:347 + 0x01, 0x72, 0x17, 0x09, // 54:370 + 0x01, 0x89, 0x16, 0x09, // 55:393 + 0x01, 0x9F, 0x17, 0x09, // 56:415 + 0x01, 0xB6, 0x17, 0x09, // 57:438 + 0x01, 0xCD, 0x05, 0x04, // 58:461 + 0x01, 0xD2, 0x06, 0x04, // 59:466 + 0x01, 0xD8, 0x17, 0x09, // 60:472 + 0x01, 0xEF, 0x17, 0x09, // 61:495 + 0x02, 0x06, 0x17, 0x09, // 62:518 + 0x02, 0x1D, 0x16, 0x09, // 63:541 + 0x02, 0x33, 0x2F, 0x10, // 64:563 + 0x02, 0x62, 0x1D, 0x0B, // 65:610 + 0x02, 0x7F, 0x1D, 0x0B, // 66:639 + 0x02, 0x9C, 0x20, 0x0C, // 67:668 + 0x02, 0xBC, 0x20, 0x0C, // 68:700 + 0x02, 0xDC, 0x1D, 0x0B, // 69:732 + 0x02, 0xF9, 0x19, 0x0A, // 70:761 + 0x03, 0x12, 0x20, 0x0C, // 71:786 + 0x03, 0x32, 0x1D, 0x0C, // 72:818 + 0x03, 0x4F, 0x05, 0x04, // 73:847 + 0x03, 0x54, 0x14, 0x08, // 74:852 + 0x03, 0x68, 0x1D, 0x0B, // 75:872 + 0x03, 0x85, 0x17, 0x09, // 76:901 + 0x03, 0x9C, 0x23, 0x0D, // 77:924 + 0x03, 0xBF, 0x1D, 0x0C, // 78:959 + 0x03, 0xDC, 0x20, 0x0C, // 79:988 + 0x03, 0xFC, 0x1C, 0x0B, // 80:1020 + 0x04, 0x18, 0x20, 0x0C, // 81:1048 + 0x04, 0x38, 0x1D, 0x0C, // 82:1080 + 0x04, 0x55, 0x1D, 0x0B, // 83:1109 + 0x04, 0x72, 0x19, 0x0A, // 84:1138 + 0x04, 0x8B, 0x1D, 0x0C, // 85:1163 + 0x04, 0xA8, 0x1C, 0x0B, // 86:1192 + 0x04, 0xC4, 0x2B, 0x0F, // 87:1220 + 0x04, 0xEF, 0x20, 0x0B, // 88:1263 + 0x05, 0x0F, 0x19, 0x0B, // 89:1295 + 0x05, 0x28, 0x1A, 0x0A, // 90:1320 + 0x05, 0x42, 0x0C, 0x04, // 91:1346 + 0x05, 0x4E, 0x0B, 0x04, // 92:1358 + 0x05, 0x59, 0x09, 0x04, // 93:1369 + 0x05, 0x62, 0x14, 0x08, // 94:1378 + 0x05, 0x76, 0x1B, 0x09, // 95:1398 + 0x05, 0x91, 0x07, 0x05, // 96:1425 + 0x05, 0x98, 0x17, 0x09, // 97:1432 + 0x05, 0xAF, 0x17, 0x09, // 98:1455 + 0x05, 0xC6, 0x14, 0x08, // 99:1478 + 0x05, 0xDA, 0x17, 0x09, // 100:1498 + 0x05, 0xF1, 0x17, 0x09, // 101:1521 + 0x06, 0x08, 0x0A, 0x04, // 102:1544 + 0x06, 0x12, 0x17, 0x09, // 103:1554 + 0x06, 0x29, 0x14, 0x09, // 104:1577 + 0x06, 0x3D, 0x05, 0x04, // 105:1597 + 0x06, 0x42, 0x06, 0x04, // 106:1602 + 0x06, 0x48, 0x17, 0x08, // 107:1608 + 0x06, 0x5F, 0x05, 0x04, // 108:1631 + 0x06, 0x64, 0x23, 0x0D, // 109:1636 + 0x06, 0x87, 0x14, 0x09, // 110:1671 + 0x06, 0x9B, 0x17, 0x09, // 111:1691 + 0x06, 0xB2, 0x17, 0x09, // 112:1714 + 0x06, 0xC9, 0x18, 0x09, // 113:1737 + 0x06, 0xE1, 0x0D, 0x05, // 114:1761 + 0x06, 0xEE, 0x14, 0x08, // 115:1774 + 0x07, 0x02, 0x0B, 0x04, // 116:1794 + 0x07, 0x0D, 0x14, 0x09, // 117:1805 + 0x07, 0x21, 0x13, 0x08, // 118:1825 + 0x07, 0x34, 0x1F, 0x0C, // 119:1844 + 0x07, 0x53, 0x14, 0x08, // 120:1875 + 0x07, 0x67, 0x13, 0x08, // 121:1895 + 0x07, 0x7A, 0x14, 0x08, // 122:1914 + 0x07, 0x8E, 0x0F, 0x05, // 123:1934 + 0x07, 0x9D, 0x06, 0x04, // 124:1949 + 0x07, 0xA3, 0x0E, 0x05, // 125:1955 + 0x07, 0xB1, 0x17, 0x09, // 126:1969 + 0xFF, 0xFF, 0x00, 0x00, // 127:65535 + 0xFF, 0xFF, 0x00, 0x10, // 128:65535 + 0xFF, 0xFF, 0x00, 0x10, // 129:65535 + 0xFF, 0xFF, 0x00, 0x10, // 130:65535 + 0xFF, 0xFF, 0x00, 0x10, // 131:65535 + 0xFF, 0xFF, 0x00, 0x10, // 132:65535 + 0xFF, 0xFF, 0x00, 0x10, // 133:65535 + 0xFF, 0xFF, 0x00, 0x10, // 134:65535 + 0xFF, 0xFF, 0x00, 0x10, // 135:65535 + 0xFF, 0xFF, 0x00, 0x10, // 136:65535 + 0xFF, 0xFF, 0x00, 0x10, // 137:65535 + 0xFF, 0xFF, 0x00, 0x10, // 138:65535 + 0xFF, 0xFF, 0x00, 0x10, // 139:65535 + 0xFF, 0xFF, 0x00, 0x10, // 140:65535 + 0xFF, 0xFF, 0x00, 0x10, // 141:65535 + 0xFF, 0xFF, 0x00, 0x10, // 142:65535 + 0xFF, 0xFF, 0x00, 0x10, // 143:65535 + 0xFF, 0xFF, 0x00, 0x10, // 144:65535 + 0xFF, 0xFF, 0x00, 0x10, // 145:65535 + 0xFF, 0xFF, 0x00, 0x10, // 146:65535 + 0xFF, 0xFF, 0x00, 0x10, // 147:65535 + 0xFF, 0xFF, 0x00, 0x10, // 148:65535 + 0xFF, 0xFF, 0x00, 0x10, // 149:65535 + 0xFF, 0xFF, 0x00, 0x10, // 150:65535 + 0xFF, 0xFF, 0x00, 0x10, // 151:65535 + 0xFF, 0xFF, 0x00, 0x10, // 152:65535 + 0xFF, 0xFF, 0x00, 0x10, // 153:65535 + 0xFF, 0xFF, 0x00, 0x10, // 154:65535 + 0xFF, 0xFF, 0x00, 0x10, // 155:65535 + 0xFF, 0xFF, 0x00, 0x10, // 156:65535 + 0xFF, 0xFF, 0x00, 0x10, // 157:65535 + 0xFF, 0xFF, 0x00, 0x10, // 158:65535 + 0xFF, 0xFF, 0x00, 0x10, // 159:65535 + 0xFF, 0xFF, 0x00, 0x04, // 160:65535 + 0x07, 0xC8, 0x09, 0x05, // 161:1992 + 0x07, 0xD1, 0x17, 0x09, // 162:2001 + 0x07, 0xE8, 0x17, 0x09, // 163:2024 + 0x07, 0xFF, 0x14, 0x09, // 164:2047 + 0x08, 0x13, 0x1A, 0x09, // 165:2067 + 0x08, 0x2D, 0x06, 0x04, // 166:2093 + 0x08, 0x33, 0x17, 0x09, // 167:2099 + 0x08, 0x4A, 0x07, 0x05, // 168:2122 + 0x08, 0x51, 0x23, 0x0C, // 169:2129 + 0x08, 0x74, 0x0E, 0x06, // 170:2164 + 0x08, 0x82, 0x14, 0x09, // 171:2178 + 0x08, 0x96, 0x17, 0x09, // 172:2198 + 0x08, 0xAD, 0x0B, 0x05, // 173:2221 + 0x08, 0xB8, 0x23, 0x0C, // 174:2232 + 0x08, 0xDB, 0x19, 0x09, // 175:2267 + 0x08, 0xF4, 0x0D, 0x06, // 176:2292 + 0x09, 0x01, 0x17, 0x09, // 177:2305 + 0x09, 0x18, 0x0E, 0x05, // 178:2328 + 0x09, 0x26, 0x0D, 0x05, // 179:2342 + 0x09, 0x33, 0x0A, 0x05, // 180:2355 + 0x09, 0x3D, 0x17, 0x09, // 181:2365 + 0x09, 0x54, 0x19, 0x09, // 182:2388 + 0x09, 0x6D, 0x08, 0x05, // 183:2413 + 0x09, 0x75, 0x0C, 0x05, // 184:2421 + 0x09, 0x81, 0x0B, 0x05, // 185:2433 + 0x09, 0x8C, 0x0D, 0x06, // 186:2444 + 0x09, 0x99, 0x17, 0x09, // 187:2457 + 0x09, 0xB0, 0x26, 0x0D, // 188:2480 + 0x09, 0xD6, 0x26, 0x0D, // 189:2518 + 0x09, 0xFC, 0x26, 0x0D, // 190:2556 + 0x0A, 0x22, 0x1A, 0x0A, // 191:2594 + 0x0A, 0x3C, 0x1D, 0x0B, // 192:2620 + 0x0A, 0x59, 0x1D, 0x0B, // 193:2649 + 0x0A, 0x76, 0x1D, 0x0B, // 194:2678 + 0x0A, 0x93, 0x1D, 0x0B, // 195:2707 + 0x0A, 0xB0, 0x1D, 0x0B, // 196:2736 + 0x0A, 0xCD, 0x1D, 0x0B, // 197:2765 + 0x0A, 0xEA, 0x2C, 0x10, // 198:2794 + 0x0B, 0x16, 0x20, 0x0C, // 199:2838 + 0x0B, 0x36, 0x1D, 0x0B, // 200:2870 + 0x0B, 0x53, 0x1D, 0x0B, // 201:2899 + 0x0B, 0x70, 0x1D, 0x0B, // 202:2928 + 0x0B, 0x8D, 0x1D, 0x0B, // 203:2957 + 0x0B, 0xAA, 0x05, 0x04, // 204:2986 + 0x0B, 0xAF, 0x07, 0x04, // 205:2991 + 0x0B, 0xB6, 0x0A, 0x04, // 206:2998 + 0x0B, 0xC0, 0x07, 0x04, // 207:3008 + 0x0B, 0xC7, 0x20, 0x0C, // 208:3015 + 0x0B, 0xE7, 0x1D, 0x0C, // 209:3047 + 0x0C, 0x04, 0x20, 0x0C, // 210:3076 + 0x0C, 0x24, 0x20, 0x0C, // 211:3108 + 0x0C, 0x44, 0x20, 0x0C, // 212:3140 + 0x0C, 0x64, 0x20, 0x0C, // 213:3172 + 0x0C, 0x84, 0x20, 0x0C, // 214:3204 + 0x0C, 0xA4, 0x17, 0x09, // 215:3236 + 0x0C, 0xBB, 0x20, 0x0C, // 216:3259 + 0x0C, 0xDB, 0x1D, 0x0C, // 217:3291 + 0x0C, 0xF8, 0x1D, 0x0C, // 218:3320 + 0x0D, 0x15, 0x1D, 0x0C, // 219:3349 + 0x0D, 0x32, 0x1D, 0x0C, // 220:3378 + 0x0D, 0x4F, 0x19, 0x0B, // 221:3407 + 0x0D, 0x68, 0x1D, 0x0B, // 222:3432 + 0x0D, 0x85, 0x17, 0x0A, // 223:3461 + 0x0D, 0x9C, 0x17, 0x09, // 224:3484 + 0x0D, 0xB3, 0x17, 0x09, // 225:3507 + 0x0D, 0xCA, 0x17, 0x09, // 226:3530 + 0x0D, 0xE1, 0x17, 0x09, // 227:3553 + 0x0D, 0xF8, 0x17, 0x09, // 228:3576 + 0x0E, 0x0F, 0x17, 0x09, // 229:3599 + 0x0E, 0x26, 0x29, 0x0E, // 230:3622 + 0x0E, 0x4F, 0x14, 0x08, // 231:3663 + 0x0E, 0x63, 0x17, 0x09, // 232:3683 + 0x0E, 0x7A, 0x17, 0x09, // 233:3706 + 0x0E, 0x91, 0x17, 0x09, // 234:3729 + 0x0E, 0xA8, 0x17, 0x09, // 235:3752 + 0x0E, 0xBF, 0x05, 0x04, // 236:3775 + 0x0E, 0xC4, 0x07, 0x04, // 237:3780 + 0x0E, 0xCB, 0x0A, 0x04, // 238:3787 + 0x0E, 0xD5, 0x07, 0x04, // 239:3797 + 0x0E, 0xDC, 0x17, 0x09, // 240:3804 + 0x0E, 0xF3, 0x14, 0x09, // 241:3827 + 0x0F, 0x07, 0x17, 0x09, // 242:3847 + 0x0F, 0x1E, 0x17, 0x09, // 243:3870 + 0x0F, 0x35, 0x17, 0x09, // 244:3893 + 0x0F, 0x4C, 0x17, 0x09, // 245:3916 + 0x0F, 0x63, 0x17, 0x09, // 246:3939 + 0x0F, 0x7A, 0x17, 0x09, // 247:3962 + 0x0F, 0x91, 0x17, 0x0A, // 248:3985 + 0x0F, 0xA8, 0x14, 0x09, // 249:4008 + 0x0F, 0xBC, 0x14, 0x09, // 250:4028 + 0x0F, 0xD0, 0x14, 0x09, // 251:4048 + 0x0F, 0xE4, 0x14, 0x09, // 252:4068 + 0x0F, 0xF8, 0x13, 0x08, // 253:4088 + 0x10, 0x0B, 0x17, 0x09, // 254:4107 + 0x10, 0x22, 0x13, 0x08, // 255:4130 + + // Font Data: + 0x00,0x00,0x00,0x00,0x00,0x00,0xF8,0x5F, // 33 + 0x00,0x00,0x00,0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x78, // 34 + 0x80,0x08,0x00,0x80,0x78,0x00,0xC0,0x0F,0x00,0xB8,0x08,0x00,0x80,0x08,0x00,0x80,0x78,0x00,0xC0,0x0F,0x00,0xB8,0x08,0x00,0x80,0x08, // 35 + 0x00,0x00,0x00,0xE0,0x10,0x00,0x10,0x21,0x00,0x08,0x41,0x00,0xFC,0xFF,0x00,0x08,0x42,0x00,0x10,0x22,0x00,0x20,0x1C, // 36 + 0x00,0x00,0x00,0xF0,0x00,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0x08,0x61,0x00,0xF0,0x18,0x00,0x00,0x06,0x00,0xC0,0x01,0x00,0x30,0x3C,0x00,0x08,0x42,0x00,0x00,0x42,0x00,0x00,0x42,0x00,0x00,0x3C, // 37 + 0x00,0x00,0x00,0x00,0x1C,0x00,0x70,0x22,0x00,0x88,0x41,0x00,0x08,0x43,0x00,0x88,0x44,0x00,0x70,0x28,0x00,0x00,0x10,0x00,0x00,0x28,0x00,0x00,0x44, // 38 + 0x00,0x00,0x00,0x78, // 39 + 0x00,0x00,0x00,0x80,0x3F,0x00,0x70,0xC0,0x01,0x08,0x00,0x02, // 40 + 0x00,0x00,0x00,0x08,0x00,0x02,0x70,0xC0,0x01,0x80,0x3F, // 41 + 0x10,0x00,0x00,0xD0,0x00,0x00,0x38,0x00,0x00,0xD0,0x00,0x00,0x10, // 42 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0xC0,0x1F,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02, // 43 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x01, // 44 + 0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08, // 45 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40, // 46 + 0x00,0x60,0x00,0x00,0x1E,0x00,0xE0,0x01,0x00,0x18, // 47 + 0x00,0x00,0x00,0xE0,0x1F,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0xE0,0x1F, // 48 + 0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0x00,0x10,0x00,0x00,0xF8,0x7F, // 49 + 0x00,0x00,0x00,0x20,0x40,0x00,0x10,0x60,0x00,0x08,0x50,0x00,0x08,0x48,0x00,0x08,0x44,0x00,0x10,0x43,0x00,0xE0,0x40, // 50 + 0x00,0x00,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x88,0x41,0x00,0xF0,0x22,0x00,0x00,0x1C, // 51 + 0x00,0x0C,0x00,0x00,0x0A,0x00,0x00,0x09,0x00,0xC0,0x08,0x00,0x20,0x08,0x00,0x10,0x08,0x00,0xF8,0x7F,0x00,0x00,0x08, // 52 + 0x00,0x00,0x00,0xC0,0x11,0x00,0xB8,0x20,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x08,0x21,0x00,0x08,0x1E, // 53 + 0x00,0x00,0x00,0xE0,0x1F,0x00,0x10,0x21,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x88,0x40,0x00,0x10,0x21,0x00,0x20,0x1E, // 54 + 0x00,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x78,0x00,0x08,0x07,0x00,0xC8,0x00,0x00,0x28,0x00,0x00,0x18, // 55 + 0x00,0x00,0x00,0x60,0x1C,0x00,0x90,0x22,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x90,0x22,0x00,0x60,0x1C, // 56 + 0x00,0x00,0x00,0xE0,0x11,0x00,0x10,0x22,0x00,0x08,0x44,0x00,0x08,0x44,0x00,0x08,0x44,0x00,0x10,0x22,0x00,0xE0,0x1F, // 57 + 0x00,0x00,0x00,0x40,0x40, // 58 + 0x00,0x00,0x00,0x40,0xC0,0x01, // 59 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x05,0x00,0x00,0x05,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x40,0x10, // 60 + 0x00,0x00,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08, // 61 + 0x00,0x00,0x00,0x40,0x10,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x80,0x08,0x00,0x00,0x05,0x00,0x00,0x05,0x00,0x00,0x02, // 62 + 0x00,0x00,0x00,0x60,0x00,0x00,0x10,0x00,0x00,0x08,0x00,0x00,0x08,0x5C,0x00,0x08,0x02,0x00,0x10,0x01,0x00,0xE0, // 63 + 0x00,0x00,0x00,0x00,0x3F,0x00,0xC0,0x40,0x00,0x20,0x80,0x00,0x10,0x1E,0x01,0x10,0x21,0x01,0x88,0x40,0x02,0x48,0x40,0x02,0x48,0x40,0x02,0x48,0x20,0x02,0x88,0x7C,0x02,0xC8,0x43,0x02,0x10,0x40,0x02,0x10,0x20,0x01,0x60,0x10,0x01,0x80,0x8F, // 64 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x70,0x04,0x00,0x08,0x04,0x00,0x70,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 65 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x90,0x22,0x00,0x60,0x1C, // 66 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10, // 67 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 68 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 69 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08, // 70 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x10,0x22,0x00,0x20,0x12,0x00,0x00,0x0E, // 71 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0xF8,0x7F, // 72 + 0x00,0x00,0x00,0xF8,0x7F, // 73 + 0x00,0x00,0x00,0x00,0x38,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0xF8,0x3F, // 74 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x04,0x00,0x00,0x02,0x00,0x00,0x01,0x00,0x80,0x03,0x00,0x40,0x04,0x00,0x20,0x18,0x00,0x10,0x20,0x00,0x08,0x40, // 75 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40, // 76 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x30,0x00,0x00,0xC0,0x00,0x00,0x00,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x00,0x03,0x00,0xC0,0x00,0x00,0x30,0x00,0x00,0xF8,0x7F, // 77 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x10,0x00,0x00,0x60,0x00,0x00,0x80,0x00,0x00,0x00,0x03,0x00,0x00,0x04,0x00,0x00,0x18,0x00,0x00,0x20,0x00,0xF8,0x7F, // 78 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 79 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x10,0x01,0x00,0xE0, // 80 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x08,0x50,0x00,0x08,0x50,0x00,0x10,0x20,0x00,0x20,0x70,0x00,0xC0,0x4F, // 81 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x02,0x00,0x08,0x06,0x00,0x08,0x1A,0x00,0x10,0x21,0x00,0xE0,0x40, // 82 + 0x00,0x00,0x00,0x60,0x10,0x00,0x90,0x20,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x10,0x22,0x00,0x20,0x1C, // 83 + 0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0xF8,0x7F,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08, // 84 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 85 + 0x00,0x00,0x00,0x18,0x00,0x00,0xE0,0x00,0x00,0x00,0x07,0x00,0x00,0x18,0x00,0x00,0x60,0x00,0x00,0x18,0x00,0x00,0x07,0x00,0xE0,0x00,0x00,0x18, // 86 + 0x18,0x00,0x00,0xE0,0x01,0x00,0x00,0x1E,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x03,0x00,0x70,0x00,0x00,0x08,0x00,0x00,0x70,0x00,0x00,0x80,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1E,0x00,0xE0,0x01,0x00,0x18, // 87 + 0x00,0x40,0x00,0x08,0x20,0x00,0x10,0x10,0x00,0x60,0x0C,0x00,0x80,0x02,0x00,0x00,0x01,0x00,0x80,0x02,0x00,0x60,0x0C,0x00,0x10,0x10,0x00,0x08,0x20,0x00,0x00,0x40, // 88 + 0x08,0x00,0x00,0x30,0x00,0x00,0x40,0x00,0x00,0x80,0x01,0x00,0x00,0x7E,0x00,0x80,0x01,0x00,0x40,0x00,0x00,0x30,0x00,0x00,0x08, // 89 + 0x00,0x40,0x00,0x08,0x60,0x00,0x08,0x58,0x00,0x08,0x44,0x00,0x08,0x43,0x00,0x88,0x40,0x00,0x68,0x40,0x00,0x18,0x40,0x00,0x08,0x40, // 90 + 0x00,0x00,0x00,0xF8,0xFF,0x03,0x08,0x00,0x02,0x08,0x00,0x02, // 91 + 0x18,0x00,0x00,0xE0,0x01,0x00,0x00,0x1E,0x00,0x00,0x60, // 92 + 0x08,0x00,0x02,0x08,0x00,0x02,0xF8,0xFF,0x03, // 93 + 0x00,0x01,0x00,0xC0,0x00,0x00,0x30,0x00,0x00,0x08,0x00,0x00,0x30,0x00,0x00,0xC0,0x00,0x00,0x00,0x01, // 94 + 0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02, // 95 + 0x00,0x00,0x00,0x08,0x00,0x00,0x10, // 96 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 97 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 98 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20, // 99 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0xF8,0x7F, // 100 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 101 + 0x40,0x00,0x00,0xF0,0x7F,0x00,0x48,0x00,0x00,0x48, // 102 + 0x00,0x00,0x00,0x00,0x1F,0x01,0x80,0x20,0x02,0x40,0x40,0x02,0x40,0x40,0x02,0x40,0x40,0x02,0x80,0x20,0x01,0xC0,0xFF, // 103 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F, // 104 + 0x00,0x00,0x00,0xC8,0x7F, // 105 + 0x00,0x00,0x02,0xC8,0xFF,0x01, // 106 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x00,0x08,0x00,0x00,0x04,0x00,0x00,0x06,0x00,0x00,0x19,0x00,0x80,0x20,0x00,0x40,0x40, // 107 + 0x00,0x00,0x00,0xF8,0x7F, // 108 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F, // 109 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x80,0x7F, // 110 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 111 + 0x00,0x00,0x00,0xC0,0xFF,0x03,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 112 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0xC0,0xFF,0x03, // 113 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x80,0x00,0x00,0x40,0x00,0x00,0x40, // 114 + 0x00,0x00,0x00,0x80,0x23,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x80,0x38, // 115 + 0x40,0x00,0x00,0xF0,0x7F,0x00,0x40,0x40,0x00,0x40,0x40, // 116 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 117 + 0xC0,0x00,0x00,0x00,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x00,0x03,0x00,0xC0, // 118 + 0xC0,0x00,0x00,0x00,0x1F,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x00,0x03,0x00,0xC0,0x00,0x00,0x00,0x03,0x00,0x00,0x1C,0x00,0x00,0x60,0x00,0x00,0x1F,0x00,0xC0, // 119 + 0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1B,0x00,0x00,0x04,0x00,0x00,0x1B,0x00,0x80,0x20,0x00,0x40,0x40, // 120 + 0xC0,0x01,0x00,0x00,0x06,0x02,0x00,0x38,0x02,0x00,0xE0,0x01,0x00,0x38,0x00,0x00,0x07,0x00,0xC0, // 121 + 0x40,0x40,0x00,0x40,0x60,0x00,0x40,0x58,0x00,0x40,0x44,0x00,0x40,0x43,0x00,0xC0,0x40,0x00,0x40,0x40, // 122 + 0x00,0x04,0x00,0x00,0x04,0x00,0xF0,0xFB,0x01,0x08,0x00,0x02,0x08,0x00,0x02, // 123 + 0x00,0x00,0x00,0xF8,0xFF,0x03, // 124 + 0x08,0x00,0x02,0x08,0x00,0x02,0xF0,0xFB,0x01,0x00,0x04,0x00,0x00,0x04, // 125 + 0x00,0x02,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x01,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x01, // 126 + 0x00,0x00,0x00,0x00,0x00,0x00,0x40,0xFF,0x03, // 161 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x03,0x40,0xF0,0x00,0x40,0x4E,0x00,0xC0,0x41,0x00,0xB8,0x20,0x00,0x00,0x11, // 162 + 0x00,0x41,0x00,0xE0,0x31,0x00,0x10,0x2F,0x00,0x08,0x21,0x00,0x08,0x21,0x00,0x08,0x40,0x00,0x10,0x40,0x00,0x20,0x20, // 163 + 0x00,0x00,0x00,0x40,0x0B,0x00,0x80,0x04,0x00,0x40,0x08,0x00,0x40,0x08,0x00,0x80,0x04,0x00,0x40,0x0B, // 164 + 0x08,0x0A,0x00,0x10,0x0A,0x00,0x60,0x0A,0x00,0x80,0x0B,0x00,0x00,0x7E,0x00,0x80,0x0B,0x00,0x60,0x0A,0x00,0x10,0x0A,0x00,0x08,0x0A, // 165 + 0x00,0x00,0x00,0xF8,0xF1,0x03, // 166 + 0x00,0x86,0x00,0x70,0x09,0x01,0xC8,0x10,0x02,0x88,0x10,0x02,0x08,0x21,0x02,0x08,0x61,0x02,0x30,0xD2,0x01,0x00,0x0C, // 167 + 0x08,0x00,0x00,0x00,0x00,0x00,0x08, // 168 + 0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0xC8,0x47,0x00,0x28,0x48,0x00,0x28,0x48,0x00,0x28,0x48,0x00,0x28,0x48,0x00,0x48,0x44,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 169 + 0xD0,0x00,0x00,0x48,0x01,0x00,0x28,0x01,0x00,0x28,0x01,0x00,0xF0,0x01, // 170 + 0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x1B,0x00,0x80,0x20,0x00,0x00,0x04,0x00,0x00,0x1B,0x00,0x80,0x20, // 171 + 0x00,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x00,0x00,0x80,0x0F, // 172 + 0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08,0x00,0x00,0x08, // 173 + 0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0xE8,0x4F,0x00,0x28,0x41,0x00,0x28,0x41,0x00,0x28,0x43,0x00,0x28,0x45,0x00,0xC8,0x48,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 174 + 0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04,0x00,0x00,0x04, // 175 + 0x00,0x00,0x00,0x30,0x00,0x00,0x48,0x00,0x00,0x48,0x00,0x00,0x30, // 176 + 0x00,0x00,0x00,0x00,0x41,0x00,0x00,0x41,0x00,0x00,0x41,0x00,0xE0,0x4F,0x00,0x00,0x41,0x00,0x00,0x41,0x00,0x00,0x41, // 177 + 0x10,0x01,0x00,0x88,0x01,0x00,0x48,0x01,0x00,0x48,0x01,0x00,0x30,0x01, // 178 + 0x90,0x00,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0x28,0x01,0x00,0xD8, // 179 + 0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x08, // 180 + 0x00,0x00,0x00,0xC0,0xFF,0x03,0x00,0x20,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 181 + 0xF0,0x00,0x00,0xF8,0x00,0x00,0xF8,0x01,0x00,0xF8,0x01,0x00,0xF8,0xFF,0x03,0x08,0x00,0x00,0x08,0x00,0x00,0xF8,0xFF,0x03,0x08, // 182 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02, // 183 + 0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x80,0x02,0x00,0x00,0x03, // 184 + 0x00,0x00,0x00,0x10,0x00,0x00,0x08,0x00,0x00,0xF8,0x01, // 185 + 0xF0,0x00,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0x08,0x01,0x00,0xF0, // 186 + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x20,0x00,0x00,0x1B,0x00,0x00,0x04,0x00,0x80,0x20,0x00,0x00,0x1B,0x00,0x00,0x04, // 187 + 0x00,0x00,0x00,0x10,0x00,0x00,0x08,0x40,0x00,0xF8,0x21,0x00,0x00,0x10,0x00,0x00,0x0C,0x00,0x00,0x02,0x00,0x80,0x01,0x00,0x40,0x30,0x00,0x30,0x28,0x00,0x08,0x24,0x00,0x00,0x7E,0x00,0x00,0x20, // 188 + 0x00,0x00,0x00,0x10,0x00,0x00,0x08,0x40,0x00,0xF8,0x31,0x00,0x00,0x08,0x00,0x00,0x04,0x00,0x00,0x03,0x00,0x80,0x00,0x00,0x60,0x44,0x00,0x10,0x62,0x00,0x08,0x52,0x00,0x00,0x52,0x00,0x00,0x4C, // 189 + 0x90,0x00,0x00,0x08,0x01,0x00,0x08,0x41,0x00,0x28,0x21,0x00,0xD8,0x18,0x00,0x00,0x04,0x00,0x00,0x03,0x00,0x80,0x00,0x00,0x40,0x30,0x00,0x30,0x28,0x00,0x08,0x24,0x00,0x00,0x7E,0x00,0x00,0x20, // 190 + 0x00,0x00,0x00,0x00,0xE0,0x00,0x00,0x10,0x01,0x00,0x08,0x02,0x40,0x07,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x01,0x00,0xC0, // 191 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x71,0x04,0x00,0x0A,0x04,0x00,0x70,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 192 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x70,0x04,0x00,0x0A,0x04,0x00,0x71,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 193 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x72,0x04,0x00,0x09,0x04,0x00,0x71,0x04,0x00,0x82,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 194 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x72,0x04,0x00,0x09,0x04,0x00,0x72,0x04,0x00,0x81,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 195 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x72,0x04,0x00,0x08,0x04,0x00,0x72,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 196 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x1C,0x00,0x80,0x07,0x00,0x7E,0x04,0x00,0x0A,0x04,0x00,0x7E,0x04,0x00,0x80,0x07,0x00,0x00,0x1C,0x00,0x00,0x60, // 197 + 0x00,0x60,0x00,0x00,0x18,0x00,0x00,0x06,0x00,0x80,0x05,0x00,0x60,0x04,0x00,0x18,0x04,0x00,0x08,0x04,0x00,0x08,0x04,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41, // 198 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x08,0x40,0x02,0x08,0xC0,0x02,0x08,0x40,0x03,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10, // 199 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x09,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 200 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x09,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 201 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x09,0x41,0x00,0x09,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 202 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x0A,0x41,0x00,0x08,0x41,0x00,0x08,0x41,0x00,0x08,0x40, // 203 + 0x01,0x00,0x00,0xFA,0x7F, // 204 + 0x00,0x00,0x00,0xFA,0x7F,0x00,0x01, // 205 + 0x02,0x00,0x00,0xF9,0x7F,0x00,0x01,0x00,0x00,0x02, // 206 + 0x02,0x00,0x00,0xF8,0x7F,0x00,0x02, // 207 + 0x00,0x02,0x00,0xF8,0x7F,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x08,0x42,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 208 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x10,0x00,0x00,0x60,0x00,0x00,0x82,0x00,0x00,0x01,0x03,0x00,0x02,0x04,0x00,0x01,0x18,0x00,0x00,0x20,0x00,0xF8,0x7F, // 209 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x09,0x40,0x00,0x0A,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 210 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 211 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x09,0x40,0x00,0x0A,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 212 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x0A,0x40,0x00,0x09,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 213 + 0x00,0x00,0x00,0xC0,0x0F,0x00,0x20,0x10,0x00,0x10,0x20,0x00,0x08,0x40,0x00,0x0A,0x40,0x00,0x08,0x40,0x00,0x0A,0x40,0x00,0x10,0x20,0x00,0x20,0x10,0x00,0xC0,0x0F, // 214 + 0x00,0x00,0x00,0x40,0x10,0x00,0x80,0x08,0x00,0x00,0x05,0x00,0x00,0x07,0x00,0x00,0x05,0x00,0x80,0x08,0x00,0x40,0x10, // 215 + 0x00,0x00,0x00,0xC0,0x4F,0x00,0x20,0x30,0x00,0x10,0x30,0x00,0x08,0x4C,0x00,0x08,0x42,0x00,0x08,0x41,0x00,0xC8,0x40,0x00,0x30,0x20,0x00,0x30,0x10,0x00,0xC8,0x0F, // 216 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x01,0x40,0x00,0x02,0x40,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 217 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x01,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 218 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x01,0x40,0x00,0x01,0x40,0x00,0x02,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 219 + 0x00,0x00,0x00,0xF8,0x1F,0x00,0x00,0x20,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x00,0x40,0x00,0x02,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xF8,0x1F, // 220 + 0x08,0x00,0x00,0x30,0x00,0x00,0x40,0x00,0x00,0x80,0x01,0x00,0x02,0x7E,0x00,0x81,0x01,0x00,0x40,0x00,0x00,0x30,0x00,0x00,0x08, // 221 + 0x00,0x00,0x00,0xF8,0x7F,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x20,0x10,0x00,0x40,0x08,0x00,0x80,0x07, // 222 + 0x00,0x00,0x00,0xE0,0x7F,0x00,0x10,0x00,0x00,0x08,0x20,0x00,0x88,0x43,0x00,0x70,0x42,0x00,0x00,0x44,0x00,0x00,0x38, // 223 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x48,0x44,0x00,0x50,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 224 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x50,0x44,0x00,0x48,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 225 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x48,0x42,0x00,0x50,0x22,0x00,0x80,0x7F, // 226 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x50,0x42,0x00,0x48,0x22,0x00,0x80,0x7F, // 227 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x50,0x44,0x00,0x40,0x44,0x00,0x50,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 228 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x5C,0x44,0x00,0x54,0x44,0x00,0x5C,0x42,0x00,0x40,0x22,0x00,0x80,0x7F, // 229 + 0x00,0x00,0x00,0x00,0x39,0x00,0x80,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x42,0x00,0x40,0x22,0x00,0x80,0x3F,0x00,0x80,0x24,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x40,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 230 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x02,0x40,0xC0,0x02,0x40,0x40,0x03,0x80,0x20, // 231 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x48,0x44,0x00,0x50,0x44,0x00,0x40,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 232 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x40,0x44,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 233 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x50,0x44,0x00,0x48,0x44,0x00,0x48,0x44,0x00,0x90,0x24,0x00,0x00,0x17, // 234 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x24,0x00,0x50,0x44,0x00,0x40,0x44,0x00,0x50,0x44,0x00,0x80,0x24,0x00,0x00,0x17, // 235 + 0x08,0x00,0x00,0xD0,0x7F, // 236 + 0x00,0x00,0x00,0xD0,0x7F,0x00,0x08, // 237 + 0x10,0x00,0x00,0xC8,0x7F,0x00,0x08,0x00,0x00,0x10, // 238 + 0x10,0x00,0x00,0xC0,0x7F,0x00,0x10, // 239 + 0x00,0x00,0x00,0x00,0x1F,0x00,0xA0,0x20,0x00,0x68,0x40,0x00,0x58,0x40,0x00,0x70,0x40,0x00,0xE8,0x20,0x00,0x00,0x1F, // 240 + 0x00,0x00,0x00,0xC0,0x7F,0x00,0x90,0x00,0x00,0x48,0x00,0x00,0x50,0x00,0x00,0x48,0x00,0x00,0x80,0x7F, // 241 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x48,0x40,0x00,0x50,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 242 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x40,0x40,0x00,0x50,0x40,0x00,0x48,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 243 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x50,0x40,0x00,0x48,0x40,0x00,0x48,0x40,0x00,0x90,0x20,0x00,0x00,0x1F, // 244 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x50,0x40,0x00,0x48,0x40,0x00,0x50,0x40,0x00,0x88,0x20,0x00,0x00,0x1F, // 245 + 0x00,0x00,0x00,0x00,0x1F,0x00,0x80,0x20,0x00,0x50,0x40,0x00,0x40,0x40,0x00,0x50,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 246 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x80,0x0A,0x00,0x00,0x02,0x00,0x00,0x02,0x00,0x00,0x02, // 247 + 0x00,0x00,0x00,0x00,0x5F,0x00,0x80,0x30,0x00,0x40,0x48,0x00,0x40,0x44,0x00,0x40,0x42,0x00,0x80,0x21,0x00,0x40,0x1F, // 248 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x00,0x40,0x00,0x08,0x40,0x00,0x10,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 249 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x00,0x40,0x00,0x00,0x40,0x00,0x10,0x40,0x00,0x08,0x20,0x00,0xC0,0x7F, // 250 + 0x00,0x00,0x00,0xC0,0x3F,0x00,0x10,0x40,0x00,0x08,0x40,0x00,0x08,0x40,0x00,0x10,0x20,0x00,0xC0,0x7F, // 251 + 0x00,0x00,0x00,0xD0,0x3F,0x00,0x00,0x40,0x00,0x10,0x40,0x00,0x00,0x40,0x00,0x00,0x20,0x00,0xC0,0x7F, // 252 + 0xC0,0x01,0x00,0x00,0x06,0x02,0x00,0x38,0x02,0x10,0xE0,0x01,0x08,0x38,0x00,0x00,0x07,0x00,0xC0, // 253 + 0x00,0x00,0x00,0xF8,0xFF,0x03,0x80,0x20,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x40,0x40,0x00,0x80,0x20,0x00,0x00,0x1F, // 254 + 0xC0,0x01,0x00,0x00,0x06,0x02,0x10,0x38,0x02,0x00,0xE0,0x01,0x10,0x38,0x00,0x00,0x07,0x00,0xC0 // 255 +}; + +const uint8_t ArialMT_Plain_24[] PROGMEM = { + 0x18, // Width: 24 + 0x1C, // Height: 28 + 0x20, // First Char: 32 + 0xE0, // Numbers of Chars: 224 + + // Jump Table: + 0xFF, 0xFF, 0x00, 0x07, // 32:65535 + 0x00, 0x00, 0x13, 0x07, // 33:0 + 0x00, 0x13, 0x1A, 0x09, // 34:19 + 0x00, 0x2D, 0x33, 0x0D, // 35:45 + 0x00, 0x60, 0x2F, 0x0D, // 36:96 + 0x00, 0x8F, 0x4F, 0x15, // 37:143 + 0x00, 0xDE, 0x3B, 0x10, // 38:222 + 0x01, 0x19, 0x0A, 0x05, // 39:281 + 0x01, 0x23, 0x1C, 0x08, // 40:291 + 0x01, 0x3F, 0x1B, 0x08, // 41:319 + 0x01, 0x5A, 0x21, 0x09, // 42:346 + 0x01, 0x7B, 0x32, 0x0E, // 43:379 + 0x01, 0xAD, 0x10, 0x07, // 44:429 + 0x01, 0xBD, 0x1B, 0x08, // 45:445 + 0x01, 0xD8, 0x0F, 0x07, // 46:472 + 0x01, 0xE7, 0x19, 0x07, // 47:487 + 0x02, 0x00, 0x2F, 0x0D, // 48:512 + 0x02, 0x2F, 0x23, 0x0D, // 49:559 + 0x02, 0x52, 0x2F, 0x0D, // 50:594 + 0x02, 0x81, 0x2F, 0x0D, // 51:641 + 0x02, 0xB0, 0x2F, 0x0D, // 52:688 + 0x02, 0xDF, 0x2F, 0x0D, // 53:735 + 0x03, 0x0E, 0x2F, 0x0D, // 54:782 + 0x03, 0x3D, 0x2D, 0x0D, // 55:829 + 0x03, 0x6A, 0x2F, 0x0D, // 56:874 + 0x03, 0x99, 0x2F, 0x0D, // 57:921 + 0x03, 0xC8, 0x0F, 0x07, // 58:968 + 0x03, 0xD7, 0x10, 0x07, // 59:983 + 0x03, 0xE7, 0x2F, 0x0E, // 60:999 + 0x04, 0x16, 0x2F, 0x0E, // 61:1046 + 0x04, 0x45, 0x2E, 0x0E, // 62:1093 + 0x04, 0x73, 0x2E, 0x0D, // 63:1139 + 0x04, 0xA1, 0x5B, 0x18, // 64:1185 + 0x04, 0xFC, 0x3B, 0x10, // 65:1276 + 0x05, 0x37, 0x3B, 0x10, // 66:1335 + 0x05, 0x72, 0x3F, 0x11, // 67:1394 + 0x05, 0xB1, 0x3F, 0x11, // 68:1457 + 0x05, 0xF0, 0x3B, 0x10, // 69:1520 + 0x06, 0x2B, 0x35, 0x0F, // 70:1579 + 0x06, 0x60, 0x43, 0x13, // 71:1632 + 0x06, 0xA3, 0x3B, 0x11, // 72:1699 + 0x06, 0xDE, 0x0F, 0x07, // 73:1758 + 0x06, 0xED, 0x27, 0x0C, // 74:1773 + 0x07, 0x14, 0x3F, 0x10, // 75:1812 + 0x07, 0x53, 0x2F, 0x0D, // 76:1875 + 0x07, 0x82, 0x43, 0x14, // 77:1922 + 0x07, 0xC5, 0x3B, 0x11, // 78:1989 + 0x08, 0x00, 0x47, 0x13, // 79:2048 + 0x08, 0x47, 0x3A, 0x10, // 80:2119 + 0x08, 0x81, 0x47, 0x13, // 81:2177 + 0x08, 0xC8, 0x3F, 0x11, // 82:2248 + 0x09, 0x07, 0x3B, 0x10, // 83:2311 + 0x09, 0x42, 0x35, 0x0F, // 84:2370 + 0x09, 0x77, 0x3B, 0x11, // 85:2423 + 0x09, 0xB2, 0x39, 0x10, // 86:2482 + 0x09, 0xEB, 0x59, 0x17, // 87:2539 + 0x0A, 0x44, 0x3B, 0x10, // 88:2628 + 0x0A, 0x7F, 0x3D, 0x10, // 89:2687 + 0x0A, 0xBC, 0x37, 0x0F, // 90:2748 + 0x0A, 0xF3, 0x14, 0x07, // 91:2803 + 0x0B, 0x07, 0x1B, 0x07, // 92:2823 + 0x0B, 0x22, 0x18, 0x07, // 93:2850 + 0x0B, 0x3A, 0x2A, 0x0B, // 94:2874 + 0x0B, 0x64, 0x34, 0x0D, // 95:2916 + 0x0B, 0x98, 0x11, 0x08, // 96:2968 + 0x0B, 0xA9, 0x2F, 0x0D, // 97:2985 + 0x0B, 0xD8, 0x33, 0x0D, // 98:3032 + 0x0C, 0x0B, 0x2B, 0x0C, // 99:3083 + 0x0C, 0x36, 0x2F, 0x0D, // 100:3126 + 0x0C, 0x65, 0x2F, 0x0D, // 101:3173 + 0x0C, 0x94, 0x1A, 0x07, // 102:3220 + 0x0C, 0xAE, 0x2F, 0x0D, // 103:3246 + 0x0C, 0xDD, 0x2F, 0x0D, // 104:3293 + 0x0D, 0x0C, 0x0F, 0x05, // 105:3340 + 0x0D, 0x1B, 0x10, 0x05, // 106:3355 + 0x0D, 0x2B, 0x2F, 0x0C, // 107:3371 + 0x0D, 0x5A, 0x0F, 0x05, // 108:3418 + 0x0D, 0x69, 0x47, 0x14, // 109:3433 + 0x0D, 0xB0, 0x2F, 0x0D, // 110:3504 + 0x0D, 0xDF, 0x2F, 0x0D, // 111:3551 + 0x0E, 0x0E, 0x33, 0x0D, // 112:3598 + 0x0E, 0x41, 0x30, 0x0D, // 113:3649 + 0x0E, 0x71, 0x1E, 0x08, // 114:3697 + 0x0E, 0x8F, 0x2B, 0x0C, // 115:3727 + 0x0E, 0xBA, 0x1B, 0x07, // 116:3770 + 0x0E, 0xD5, 0x2F, 0x0D, // 117:3797 + 0x0F, 0x04, 0x2A, 0x0C, // 118:3844 + 0x0F, 0x2E, 0x42, 0x11, // 119:3886 + 0x0F, 0x70, 0x2B, 0x0C, // 120:3952 + 0x0F, 0x9B, 0x2A, 0x0C, // 121:3995 + 0x0F, 0xC5, 0x2B, 0x0C, // 122:4037 + 0x0F, 0xF0, 0x1C, 0x08, // 123:4080 + 0x10, 0x0C, 0x10, 0x06, // 124:4108 + 0x10, 0x1C, 0x1B, 0x08, // 125:4124 + 0x10, 0x37, 0x32, 0x0E, // 126:4151 + 0xFF, 0xFF, 0x00, 0x00, // 127:65535 + 0xFF, 0xFF, 0x00, 0x18, // 128:65535 + 0xFF, 0xFF, 0x00, 0x18, // 129:65535 + 0xFF, 0xFF, 0x00, 0x18, // 130:65535 + 0xFF, 0xFF, 0x00, 0x18, // 131:65535 + 0xFF, 0xFF, 0x00, 0x18, // 132:65535 + 0xFF, 0xFF, 0x00, 0x18, // 133:65535 + 0xFF, 0xFF, 0x00, 0x18, // 134:65535 + 0xFF, 0xFF, 0x00, 0x18, // 135:65535 + 0xFF, 0xFF, 0x00, 0x18, // 136:65535 + 0xFF, 0xFF, 0x00, 0x18, // 137:65535 + 0xFF, 0xFF, 0x00, 0x18, // 138:65535 + 0xFF, 0xFF, 0x00, 0x18, // 139:65535 + 0xFF, 0xFF, 0x00, 0x18, // 140:65535 + 0xFF, 0xFF, 0x00, 0x18, // 141:65535 + 0xFF, 0xFF, 0x00, 0x18, // 142:65535 + 0xFF, 0xFF, 0x00, 0x18, // 143:65535 + 0xFF, 0xFF, 0x00, 0x18, // 144:65535 + 0xFF, 0xFF, 0x00, 0x18, // 145:65535 + 0xFF, 0xFF, 0x00, 0x18, // 146:65535 + 0xFF, 0xFF, 0x00, 0x18, // 147:65535 + 0xFF, 0xFF, 0x00, 0x18, // 148:65535 + 0xFF, 0xFF, 0x00, 0x18, // 149:65535 + 0xFF, 0xFF, 0x00, 0x18, // 150:65535 + 0xFF, 0xFF, 0x00, 0x18, // 151:65535 + 0xFF, 0xFF, 0x00, 0x18, // 152:65535 + 0xFF, 0xFF, 0x00, 0x18, // 153:65535 + 0xFF, 0xFF, 0x00, 0x18, // 154:65535 + 0xFF, 0xFF, 0x00, 0x18, // 155:65535 + 0xFF, 0xFF, 0x00, 0x18, // 156:65535 + 0xFF, 0xFF, 0x00, 0x18, // 157:65535 + 0xFF, 0xFF, 0x00, 0x18, // 158:65535 + 0xFF, 0xFF, 0x00, 0x18, // 159:65535 + 0xFF, 0xFF, 0x00, 0x07, // 160:65535 + 0x10, 0x69, 0x14, 0x08, // 161:4201 + 0x10, 0x7D, 0x2B, 0x0D, // 162:4221 + 0x10, 0xA8, 0x2F, 0x0D, // 163:4264 + 0x10, 0xD7, 0x33, 0x0D, // 164:4311 + 0x11, 0x0A, 0x31, 0x0D, // 165:4362 + 0x11, 0x3B, 0x10, 0x06, // 166:4411 + 0x11, 0x4B, 0x2F, 0x0D, // 167:4427 + 0x11, 0x7A, 0x19, 0x08, // 168:4474 + 0x11, 0x93, 0x46, 0x12, // 169:4499 + 0x11, 0xD9, 0x1A, 0x09, // 170:4569 + 0x11, 0xF3, 0x27, 0x0D, // 171:4595 + 0x12, 0x1A, 0x2F, 0x0E, // 172:4634 + 0x12, 0x49, 0x1B, 0x08, // 173:4681 + 0x12, 0x64, 0x46, 0x12, // 174:4708 + 0x12, 0xAA, 0x31, 0x0D, // 175:4778 + 0x12, 0xDB, 0x1E, 0x0A, // 176:4827 + 0x12, 0xF9, 0x33, 0x0D, // 177:4857 + 0x13, 0x2C, 0x1A, 0x08, // 178:4908 + 0x13, 0x46, 0x1A, 0x08, // 179:4934 + 0x13, 0x60, 0x19, 0x08, // 180:4960 + 0x13, 0x79, 0x2F, 0x0E, // 181:4985 + 0x13, 0xA8, 0x31, 0x0D, // 182:5032 + 0x13, 0xD9, 0x12, 0x08, // 183:5081 + 0x13, 0xEB, 0x18, 0x08, // 184:5099 + 0x14, 0x03, 0x16, 0x08, // 185:5123 + 0x14, 0x19, 0x1E, 0x09, // 186:5145 + 0x14, 0x37, 0x2E, 0x0D, // 187:5175 + 0x14, 0x65, 0x4F, 0x14, // 188:5221 + 0x14, 0xB4, 0x4B, 0x14, // 189:5300 + 0x14, 0xFF, 0x4B, 0x14, // 190:5375 + 0x15, 0x4A, 0x33, 0x0F, // 191:5450 + 0x15, 0x7D, 0x3B, 0x10, // 192:5501 + 0x15, 0xB8, 0x3B, 0x10, // 193:5560 + 0x15, 0xF3, 0x3B, 0x10, // 194:5619 + 0x16, 0x2E, 0x3B, 0x10, // 195:5678 + 0x16, 0x69, 0x3B, 0x10, // 196:5737 + 0x16, 0xA4, 0x3B, 0x10, // 197:5796 + 0x16, 0xDF, 0x5B, 0x18, // 198:5855 + 0x17, 0x3A, 0x3F, 0x11, // 199:5946 + 0x17, 0x79, 0x3B, 0x10, // 200:6009 + 0x17, 0xB4, 0x3B, 0x10, // 201:6068 + 0x17, 0xEF, 0x3B, 0x10, // 202:6127 + 0x18, 0x2A, 0x3B, 0x10, // 203:6186 + 0x18, 0x65, 0x11, 0x07, // 204:6245 + 0x18, 0x76, 0x11, 0x07, // 205:6262 + 0x18, 0x87, 0x15, 0x07, // 206:6279 + 0x18, 0x9C, 0x15, 0x07, // 207:6300 + 0x18, 0xB1, 0x3F, 0x11, // 208:6321 + 0x18, 0xF0, 0x3B, 0x11, // 209:6384 + 0x19, 0x2B, 0x47, 0x13, // 210:6443 + 0x19, 0x72, 0x47, 0x13, // 211:6514 + 0x19, 0xB9, 0x47, 0x13, // 212:6585 + 0x1A, 0x00, 0x47, 0x13, // 213:6656 + 0x1A, 0x47, 0x47, 0x13, // 214:6727 + 0x1A, 0x8E, 0x2B, 0x0E, // 215:6798 + 0x1A, 0xB9, 0x47, 0x13, // 216:6841 + 0x1B, 0x00, 0x3B, 0x11, // 217:6912 + 0x1B, 0x3B, 0x3B, 0x11, // 218:6971 + 0x1B, 0x76, 0x3B, 0x11, // 219:7030 + 0x1B, 0xB1, 0x3B, 0x11, // 220:7089 + 0x1B, 0xEC, 0x3D, 0x10, // 221:7148 + 0x1C, 0x29, 0x3A, 0x10, // 222:7209 + 0x1C, 0x63, 0x37, 0x0F, // 223:7267 + 0x1C, 0x9A, 0x2F, 0x0D, // 224:7322 + 0x1C, 0xC9, 0x2F, 0x0D, // 225:7369 + 0x1C, 0xF8, 0x2F, 0x0D, // 226:7416 + 0x1D, 0x27, 0x2F, 0x0D, // 227:7463 + 0x1D, 0x56, 0x2F, 0x0D, // 228:7510 + 0x1D, 0x85, 0x2F, 0x0D, // 229:7557 + 0x1D, 0xB4, 0x53, 0x15, // 230:7604 + 0x1E, 0x07, 0x2B, 0x0C, // 231:7687 + 0x1E, 0x32, 0x2F, 0x0D, // 232:7730 + 0x1E, 0x61, 0x2F, 0x0D, // 233:7777 + 0x1E, 0x90, 0x2F, 0x0D, // 234:7824 + 0x1E, 0xBF, 0x2F, 0x0D, // 235:7871 + 0x1E, 0xEE, 0x11, 0x07, // 236:7918 + 0x1E, 0xFF, 0x11, 0x07, // 237:7935 + 0x1F, 0x10, 0x15, 0x07, // 238:7952 + 0x1F, 0x25, 0x15, 0x07, // 239:7973 + 0x1F, 0x3A, 0x2F, 0x0D, // 240:7994 + 0x1F, 0x69, 0x2F, 0x0D, // 241:8041 + 0x1F, 0x98, 0x2F, 0x0D, // 242:8088 + 0x1F, 0xC7, 0x2F, 0x0D, // 243:8135 + 0x1F, 0xF6, 0x2F, 0x0D, // 244:8182 + 0x20, 0x25, 0x2F, 0x0D, // 245:8229 + 0x20, 0x54, 0x2F, 0x0D, // 246:8276 + 0x20, 0x83, 0x32, 0x0D, // 247:8323 + 0x20, 0xB5, 0x33, 0x0F, // 248:8373 + 0x20, 0xE8, 0x2F, 0x0D, // 249:8424 + 0x21, 0x17, 0x2F, 0x0D, // 250:8471 + 0x21, 0x46, 0x2F, 0x0D, // 251:8518 + 0x21, 0x75, 0x2F, 0x0D, // 252:8565 + 0x21, 0xA4, 0x2A, 0x0C, // 253:8612 + 0x21, 0xCE, 0x2F, 0x0D, // 254:8654 + 0x21, 0xFD, 0x2A, 0x0C, // 255:8701 + + // Font Data: + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x33,0x00,0xE0,0xFF,0x33, // 33 + 0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xE0,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xE0,0x07, // 34 + 0x00,0x0C,0x03,0x00,0x00,0x0C,0x33,0x00,0x00,0x0C,0x3F,0x00,0x00,0xFC,0x0F,0x00,0x80,0xFF,0x03,0x00,0xE0,0x0F,0x03,0x00,0x60,0x0C,0x33,0x00,0x00,0x0C,0x3F,0x00,0x00,0xFC,0x0F,0x00,0x80,0xFF,0x03,0x00,0xE0,0x0F,0x03,0x00,0x60,0x0C,0x03,0x00,0x00,0x0C,0x03, // 35 + 0x00,0x00,0x00,0x00,0x80,0x07,0x06,0x00,0xC0,0x0F,0x1E,0x00,0xC0,0x18,0x1C,0x00,0x60,0x18,0x38,0x00,0x60,0x30,0x30,0x00,0xF0,0xFF,0xFF,0x00,0x60,0x30,0x30,0x00,0x60,0x60,0x38,0x00,0xC0,0x60,0x18,0x00,0xC0,0xC1,0x1F,0x00,0x00,0x81,0x07, // 36 + 0x00,0x00,0x00,0x00,0x80,0x0F,0x00,0x00,0xC0,0x1F,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x20,0x00,0x00,0x20,0x20,0x20,0x00,0x60,0x30,0x38,0x00,0xC0,0x1F,0x1E,0x00,0x80,0x8F,0x0F,0x00,0x00,0xC0,0x03,0x00,0x00,0xF0,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0x8F,0x0F,0x00,0xC0,0xC3,0x1F,0x00,0xE0,0x60,0x30,0x00,0x20,0x20,0x20,0x00,0x00,0x20,0x20,0x00,0x00,0x60,0x30,0x00,0x00,0xC0,0x1F,0x00,0x00,0x80,0x0F, // 37 + 0x00,0x00,0x00,0x00,0x00,0x80,0x07,0x00,0x00,0xC0,0x0F,0x00,0x80,0xE3,0x1C,0x00,0xC0,0x77,0x38,0x00,0xE0,0x3C,0x30,0x00,0x60,0x38,0x30,0x00,0x60,0x78,0x30,0x00,0xE0,0xEC,0x38,0x00,0xC0,0x8F,0x1B,0x00,0x80,0x03,0x1F,0x00,0x00,0x00,0x0F,0x00,0x00,0xC0,0x1F,0x00,0x00,0xC0,0x38,0x00,0x00,0x00,0x10, // 38 + 0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xE0,0x07, // 39 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x0F,0x00,0x00,0xFE,0x7F,0x00,0x80,0x0F,0xF0,0x01,0xC0,0x01,0x80,0x03,0x60,0x00,0x00,0x06,0x20,0x00,0x00,0x04, // 40 + 0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x04,0x60,0x00,0x00,0x06,0xC0,0x01,0x80,0x03,0x80,0x0F,0xF0,0x01,0x00,0xFE,0x7F,0x00,0x00,0xF0,0x0F, // 41 + 0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x80,0x04,0x00,0x00,0x80,0x0F,0x00,0x00,0xE0,0x03,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x80,0x04,0x00,0x00,0x80, // 42 + 0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xFF,0x0F,0x00,0x00,0xFF,0x0F,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 43 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x03,0x00,0x00,0xF0,0x01, // 44 + 0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01, // 45 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30, // 46 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0xE0,0x0F,0x00,0x00,0xFC,0x01,0x00,0x80,0x3F,0x00,0x00,0xE0,0x03,0x00,0x00,0x60, // 47 + 0x00,0x00,0x00,0x00,0x00,0xFE,0x03,0x00,0x80,0xFF,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xE0,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x01,0x1C,0x00,0x80,0xFF,0x0F,0x00,0x00,0xFE,0x03, // 48 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x03,0x00,0x00,0x80,0x01,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 49 + 0x00,0x00,0x00,0x00,0x00,0x03,0x30,0x00,0xC0,0x03,0x38,0x00,0xC0,0x00,0x3C,0x00,0x60,0x00,0x36,0x00,0x60,0x00,0x33,0x00,0x60,0x80,0x31,0x00,0x60,0xC0,0x30,0x00,0x60,0x60,0x30,0x00,0xC0,0x30,0x30,0x00,0xC0,0x1F,0x30,0x00,0x00,0x0F,0x30, // 50 + 0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x00,0xC0,0x01,0x0E,0x00,0xC0,0x00,0x1C,0x00,0x60,0x00,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0xC0,0x38,0x30,0x00,0xC0,0x6F,0x18,0x00,0x80,0xC7,0x0F,0x00,0x00,0x80,0x07, // 51 + 0x00,0x00,0x00,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x03,0x00,0x00,0xF0,0x03,0x00,0x00,0x3C,0x03,0x00,0x00,0x0E,0x03,0x00,0x80,0x07,0x03,0x00,0xC0,0x01,0x03,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x03, // 52 + 0x00,0x00,0x00,0x00,0x00,0x30,0x06,0x00,0x80,0x3F,0x0E,0x00,0xE0,0x1F,0x18,0x00,0x60,0x08,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x0C,0x30,0x00,0x60,0x18,0x1C,0x00,0x60,0xF0,0x0F,0x00,0x00,0xE0,0x03, // 53 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x03,0x00,0x80,0xFF,0x0F,0x00,0xC0,0x63,0x1C,0x00,0xC0,0x30,0x38,0x00,0x60,0x18,0x30,0x00,0x60,0x18,0x30,0x00,0x60,0x18,0x30,0x00,0x60,0x18,0x30,0x00,0xE0,0x30,0x18,0x00,0xC0,0xF1,0x0F,0x00,0x80,0xC1,0x07, // 54 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x3C,0x00,0x60,0x80,0x3F,0x00,0x60,0xE0,0x03,0x00,0x60,0x78,0x00,0x00,0x60,0x0E,0x00,0x00,0x60,0x03,0x00,0x00,0xE0,0x01,0x00,0x00,0x60, // 55 + 0x00,0x00,0x00,0x00,0x00,0x80,0x07,0x00,0x80,0xC7,0x1F,0x00,0xC0,0x6F,0x18,0x00,0xE0,0x38,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0xE0,0x38,0x30,0x00,0xC0,0x6F,0x18,0x00,0x80,0xC7,0x1F,0x00,0x00,0x80,0x07, // 56 + 0x00,0x00,0x00,0x00,0x00,0x1F,0x0C,0x00,0x80,0x7F,0x1C,0x00,0xC0,0x61,0x38,0x00,0x60,0xC0,0x30,0x00,0x60,0xC0,0x30,0x00,0x60,0xC0,0x30,0x00,0x60,0xC0,0x30,0x00,0x60,0x60,0x18,0x00,0xC0,0x31,0x1E,0x00,0x80,0xFF,0x0F,0x00,0x00,0xFE,0x01, // 57 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30, // 58 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x30,0x03,0x00,0x06,0xF0,0x01, // 59 + 0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x50,0x00,0x00,0x00,0xD8,0x00,0x00,0x00,0xD8,0x00,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x04,0x01,0x00,0x00,0x06,0x03,0x00,0x00,0x06,0x03,0x00,0x00,0x03,0x06, // 60 + 0x00,0x00,0x00,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01, // 61 + 0x00,0x00,0x00,0x00,0x00,0x03,0x06,0x00,0x00,0x06,0x03,0x00,0x00,0x06,0x03,0x00,0x00,0x04,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0x8C,0x01,0x00,0x00,0xD8,0x00,0x00,0x00,0xD8,0x00,0x00,0x00,0x50,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x20, // 62 + 0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x00,0x00,0x00,0x60,0x80,0x33,0x00,0x60,0xC0,0x33,0x00,0x60,0xE0,0x00,0x00,0x60,0x30,0x00,0x00,0xC0,0x38,0x00,0x00,0xC0,0x1F,0x00,0x00,0x00,0x07, // 63 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x0F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x1E,0xF0,0x00,0x00,0x07,0xC0,0x01,0x80,0xC3,0x87,0x01,0xC0,0xF1,0x9F,0x03,0xC0,0x38,0x18,0x03,0xC0,0x0C,0x30,0x03,0x60,0x0E,0x30,0x06,0x60,0x06,0x30,0x06,0x60,0x06,0x18,0x06,0x60,0x06,0x0C,0x06,0x60,0x0C,0x1E,0x06,0x60,0xF8,0x3F,0x06,0xE0,0xFE,0x31,0x06,0xC0,0x0E,0x30,0x06,0xC0,0x01,0x18,0x03,0x80,0x03,0x1C,0x03,0x00,0x07,0x8F,0x01,0x00,0xFE,0x87,0x01,0x00,0xF8,0xC1,0x00,0x00,0x00,0x40, // 64 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x80,0x8F,0x01,0x00,0xE0,0x83,0x01,0x00,0x60,0x80,0x01,0x00,0xE0,0x83,0x01,0x00,0x80,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 65 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0xC0,0x78,0x30,0x00,0xC0,0xFF,0x18,0x00,0x80,0xC7,0x1F,0x00,0x00,0x80,0x07, // 66 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0F,0x00,0x00,0x02,0x03, // 67 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0E,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 68 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 69 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60, // 70 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x18,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x60,0x30,0x00,0x60,0x60,0x30,0x00,0xE0,0x60,0x38,0x00,0xC0,0x60,0x18,0x00,0xC0,0x61,0x18,0x00,0x80,0xE3,0x0F,0x00,0x00,0xE2,0x0F, // 71 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 72 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 73 + 0x00,0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0xE0,0xFF,0x1F,0x00,0xE0,0xFF,0x0F, // 74 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0xE0,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x7C,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0xE7,0x01,0x00,0x80,0x83,0x07,0x00,0xC0,0x01,0x0F,0x00,0xE0,0x00,0x1E,0x00,0x60,0x00,0x38,0x00,0x20,0x00,0x30,0x00,0x00,0x00,0x20, // 75 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30, // 76 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0x01,0x00,0x00,0xC0,0x0F,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x3F,0x00,0x00,0xE0,0x07,0x00,0x00,0xFE,0x00,0x00,0xC0,0x0F,0x00,0x00,0xE0,0x01,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 77 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0xC0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0xE0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 78 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 79 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0x60,0x60,0x00,0x00,0xC0,0x30,0x00,0x00,0xC0,0x3F,0x00,0x00,0x00,0x0F, // 80 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x0C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x18,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x36,0x00,0x60,0x00,0x36,0x00,0xE0,0x00,0x3C,0x00,0xC0,0x00,0x1C,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x3F,0x00,0x00,0xFF,0x77,0x00,0x00,0xFC,0x61, // 81 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x70,0x00,0x00,0x60,0xF0,0x00,0x00,0x60,0xF0,0x03,0x00,0x60,0xB0,0x07,0x00,0xE0,0x18,0x1F,0x00,0xC0,0x1F,0x3C,0x00,0x80,0x0F,0x30,0x00,0x00,0x00,0x20, // 82 + 0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x07,0x0F,0x00,0xC0,0x1F,0x1C,0x00,0xC0,0x18,0x18,0x00,0x60,0x38,0x38,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x70,0x30,0x00,0xC0,0x60,0x18,0x00,0xC0,0xE1,0x18,0x00,0x80,0xC3,0x0F,0x00,0x00,0x83,0x07, // 83 + 0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 84 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 85 + 0x20,0x00,0x00,0x00,0xE0,0x01,0x00,0x00,0xC0,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0xF8,0x01,0x00,0x00,0xC0,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x3E,0x00,0x00,0xC0,0x0F,0x00,0x00,0xF8,0x01,0x00,0x00,0x3E,0x00,0x00,0xC0,0x0F,0x00,0x00,0xE0,0x01,0x00,0x00,0x20, // 86 + 0x60,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0x80,0xFF,0x00,0x00,0x00,0xF8,0x0F,0x00,0x00,0x80,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x3F,0x00,0x00,0xE0,0x0F,0x00,0x00,0xFC,0x01,0x00,0x80,0x1F,0x00,0x00,0xE0,0x03,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xE0,0x0F,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x30,0x00,0x00,0x80,0x3F,0x00,0x00,0xF8,0x0F,0x00,0x80,0xFF,0x00,0x00,0xE0,0x07,0x00,0x00,0x60, // 87 + 0x00,0x00,0x20,0x00,0x20,0x00,0x30,0x00,0x60,0x00,0x3C,0x00,0xE0,0x01,0x1E,0x00,0xC0,0x83,0x07,0x00,0x00,0xCF,0x03,0x00,0x00,0xFE,0x01,0x00,0x00,0x38,0x00,0x00,0x00,0xFE,0x01,0x00,0x00,0xCF,0x03,0x00,0xC0,0x03,0x07,0x00,0xE0,0x01,0x1E,0x00,0x60,0x00,0x3C,0x00,0x20,0x00,0x30,0x00,0x00,0x00,0x20, // 88 + 0x20,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xC0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0xF0,0x3F,0x00,0x00,0xF0,0x3F,0x00,0x00,0x3C,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x07,0x00,0x00,0xC0,0x03,0x00,0x00,0xE0,0x01,0x00,0x00,0x60,0x00,0x00,0x00,0x20, // 89 + 0x00,0x00,0x30,0x00,0x60,0x00,0x38,0x00,0x60,0x00,0x3C,0x00,0x60,0x00,0x37,0x00,0x60,0x80,0x33,0x00,0x60,0xC0,0x31,0x00,0x60,0xE0,0x30,0x00,0x60,0x38,0x30,0x00,0x60,0x1C,0x30,0x00,0x60,0x0E,0x30,0x00,0x60,0x07,0x30,0x00,0xE0,0x01,0x30,0x00,0xE0,0x00,0x30,0x00,0x60,0x00,0x30, // 90 + 0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06, // 91 + 0x60,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x3F,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xE0,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 92 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07, // 93 + 0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1F,0x00,0x00,0xC0,0x07,0x00,0x00,0xE0,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0xC0,0x07,0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x20, // 94 + 0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06, // 95 + 0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0x80, // 96 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x00,0x86,0x31,0x00,0x00,0x86,0x31,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x18,0x00,0x00,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 97 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x18,0x0C,0x00,0x00,0x0C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xE0,0x03, // 98 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0x18,0x0C, // 99 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0C,0x18,0x00,0x00,0x18,0x0C,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 100 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x00,0xCE,0x38,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xCE,0x38,0x00,0x00,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 101 + 0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0xC0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x06,0x00,0x00,0x60,0x06,0x00,0x00,0x60,0x06, // 102 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x83,0x01,0x00,0xF8,0x8F,0x03,0x00,0x1C,0x1C,0x07,0x00,0x0E,0x38,0x06,0x00,0x06,0x30,0x06,0x00,0x06,0x30,0x06,0x00,0x06,0x30,0x06,0x00,0x0C,0x18,0x07,0x00,0x18,0x8C,0x03,0x00,0xFE,0xFF,0x01,0x00,0xFE,0xFF, // 103 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 104 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0xFE,0x3F,0x00,0x60,0xFE,0x3F, // 105 + 0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x60,0xFE,0xFF,0x07,0x60,0xFE,0xFF,0x03, // 106 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0xC0,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0xF0,0x01,0x00,0x00,0x98,0x07,0x00,0x00,0x0C,0x0E,0x00,0x00,0x06,0x3C,0x00,0x00,0x02,0x30,0x00,0x00,0x00,0x20, // 107 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 108 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0x00,0x0C,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x0C,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 109 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 110 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 111 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0xFF,0x07,0x00,0xFE,0xFF,0x07,0x00,0x18,0x0C,0x00,0x00,0x0C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xE0,0x03, // 112 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0C,0x18,0x00,0x00,0x18,0x0C,0x00,0x00,0xFE,0xFF,0x07,0x00,0xFE,0xFF,0x07, // 113 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0x00,0x0C,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x06, // 114 + 0x00,0x00,0x00,0x00,0x00,0x38,0x0C,0x00,0x00,0x7C,0x1C,0x00,0x00,0xEE,0x38,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x31,0x00,0x00,0xC6,0x31,0x00,0x00,0x8E,0x39,0x00,0x00,0x9C,0x1F,0x00,0x00,0x18,0x0F, // 115 + 0x00,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0xC0,0xFF,0x1F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30, // 116 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 117 + 0x00,0x06,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0xC0,0x07,0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1F,0x00,0x00,0xC0,0x07,0x00,0x00,0xF8,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x06, // 118 + 0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x80,0x1F,0x00,0x00,0xE0,0x03,0x00,0x00,0x7C,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7C,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x80,0x1F,0x00,0x00,0xF0,0x03,0x00,0x00,0x7E,0x00,0x00,0x00,0x0E, // 119 + 0x00,0x02,0x20,0x00,0x00,0x06,0x30,0x00,0x00,0x1E,0x3C,0x00,0x00,0x38,0x0E,0x00,0x00,0xF0,0x07,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x07,0x00,0x00,0x38,0x0E,0x00,0x00,0x1C,0x3C,0x00,0x00,0x0E,0x30,0x00,0x00,0x02,0x20, // 120 + 0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0x00,0xF0,0x01,0x06,0x00,0x80,0x0F,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xFC,0x00,0x00,0xC0,0x1F,0x00,0x00,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06, // 121 + 0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x06,0x3C,0x00,0x00,0x06,0x3E,0x00,0x00,0x06,0x37,0x00,0x00,0xC6,0x33,0x00,0x00,0xE6,0x30,0x00,0x00,0x76,0x30,0x00,0x00,0x3E,0x30,0x00,0x00,0x1E,0x30,0x00,0x00,0x06,0x30, // 122 + 0x00,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x03,0x00,0xC0,0x7F,0xFE,0x03,0xE0,0x3F,0xFC,0x07,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06, // 123 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x0F,0xE0,0xFF,0xFF,0x0F, // 124 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x06,0x60,0x00,0x00,0x06,0xE0,0x3F,0xFC,0x07,0xC0,0x7F,0xFF,0x03,0x00,0xC0,0x03,0x00,0x00,0x80,0x01, // 125 + 0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0x60, // 126 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE6,0xFF,0x07,0x00,0xE6,0xFF,0x07, // 161 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x9C,0x07,0x00,0x0E,0x78,0x00,0x00,0x06,0x3F,0x00,0x00,0xF6,0x30,0x00,0x00,0x0E,0x30,0x00,0xE0,0x0D,0x1C,0x00,0x00,0x1C,0x0E,0x00,0x00,0x10,0x06, // 162 + 0x00,0x60,0x10,0x00,0x00,0x60,0x38,0x00,0x00,0x7F,0x1C,0x00,0xC0,0xFF,0x1F,0x00,0xE0,0xE0,0x19,0x00,0x60,0x60,0x18,0x00,0x60,0x60,0x18,0x00,0x60,0x60,0x30,0x00,0xE0,0x00,0x30,0x00,0xC0,0x01,0x30,0x00,0x80,0x01,0x38,0x00,0x00,0x00,0x10, // 163 + 0x00,0x00,0x00,0x00,0x00,0x02,0x04,0x00,0x00,0xF7,0x0E,0x00,0x00,0xFE,0x07,0x00,0x00,0x0C,0x03,0x00,0x00,0x06,0x06,0x00,0x00,0x06,0x06,0x00,0x00,0x06,0x06,0x00,0x00,0x06,0x06,0x00,0x00,0x0C,0x03,0x00,0x00,0xFE,0x07,0x00,0x00,0xF7,0x0E,0x00,0x00,0x02,0x04, // 164 + 0xE0,0x60,0x06,0x00,0xC0,0x61,0x06,0x00,0x80,0x67,0x06,0x00,0x00,0x7E,0x06,0x00,0x00,0x7C,0x06,0x00,0x00,0xF0,0x3F,0x00,0x00,0xF0,0x3F,0x00,0x00,0x7C,0x06,0x00,0x00,0x7E,0x06,0x00,0x80,0x67,0x06,0x00,0xC0,0x61,0x06,0x00,0xE0,0x60,0x06,0x00,0x20, // 165 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0x7F,0xF8,0x0F,0xE0,0x7F,0xF8,0x0F, // 166 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x00,0x00,0x80,0xF3,0xC1,0x00,0xC0,0x1F,0xC3,0x03,0xE0,0x0C,0x07,0x03,0x60,0x1C,0x06,0x06,0x60,0x18,0x0C,0x06,0x60,0x30,0x1C,0x06,0xE0,0x70,0x38,0x07,0xC0,0xE1,0xF4,0x03,0x80,0xC1,0xE7,0x01,0x00,0x80,0x03, // 167 + 0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 168 + 0x00,0xF8,0x00,0x00,0x00,0xFE,0x03,0x00,0x00,0x07,0x07,0x00,0x80,0x01,0x0C,0x00,0xC0,0x79,0x1C,0x00,0xC0,0xFE,0x19,0x00,0x60,0x86,0x31,0x00,0x60,0x03,0x33,0x00,0x60,0x03,0x33,0x00,0x60,0x03,0x33,0x00,0x60,0x03,0x33,0x00,0x60,0x87,0x33,0x00,0xC0,0x86,0x19,0x00,0xC0,0x85,0x1C,0x00,0x80,0x01,0x0C,0x00,0x00,0x07,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xF8, // 169 + 0x00,0x00,0x00,0x00,0xC0,0x1C,0x00,0x00,0xE0,0x3E,0x00,0x00,0x60,0x32,0x00,0x00,0x60,0x32,0x00,0x00,0xE0,0x3F,0x00,0x00,0xC0,0x3F, // 170 + 0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0xE0,0x03,0x00,0x00,0x78,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x84,0x10,0x00,0x00,0xE0,0x03,0x00,0x00,0x78,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x04,0x10, // 171 + 0x00,0x00,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFC,0x01, // 172 + 0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01,0x00,0x00,0x80,0x01, // 173 + 0x00,0xF8,0x00,0x00,0x00,0xFE,0x03,0x00,0x00,0x07,0x07,0x00,0x80,0x01,0x0C,0x00,0xC0,0x01,0x1C,0x00,0xC0,0xFE,0x1B,0x00,0x60,0xFE,0x33,0x00,0x60,0x66,0x30,0x00,0x60,0x66,0x30,0x00,0x60,0xE6,0x30,0x00,0x60,0xFE,0x31,0x00,0x60,0x3C,0x33,0x00,0xC0,0x00,0x1A,0x00,0xC0,0x01,0x1C,0x00,0x80,0x01,0x0C,0x00,0x00,0x07,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xF8, // 174 + 0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0x0C, // 175 + 0x00,0x00,0x00,0x00,0x80,0x03,0x00,0x00,0x40,0x04,0x00,0x00,0x20,0x08,0x00,0x00,0x20,0x08,0x00,0x00,0x20,0x08,0x00,0x00,0x40,0x04,0x00,0x00,0x80,0x03, // 176 + 0x00,0x00,0x00,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0xFF,0x3F,0x00,0x00,0xFF,0x3F,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30,0x00,0x00,0x60,0x30, // 177 + 0x40,0x20,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x38,0x00,0x00,0x20,0x2C,0x00,0x00,0x20,0x26,0x00,0x00,0xE0,0x23,0x00,0x00,0xC0,0x21, // 178 + 0x40,0x10,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x20,0x00,0x00,0x20,0x22,0x00,0x00,0x20,0x22,0x00,0x00,0xE0,0x3D,0x00,0x00,0xC0,0x1D, // 179 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x20, // 180 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0xFF,0x07,0x00,0xFE,0xFF,0x07,0x00,0x00,0x1C,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x1C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 181 + 0x00,0x0F,0x00,0x00,0xC0,0x3F,0x00,0x00,0xC0,0x3F,0x00,0x00,0xE0,0x7F,0x00,0x00,0xE0,0x7F,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x60,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x60,0x00,0x00,0x00,0x60, // 182 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x60, // 183 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0xC0,0x02,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x01, // 184 + 0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0xE0,0x3F,0x00,0x00,0xE0,0x3F, // 185 + 0x00,0x00,0x00,0x00,0x80,0x0F,0x00,0x00,0xC0,0x1F,0x00,0x00,0xE0,0x38,0x00,0x00,0x60,0x30,0x00,0x00,0xE0,0x38,0x00,0x00,0xC0,0x1F,0x00,0x00,0x80,0x0F, // 186 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x10,0x00,0x00,0x1C,0x1C,0x00,0x00,0x78,0x0F,0x00,0x00,0xE0,0x03,0x00,0x00,0x84,0x10,0x00,0x00,0x1C,0x1C,0x00,0x00,0x78,0x0F,0x00,0x00,0xE0,0x03,0x00,0x00,0x80, // 187 + 0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x20,0x00,0xE0,0x3F,0x38,0x00,0xE0,0x3F,0x1C,0x00,0x00,0x00,0x0E,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x07,0x0C,0x00,0xC0,0x01,0x0E,0x00,0xE0,0x80,0x0B,0x00,0x60,0xC0,0x08,0x00,0x00,0xE0,0x3F,0x00,0x00,0xE0,0x3F,0x00,0x00,0x00,0x08, // 188 + 0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x00,0xC0,0x00,0x00,0x00,0xC0,0x00,0x20,0x00,0xE0,0x3F,0x30,0x00,0xE0,0x3F,0x1C,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x07,0x00,0x00,0xC0,0x01,0x00,0x00,0xE0,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x4E,0x20,0x00,0x00,0x67,0x30,0x00,0xC0,0x21,0x38,0x00,0xE0,0x20,0x2C,0x00,0x60,0x20,0x26,0x00,0x00,0xE0,0x27,0x00,0x00,0xC0,0x21, // 189 + 0x40,0x10,0x00,0x00,0x60,0x30,0x00,0x00,0x20,0x20,0x00,0x00,0x20,0x22,0x20,0x00,0x20,0x22,0x30,0x00,0xE0,0x3D,0x38,0x00,0xC0,0x1D,0x0E,0x00,0x00,0x00,0x07,0x00,0x00,0x80,0x03,0x00,0x00,0xE0,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x0E,0x0C,0x00,0x00,0x07,0x0E,0x00,0x80,0x83,0x0B,0x00,0xE0,0xC0,0x08,0x00,0x60,0xE0,0x3F,0x00,0x20,0xE0,0x3F,0x00,0x00,0x00,0x08, // 190 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0xF8,0x03,0x00,0x00,0x1E,0x03,0x00,0x00,0x07,0x07,0x00,0xE6,0x03,0x06,0x00,0xE6,0x01,0x06,0x00,0x00,0x00,0x06,0x00,0x00,0x00,0x07,0x00,0x00,0x80,0x03,0x00,0x00,0xC0,0x01,0x00,0x00,0xC0, // 191 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x82,0x8F,0x01,0x00,0xE6,0x83,0x01,0x00,0x6E,0x80,0x01,0x00,0xE8,0x83,0x01,0x00,0x80,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 192 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x80,0x8F,0x01,0x00,0xE8,0x83,0x01,0x00,0x6E,0x80,0x01,0x00,0xE6,0x83,0x01,0x00,0x82,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 193 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x88,0x8F,0x01,0x00,0xEC,0x83,0x01,0x00,0x66,0x80,0x01,0x00,0xE6,0x83,0x01,0x00,0x8C,0x8F,0x01,0x00,0x08,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 194 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x0C,0xFE,0x01,0x00,0x8E,0x8F,0x01,0x00,0xE6,0x83,0x01,0x00,0x66,0x80,0x01,0x00,0xEC,0x83,0x01,0x00,0x8C,0x8F,0x01,0x00,0x0E,0xFE,0x01,0x00,0x06,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 195 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x8C,0x8F,0x01,0x00,0xEC,0x83,0x01,0x00,0x60,0x80,0x01,0x00,0xE0,0x83,0x01,0x00,0x8C,0x8F,0x01,0x00,0x0C,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 196 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3E,0x00,0x00,0x80,0x0F,0x00,0x00,0xF0,0x03,0x00,0x00,0xFE,0x01,0x00,0x9C,0x8F,0x01,0x00,0xE2,0x83,0x01,0x00,0x62,0x80,0x01,0x00,0xE2,0x83,0x01,0x00,0x9C,0x8F,0x01,0x00,0x00,0xFE,0x01,0x00,0x00,0xF0,0x03,0x00,0x00,0x80,0x0F,0x00,0x00,0x00,0x3E,0x00,0x00,0x00,0x30, // 197 + 0x00,0x00,0x30,0x00,0x00,0x00,0x3C,0x00,0x00,0x00,0x0F,0x00,0x00,0xC0,0x03,0x00,0x00,0xF0,0x01,0x00,0x00,0xBC,0x01,0x00,0x00,0x8F,0x01,0x00,0xC0,0x83,0x01,0x00,0xE0,0x80,0x01,0x00,0x60,0x80,0x01,0x00,0x60,0x80,0x01,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 198 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0x60,0x00,0x30,0x02,0x60,0x00,0x30,0x02,0x60,0x00,0xF0,0x02,0x60,0x00,0xB0,0x03,0x60,0x00,0x30,0x01,0x60,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0F,0x00,0x00,0x02,0x03, // 199 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x62,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x6E,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 200 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x6E,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x62,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 201 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x66,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x68,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 202 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x6C,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30, // 203 + 0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0xE6,0xFF,0x3F,0x00,0xEE,0xFF,0x3F,0x00,0x08, // 204 + 0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0xEE,0xFF,0x3F,0x00,0xE6,0xFF,0x3F,0x00,0x02, // 205 + 0x08,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0xE6,0xFF,0x3F,0x00,0xE6,0xFF,0x3F,0x00,0x0C,0x00,0x00,0x00,0x08, // 206 + 0x0C,0x00,0x00,0x00,0x0C,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x0C,0x00,0x00,0x00,0x0C, // 207 + 0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x30,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x03,0x0E,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 208 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0xC0,0x01,0x00,0x00,0x8C,0x03,0x00,0x00,0x0E,0x0E,0x00,0x00,0x06,0x3C,0x00,0x00,0x06,0x70,0x00,0x00,0x0C,0xE0,0x01,0x00,0x0C,0x80,0x03,0x00,0x0E,0x00,0x0F,0x00,0x06,0x00,0x1C,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F, // 209 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x62,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x6E,0x00,0x30,0x00,0x68,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 210 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0x68,0x00,0x30,0x00,0x6E,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x62,0x00,0x30,0x00,0xE0,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 211 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x68,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0xE8,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 212 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xCC,0x00,0x18,0x00,0xEE,0x00,0x38,0x00,0x66,0x00,0x30,0x00,0x66,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x6E,0x00,0x30,0x00,0xE6,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 213 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x01,0x00,0x00,0xFF,0x07,0x00,0x80,0x07,0x0F,0x00,0xC0,0x01,0x1C,0x00,0xC0,0x00,0x18,0x00,0xE0,0x00,0x38,0x00,0x6C,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x6C,0x00,0x30,0x00,0xEC,0x00,0x38,0x00,0xC0,0x00,0x18,0x00,0xC0,0x01,0x1C,0x00,0x80,0x07,0x0F,0x00,0x00,0xFF,0x07,0x00,0x00,0xFC,0x01, // 214 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x03,0x00,0x00,0x8E,0x03,0x00,0x00,0xDC,0x01,0x00,0x00,0xF8,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0xDC,0x01,0x00,0x00,0x8E,0x03,0x00,0x00,0x06,0x03, // 215 + 0x00,0x00,0x00,0x00,0x00,0xFC,0x21,0x00,0x00,0xFF,0x77,0x00,0x80,0x07,0x3F,0x00,0xC0,0x01,0x1E,0x00,0xC0,0x00,0x1F,0x00,0xE0,0x80,0x3B,0x00,0x60,0xC0,0x31,0x00,0x60,0xE0,0x30,0x00,0x60,0x70,0x30,0x00,0x60,0x38,0x30,0x00,0x60,0x1C,0x30,0x00,0xE0,0x0E,0x38,0x00,0xC0,0x07,0x18,0x00,0xC0,0x03,0x1C,0x00,0xE0,0x07,0x0F,0x00,0x70,0xFF,0x07,0x00,0x20,0xFC,0x01, // 216 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x02,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x0E,0x00,0x30,0x00,0x08,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 217 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x08,0x00,0x30,0x00,0x0E,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x02,0x00,0x30,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 218 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x08,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x06,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x08,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 219 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x03,0x00,0xE0,0xFF,0x0F,0x00,0x00,0x00,0x1C,0x00,0x00,0x00,0x38,0x00,0x0C,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x0C,0x00,0x30,0x00,0x0C,0x00,0x38,0x00,0x00,0x00,0x1C,0x00,0xE0,0xFF,0x0F,0x00,0xE0,0xFF,0x03, // 220 + 0x20,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0xC0,0x01,0x00,0x00,0x80,0x03,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x1E,0x00,0x00,0x00,0x3C,0x00,0x00,0x08,0xF0,0x3F,0x00,0x0E,0xF0,0x3F,0x00,0x06,0x3C,0x00,0x00,0x02,0x1E,0x00,0x00,0x00,0x07,0x00,0x00,0xC0,0x03,0x00,0x00,0xE0,0x01,0x00,0x00,0x60,0x00,0x00,0x00,0x20, // 221 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0x3F,0x00,0xE0,0xFF,0x3F,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x06,0x00,0x00,0x03,0x07,0x00,0x00,0x86,0x03,0x00,0x00,0xFE,0x01,0x00,0x00,0xF8, // 222 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xFF,0x3F,0x00,0xC0,0xFF,0x3F,0x00,0xC0,0x00,0x00,0x00,0x60,0x00,0x08,0x00,0x60,0x00,0x1C,0x00,0x60,0x00,0x38,0x00,0xE0,0x78,0x30,0x00,0xC0,0x7F,0x30,0x00,0x80,0xC7,0x30,0x00,0x00,0x80,0x39,0x00,0x00,0x80,0x1F,0x00,0x00,0x00,0x0F, // 223 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x20,0x86,0x31,0x00,0x60,0x86,0x31,0x00,0xE0,0xC6,0x30,0x00,0x80,0xC6,0x18,0x00,0x00,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 224 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x00,0x86,0x31,0x00,0x80,0x86,0x31,0x00,0xE0,0xC6,0x30,0x00,0x60,0xC6,0x18,0x00,0x20,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 225 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x80,0x8C,0x39,0x00,0xC0,0x86,0x31,0x00,0x60,0x86,0x31,0x00,0x60,0xC6,0x30,0x00,0xC0,0xC6,0x18,0x00,0x80,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 226 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0xC0,0x1C,0x1F,0x00,0xE0,0x8C,0x39,0x00,0x60,0x86,0x31,0x00,0x60,0x86,0x31,0x00,0xC0,0xC6,0x30,0x00,0xC0,0xC6,0x18,0x00,0xE0,0xCE,0x0C,0x00,0x60,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 227 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0xC0,0x8C,0x39,0x00,0xC0,0x86,0x31,0x00,0x00,0x86,0x31,0x00,0x00,0xC6,0x30,0x00,0xC0,0xC6,0x18,0x00,0xC0,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 228 + 0x00,0x00,0x00,0x00,0x00,0x18,0x0E,0x00,0x00,0x1C,0x1F,0x00,0x00,0x8C,0x39,0x00,0x70,0x86,0x31,0x00,0x88,0x86,0x31,0x00,0x88,0xC6,0x30,0x00,0x88,0xC6,0x18,0x00,0x70,0xCE,0x0C,0x00,0x00,0xFC,0x1F,0x00,0x00,0xF8,0x3F,0x00,0x00,0x00,0x20, // 229 + 0x00,0x00,0x00,0x00,0x00,0x10,0x0F,0x00,0x00,0x9C,0x1F,0x00,0x00,0xCC,0x39,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0x66,0x18,0x00,0x00,0x6E,0x1C,0x00,0x00,0xFC,0x0F,0x00,0x00,0xFC,0x1F,0x00,0x00,0xCC,0x1C,0x00,0x00,0xCE,0x38,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xCC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xE0,0x04, // 230 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x02,0x00,0x06,0x30,0x02,0x00,0x06,0xF0,0x02,0x00,0x06,0xB0,0x03,0x00,0x0E,0x38,0x01,0x00,0x1C,0x1C,0x00,0x00,0x18,0x0C, // 231 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x20,0xCE,0x38,0x00,0x60,0xC6,0x30,0x00,0xE0,0xC6,0x30,0x00,0x80,0xC6,0x30,0x00,0x00,0xCE,0x38,0x00,0x00,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 232 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x00,0xCE,0x38,0x00,0x80,0xC6,0x30,0x00,0xE0,0xC6,0x30,0x00,0x60,0xC6,0x30,0x00,0x20,0xCE,0x38,0x00,0x00,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 233 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0x80,0xCE,0x38,0x00,0xC0,0xC6,0x30,0x00,0x60,0xC6,0x30,0x00,0x60,0xC6,0x30,0x00,0xC0,0xCE,0x38,0x00,0x80,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 234 + 0x00,0x00,0x00,0x00,0x00,0xE0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0xDC,0x1C,0x00,0xC0,0xCE,0x38,0x00,0xC0,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0x00,0xC6,0x30,0x00,0xC0,0xCE,0x38,0x00,0xC0,0xDC,0x18,0x00,0x00,0xF8,0x0C,0x00,0x00,0xF0,0x04, // 235 + 0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x60,0xFE,0x3F,0x00,0xE0,0xFE,0x3F,0x00,0x80, // 236 + 0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0xE0,0xFE,0x3F,0x00,0x60,0xFE,0x3F,0x00,0x20, // 237 + 0x80,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x60,0xFE,0x3F,0x00,0x60,0xFE,0x3F,0x00,0xC0,0x00,0x00,0x00,0x80, // 238 + 0xC0,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F,0x00,0xC0,0x00,0x00,0x00,0xC0, // 239 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1D,0x1C,0x00,0xA0,0x0F,0x38,0x00,0xA0,0x06,0x30,0x00,0xE0,0x06,0x30,0x00,0xC0,0x06,0x30,0x00,0xC0,0x0F,0x38,0x00,0x20,0x1F,0x1C,0x00,0x00,0xFC,0x0F,0x00,0x00,0xE0,0x07, // 240 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x3F,0x00,0xC0,0xFE,0x3F,0x00,0xE0,0x18,0x00,0x00,0x60,0x0C,0x00,0x00,0x60,0x06,0x00,0x00,0xC0,0x06,0x00,0x00,0xC0,0x06,0x00,0x00,0xE0,0x0E,0x00,0x00,0x60,0xFC,0x3F,0x00,0x00,0xF8,0x3F, // 241 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x20,0x0E,0x38,0x00,0x60,0x06,0x30,0x00,0xE0,0x06,0x30,0x00,0x80,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 242 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x38,0x00,0x80,0x06,0x30,0x00,0xE0,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0x20,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 243 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0x80,0x0E,0x38,0x00,0xC0,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0xC0,0x0E,0x38,0x00,0x80,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 244 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0xC0,0x1C,0x1C,0x00,0xE0,0x0E,0x38,0x00,0x60,0x06,0x30,0x00,0x60,0x06,0x30,0x00,0xC0,0x06,0x30,0x00,0xC0,0x0E,0x38,0x00,0xE0,0x1C,0x1C,0x00,0x60,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 245 + 0x00,0x00,0x00,0x00,0x00,0xF0,0x07,0x00,0x00,0xF8,0x0F,0x00,0x00,0x1C,0x1C,0x00,0xC0,0x0E,0x38,0x00,0xC0,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0xC0,0x0E,0x38,0x00,0xC0,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x07, // 246 + 0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0xB6,0x01,0x00,0x00,0xB6,0x01,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30, // 247 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xF0,0x67,0x00,0x00,0xF8,0x7F,0x00,0x00,0x1C,0x1C,0x00,0x00,0x0E,0x3F,0x00,0x00,0x86,0x33,0x00,0x00,0xE6,0x31,0x00,0x00,0x76,0x30,0x00,0x00,0x3E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xFF,0x0F,0x00,0x00,0xF3,0x07, // 248 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x20,0x00,0x38,0x00,0x60,0x00,0x30,0x00,0xE0,0x00,0x30,0x00,0x80,0x00,0x30,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 249 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x30,0x00,0x80,0x00,0x30,0x00,0xE0,0x00,0x30,0x00,0x60,0x00,0x18,0x00,0x20,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 250 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0x80,0x00,0x38,0x00,0xC0,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0x60,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0x80,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 251 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x0F,0x00,0x00,0xFE,0x1F,0x00,0xC0,0x00,0x38,0x00,0xC0,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x30,0x00,0xC0,0x00,0x18,0x00,0xC0,0x00,0x0C,0x00,0x00,0xFE,0x3F,0x00,0x00,0xFE,0x3F, // 252 + 0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0x00,0xF0,0x01,0x06,0x00,0x80,0x0F,0x07,0x80,0x00,0xFE,0x03,0xE0,0x00,0xFC,0x00,0x60,0xC0,0x1F,0x00,0x20,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06, // 253 + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0x07,0x00,0x1C,0x18,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x06,0x30,0x00,0x00,0x0E,0x38,0x00,0x00,0x1C,0x1C,0x00,0x00,0xF8,0x0F,0x00,0x00,0xF0,0x03, // 254 + 0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x00,0x00,0x7E,0x00,0x06,0xC0,0xF0,0x01,0x06,0xC0,0x80,0x0F,0x07,0x00,0x00,0xFE,0x03,0x00,0x00,0xFC,0x00,0xC0,0xC0,0x1F,0x00,0xC0,0xF8,0x03,0x00,0x00,0x3E,0x00,0x00,0x00,0x06 // 255 +}; diff --git a/src/helpers/ui/OLEDDisplayFonts.h b/src/helpers/ui/OLEDDisplayFonts.h new file mode 100644 index 00000000..266e4078 --- /dev/null +++ b/src/helpers/ui/OLEDDisplayFonts.h @@ -0,0 +1,13 @@ +#ifndef OLEDDISPLAYFONTS_h +#define OLEDDISPLAYFONTS_h + +#ifdef ARDUINO +#include +#elif __MBED__ +#define PROGMEM +#endif + +extern const uint8_t ArialMT_Plain_10[] PROGMEM; +extern const uint8_t ArialMT_Plain_16[] PROGMEM; +extern const uint8_t ArialMT_Plain_24[] PROGMEM; +#endif diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp new file mode 100644 index 00000000..f383bb00 --- /dev/null +++ b/src/helpers/ui/SH1106Display.cpp @@ -0,0 +1,91 @@ +#include "SH1106Display.h" +#include +#include "Adafruit_SH110X.h" + +bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) +{ + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); +} + +bool SH1106Display::begin() +{ + return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); +} + +void SH1106Display::turnOn() +{ + display.oled_command(SH110X_DISPLAYON); + _isOn = true; +} + +void SH1106Display::turnOff() +{ + display.oled_command(SH110X_DISPLAYOFF); + _isOn = false; +} + +void SH1106Display::clear() +{ + display.clearDisplay(); + display.display(); +} + +void SH1106Display::startFrame(Color bkg) +{ + display.clearDisplay(); // TODO: apply 'bkg' + _color = SH110X_WHITE; + display.setTextColor(_color); + display.setTextSize(1); + display.cp437(true); // Use full 256 char 'Code Page 437' font +} + +void SH1106Display::setTextSize(int sz) +{ + display.setTextSize(sz); +} + +void SH1106Display::setColor(Color c) +{ + _color = (c != 0) ? SH110X_WHITE : SH110X_BLACK; + display.setTextColor(_color); +} + +void SH1106Display::setCursor(int x, int y) +{ + display.setCursor(x, y); +} + +void SH1106Display::print(const char *str) +{ + display.print(str); +} + +void SH1106Display::fillRect(int x, int y, int w, int h) +{ + display.fillRect(x, y, w, h, _color); +} + +void SH1106Display::drawRect(int x, int y, int w, int h) +{ + display.drawRect(x, y, w, h, _color); +} + +void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) +{ + display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); +} + +uint16_t SH1106Display::getTextWidth(const char *str) +{ + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w; +} + +void SH1106Display::endFrame() +{ + display.display(); +} diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h new file mode 100644 index 00000000..b52a6adf --- /dev/null +++ b/src/helpers/ui/SH1106Display.h @@ -0,0 +1,43 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#define SH110X_NO_SPLASH +#include + +#ifndef PIN_OLED_RESET +#define PIN_OLED_RESET -1 +#endif + +#ifndef DISPLAY_ADDRESS +#define DISPLAY_ADDRESS 0x3C +#endif + +class SH1106Display : public DisplayDriver +{ + Adafruit_SH1106G display; + bool _isOn; + uint8_t _color; + + bool i2c_probe(TwoWire &wire, uint8_t addr); + +public: + SH1106Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { _isOn = false; } + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char *str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override; + uint16_t getTextWidth(const char *str) override; + void endFrame() override; +}; diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 55516378..8d977db0 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -62,6 +62,13 @@ void SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { display.drawBitmap(x, y, bits, w, h, SSD1306_WHITE); } +uint16_t SSD1306Display::getTextWidth(const char* str) { + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w; +} + void SSD1306Display::endFrame() { display.display(); } diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index cd0e2a0a..1a3a9602 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -36,5 +36,6 @@ public: void fillRect(int x, int y, int w, int h) override; void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; void endFrame() override; }; diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp new file mode 100644 index 00000000..e9eea69b --- /dev/null +++ b/src/helpers/ui/ST7735Display.cpp @@ -0,0 +1,130 @@ +#include "ST7735Display.h" + +#ifndef DISPLAY_ROTATION + #define DISPLAY_ROTATION 2 +#endif + +#define SCALE_X 1.25f // 160 / 128 +#define SCALE_Y 1.25f // 80 / 64 + +bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { + return true; +/* + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); +*/ +} + +bool ST7735Display::begin() { + if (!_isOn) { + if (_peripher_power) _peripher_power->claim(); + + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); + digitalWrite(PIN_TFT_LEDA_CTL, HIGH); + digitalWrite(PIN_TFT_RST, HIGH); + + display.initR(INITR_MINI160x80_PLUGIN); + display.setRotation(DISPLAY_ROTATION); + display.setSPISpeed(40000000); + display.fillScreen(ST77XX_BLACK); + display.setTextColor(ST77XX_WHITE); + display.setTextSize(2); + display.cp437(true); // Use full 256 char 'Code Page 437' font + + _isOn = true; + } + return true; +} + +void ST7735Display::turnOn() { + ST7735Display::begin(); +} + +void ST7735Display::turnOff() { + if (_isOn) { + digitalWrite(PIN_TFT_LEDA_CTL, HIGH); + digitalWrite(PIN_TFT_RST, LOW); + digitalWrite(PIN_TFT_LEDA_CTL, LOW); + _isOn = false; + + if (_peripher_power) _peripher_power->release(); + } +} + +void ST7735Display::clear() { + //Serial.println("DBG: display.Clear"); + display.fillScreen(ST77XX_BLACK); +} + +void ST7735Display::startFrame(Color bkg) { + display.fillScreen(0x00); + display.setTextColor(ST77XX_WHITE); + display.setTextSize(1); // This one affects size of Please wait... message + display.cp437(true); // Use full 256 char 'Code Page 437' font +} + +void ST7735Display::setTextSize(int sz) { + display.setTextSize(sz); +} + +void ST7735Display::setColor(Color c) { + switch (c) { + case DisplayDriver::DARK : + _color = ST77XX_BLACK; + break; + case DisplayDriver::LIGHT : + _color = ST77XX_WHITE; + break; + case DisplayDriver::RED : + _color = ST77XX_RED; + break; + case DisplayDriver::GREEN : + _color = ST77XX_GREEN; + break; + case DisplayDriver::BLUE : + _color = ST77XX_BLUE; + break; + case DisplayDriver::YELLOW : + _color = ST77XX_YELLOW; + break; + case DisplayDriver::ORANGE : + _color = ST77XX_ORANGE; + break; + default: + _color = ST77XX_WHITE; + break; + } + display.setTextColor(_color); +} + +void ST7735Display::setCursor(int x, int y) { + display.setCursor(x*SCALE_X, y*SCALE_Y); +} + +void ST7735Display::print(const char* str) { + display.print(str); +} + +void ST7735Display::fillRect(int x, int y, int w, int h) { + display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _color); +} + +void ST7735Display::drawRect(int x, int y, int w, int h) { + display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _color); +} + +void ST7735Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + display.drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, _color); +} + +uint16_t ST7735Display::getTextWidth(const char* str) { + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w / SCALE_X; +} + +void ST7735Display::endFrame() { + // display.display(); +} diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h new file mode 100644 index 00000000..94797503 --- /dev/null +++ b/src/helpers/ui/ST7735Display.h @@ -0,0 +1,49 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#include +#include +#include + +class ST7735Display : public DisplayDriver { + Adafruit_ST7735 display; + bool _isOn; + uint16_t _color; + RefCountedDigitalPin* _peripher_power; + + bool i2c_probe(TwoWire& wire, uint8_t addr); +public: +#ifdef USE_PIN_TFT + ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), + display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST), + _peripher_power(peripher_power) + { + _isOn = false; + } +#else + ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), + display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST), + _peripher_power(peripher_power) + { + _isOn = false; + } +#endif + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; +}; diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 8f04f872..0deebe3f 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -2,14 +2,16 @@ #include "ST7789Display.h" -bool ST7789Display::i2c_probe(TwoWire& wire, uint8_t addr) { - return true; -/* - wire.beginTransmission(addr); - uint8_t error = wire.endTransmission(); - return (error == 0); -*/ -} +#ifndef X_OFFSET +#define X_OFFSET 0 // No offset needed for landscape +#endif + +#ifndef Y_OFFSET +#define Y_OFFSET 1 // Vertical offset to prevent top row cutoff +#endif + +#define SCALE_X 1.875f // 240 / 128 +#define SCALE_Y 2.109375f // 135 / 64 bool ST7789Display::begin() { if(!_isOn) { @@ -19,14 +21,11 @@ bool ST7789Display::begin() { digitalWrite(PIN_TFT_LEDA_CTL, LOW); digitalWrite(PIN_TFT_RST, HIGH); - display.init(135, 240); - display.setRotation(2); - display.setSPISpeed(40000000); - display.fillScreen(ST77XX_BLACK); - display.setTextColor(ST77XX_WHITE); - display.setTextSize(2); - display.cp437(true); // Use full 256 char 'Code Page 437' font - + display.init(); + display.landscapeScreen(); + display.displayOn(); + setCursor(0,0); + _isOn = true; } return true; @@ -40,24 +39,28 @@ void ST7789Display::turnOff() { digitalWrite(PIN_TFT_VDD_CTL, HIGH); digitalWrite(PIN_TFT_LEDA_CTL, HIGH); digitalWrite(PIN_TFT_RST, LOW); - // digitalWrite(PIN_TFT_VDD_CTL, LOW); - // digitalWrite(PIN_TFT_LEDA_CTL, LOW); _isOn = false; } void ST7789Display::clear() { - display.fillScreen(ST77XX_BLACK); + display.clear(); } void ST7789Display::startFrame(Color bkg) { - display.fillScreen(0x00); - display.setTextColor(ST77XX_WHITE); - display.setTextSize(2); - display.cp437(true); // Use full 256 char 'Code Page 437' font + display.clear(); } void ST7789Display::setTextSize(int sz) { - display.setTextSize(sz); + switch(sz) { + case 1 : + display.setFont(ArialMT_Plain_16); + break; + case 2 : + display.setFont(ArialMT_Plain_24); + break; + default: + display.setFont(ArialMT_Plain_16); + } } void ST7789Display::setColor(Color c) { @@ -87,31 +90,68 @@ void ST7789Display::setColor(Color c) { _color = ST77XX_WHITE; break; } - display.setTextColor(_color); + display.setRGB(_color); } void ST7789Display::setCursor(int x, int y) { - display.setCursor(x, y); + _x = x*SCALE_X + X_OFFSET; + _y = y*SCALE_Y + Y_OFFSET; } void ST7789Display::print(const char* str) { - display.print(str); + display.drawString(_x, _y, str); } void ST7789Display::fillRect(int x, int y, int w, int h) { - display.fillRect(x, y, w, h, _color); + display.fillRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y); } void ST7789Display::drawRect(int x, int y, int w, int h) { - display.drawRect(x, y, w, h, _color); + display.drawRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y); } void ST7789Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { - display.drawBitmap(x, y, bits, w, h, _color); + // Calculate the base position in display coordinates + uint16_t startX = x * SCALE_X + X_OFFSET; + uint16_t startY = y * SCALE_Y + Y_OFFSET; + + // Width in bytes for bitmap processing + uint16_t widthInBytes = (w + 7) / 8; + + // Process the bitmap row by row + for (uint16_t by = 0; by < h; by++) { + // Calculate the target y-coordinates for this logical row + int y1 = startY + (int)(by * SCALE_Y); + int y2 = startY + (int)((by + 1) * SCALE_Y); + int block_h = y2 - y1; + + // Scan across the row bit by bit + for (uint16_t bx = 0; bx < w; bx++) { + // Calculate the target x-coordinates for this logical column + int x1 = startX + (int)(bx * SCALE_X); + int x2 = startX + (int)((bx + 1) * SCALE_X); + int block_w = x2 - x1; + + // Get the current bit + uint16_t byteOffset = (by * widthInBytes) + (bx / 8); + uint8_t bitMask = 0x80 >> (bx & 7); + bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask; + + // If the bit is set, draw a block of pixels + if (bitSet) { + // Draw the block as a filled rectangle + display.fillRect(x1, y1, block_w, block_h); + } + } + } +} + +uint16_t ST7789Display::getTextWidth(const char* str) { + return display.getStringWidth(str) / SCALE_X; } void ST7789Display::endFrame() { - // display.display(); + display.display(); } #endif \ No newline at end of file diff --git a/src/helpers/ui/ST7789Display.h b/src/helpers/ui/ST7789Display.h index af319ef0..b267a2cb 100644 --- a/src/helpers/ui/ST7789Display.h +++ b/src/helpers/ui/ST7789Display.h @@ -4,17 +4,18 @@ #include #include #include -#include +#include class ST7789Display : public DisplayDriver { - Adafruit_ST7789 display; + ST7789Spi display; bool _isOn; uint16_t _color; + int _x=0, _y=0; bool i2c_probe(TwoWire& wire, uint8_t addr); public: - ST7789Display() : DisplayDriver(135, 240), display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST) { _isOn = false; } -// ST7789Display() : DisplayDriver(135, 240), display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST) { _isOn = false; } + ST7789Display() : DisplayDriver(128, 64), display(&SPI1, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 240, 135) {_isOn = false;} + bool begin(); bool isOn() override { return _isOn; } @@ -29,5 +30,6 @@ public: void fillRect(int x, int y, int w, int h) override; void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; void endFrame() override; }; diff --git a/src/helpers/ui/ST7789Spi.h b/src/helpers/ui/ST7789Spi.h new file mode 100644 index 00000000..09af6457 --- /dev/null +++ b/src/helpers/ui/ST7789Spi.h @@ -0,0 +1,464 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 by ThingPulse, Daniel Eichhorn + * Copyright (c) 2018 by Fabrice Weinberg + * Copyright (c) 2024 by Heltec AutoMation + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ThingPulse invests considerable time and money to develop these open source libraries. + * Please support us by buying our products (and not the clones) from + * https://thingpulse.com + * + */ + +#ifndef ST7789Spi_h +#define ST7789Spi_h + +#include "OLEDDisplay.h" +#include + + +#define ST_CMD_DELAY 0x80 // special signifier for command lists + +#define ST77XX_NOP 0x00 +#define ST77XX_SWRESET 0x01 +#define ST77XX_RDDID 0x04 +#define ST77XX_RDDST 0x09 + +#define ST77XX_SLPIN 0x10 +#define ST77XX_SLPOUT 0x11 +#define ST77XX_PTLON 0x12 +#define ST77XX_NORON 0x13 + +#define ST77XX_INVOFF 0x20 +#define ST77XX_INVON 0x21 +#define ST77XX_DISPOFF 0x28 +#define ST77XX_DISPON 0x29 +#define ST77XX_CASET 0x2A +#define ST77XX_RASET 0x2B +#define ST77XX_RAMWR 0x2C +#define ST77XX_RAMRD 0x2E + +#define ST77XX_PTLAR 0x30 +#define ST77XX_TEOFF 0x34 +#define ST77XX_TEON 0x35 +#define ST77XX_MADCTL 0x36 +#define ST77XX_COLMOD 0x3A + +#define ST77XX_MADCTL_MY 0x80 +#define ST77XX_MADCTL_MX 0x40 +#define ST77XX_MADCTL_MV 0x20 +#define ST77XX_MADCTL_ML 0x10 +#define ST77XX_MADCTL_RGB 0x00 + +#define ST77XX_RDID1 0xDA +#define ST77XX_RDID2 0xDB +#define ST77XX_RDID3 0xDC +#define ST77XX_RDID4 0xDD + +// Some ready-made 16-bit ('565') color settings: +#define ST77XX_BLACK 0x0000 +#define ST77XX_WHITE 0xFFFF +#define ST77XX_RED 0xF800 +#define ST77XX_GREEN 0x07E0 +#define ST77XX_BLUE 0x001F +#define ST77XX_CYAN 0x07FF +#define ST77XX_MAGENTA 0xF81F +#define ST77XX_YELLOW 0xFFE0 +#define ST77XX_ORANGE 0xFC00 + +#define LED_A_ON LOW + +#ifdef ESP_PLATFORM +#undef LED_A_ON +#define LED_A_ON HIGH +#define rtos_free free +#define rtos_malloc malloc +//SPIClass SPI1(HSPI); +#endif +class ST7789Spi : public OLEDDisplay { + private: + uint8_t _rst; + uint8_t _dc; + uint8_t _cs; + uint8_t _ledA; + int _miso; + int _mosi; + int _clk; + SPIClass * _spi; + SPISettings _spiSettings; + uint16_t _RGB=0xFFFF; + uint8_t _buffheight; + public: + /* pass _cs as -1 to indicate "do not use CS pin", for cases where it is hard wired low */ + ST7789Spi(SPIClass *spiClass,uint8_t _rst, uint8_t _dc, uint8_t _cs, OLEDDISPLAY_GEOMETRY g = GEOMETRY_RAWMODE,uint16_t width=240,uint16_t height=135,int mosi=-1,int miso=-1,int clk=-1) { + this->_spi = spiClass; + this->_rst = _rst; + this->_dc = _dc; + this->_cs = _cs; + this->_mosi=mosi; + this->_miso=miso; + this->_clk=clk; + //this->_ledA = _ledA; + _spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); + setGeometry(g,width,height); + } + + bool connect(){ + this->_buffheight=displayHeight / 8; + this->_buffheight+=displayHeight % 8 ? 1:0; + pinMode(_cs, OUTPUT); + pinMode(_dc, OUTPUT); + //pinMode(_ledA, OUTPUT); + if (_cs != (uint8_t) -1) { + pinMode(_cs, OUTPUT); + } + pinMode(_rst, OUTPUT); + +#ifdef ESP_PLATFORM + _spi->begin(_clk,_miso,_mosi,-1); +#else + _spi->begin(); +#endif + _spi->setClockDivider (SPI_CLOCK_DIV2); + + // Pulse Reset low for 10ms + digitalWrite(_rst, HIGH); + delay(1); + digitalWrite(_rst, LOW); + delay(10); + digitalWrite(_rst, HIGH); + _spi->begin (); + //digitalWrite(_ledA, LED_A_ON); + return true; + } + + void display(void) { + #ifdef OLEDDISPLAY_DOUBLE_BUFFER + + uint16_t minBoundY = UINT16_MAX; + uint16_t maxBoundY = 0; + + uint16_t minBoundX = UINT16_MAX; + uint16_t maxBoundX = 0; + + uint16_t x, y; + + // Calculate the Y bounding box of changes + // and copy buffer[pos] to buffer_back[pos]; + for (y = 0; y < _buffheight; y++) { + for (x = 0; x < displayWidth; x++) { + //Serial.printf("x %d y %d\r\n",x,y); + uint16_t pos = x + y * displayWidth; + if (buffer[pos] != buffer_back[pos]) { + minBoundY = min(minBoundY, y); + maxBoundY = max(maxBoundY, y); + minBoundX = min(minBoundX, x); + maxBoundX = max(maxBoundX, x); + } + buffer_back[pos] = buffer[pos]; + } + yield(); + } + + // If the minBoundY wasn't updated + // we can savely assume that buffer_back[pos] == buffer[pos] + // holdes true for all values of pos + if (minBoundY == UINT16_MAX) return; + + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + + for (y = minBoundY; y <= maxBoundY; y++) + { + for(int temp = 0; temp<8;temp++) + { + //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + //setAddrWindow(y*8+temp,minBoundX,1,maxBoundX-minBoundX+1); + uint32_t const pixbufcount = maxBoundX-minBoundX+1; + uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount); + for (x = minBoundX; x <= maxBoundX; x++) + { + pixbuf[x-minBoundX] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0; + } +#ifdef ESP_PLATFORM + _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount); +#else + _spi->transfer(pixbuf, NULL, 2 * pixbufcount); +#endif + rtos_free(pixbuf); + } + } + _spi->endTransaction(); + set_CS(HIGH); + + #else + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + uint8_t x, y; + for (y = 0; y < _buffheight; y++) + { + for(int temp = 0; temp<8;temp++) + { + //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + //setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1); + setAddrWindow(y*8+temp,0,1,displayWidth); + uint32_t const pixbufcount = displayWidth; + uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount); + for (x = 0; x < displayWidth; x++) + { + pixbuf[x] = ((buffer[x + y * displayWidth]>>temp)&0x01)==1?_RGB:0; + } +#ifdef ESP_PLATFORM + _spi->transferBytes((uint8_t *)pixbuf, NULL, 2 * pixbufcount); +#else + _spi->transfer(pixbuf, NULL, 2 * pixbufcount); +#endif + rtos_free(pixbuf); + } + } + _spi->endTransaction(); + set_CS(HIGH); + + #endif + } + + virtual void resetOrientation() { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + virtual void flipScreenVertically() { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MY; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + virtual void mirrorScreen() { + uint8_t madctl = ST77XX_MADCTL_RGB|ST77XX_MADCTL_MV|ST77XX_MADCTL_MX|ST77XX_MADCTL_MY; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + virtual void landscapeScreen() { + // For landscape mode rotated 180 degrees with correct text direction + // MV swaps rows/columns for landscape orientation + // Adding MX (instead of MY) flips X axis and rotates 180 degrees + uint8_t madctl = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MX; + sendCommand(ST77XX_MADCTL); + WriteData(madctl); + delay(10); + } + + + void setRGB(uint16_t c) + { + + this->_RGB=0x00|c>>8|c<<8&0xFF00; + } + + void displayOn(void) { + //sendCommand(DISPLAYON); + } + + void displayOff(void) { + //sendCommand(DISPLAYOFF); + } + + void drawBitmap(int16_t xMove, int16_t yMove, int16_t width, int16_t height, const uint8_t *xbm) { + int16_t widthInXbm = (width + 7) / 8; + uint8_t data = 0; + + for(int16_t y = 0; y < height; y++) { + for(int16_t x = 0; x < width; x++ ) { + if (x & 7) { + data <<= 1; // Move a bit + } else { // Read new data every 8 bit + data = pgm_read_byte(xbm + (x / 8) + y * widthInXbm); + } + // if there is a bit draw it + if (data & 0x80) { + setPixel(xMove + x, yMove + y); + } + } + } + } + + +//#define ST77XX_MADCTL_MY 0x80 +//#define ST77XX_MADCTL_MX 0x40 +//#define ST77XX_MADCTL_MV 0x20 +//#define ST77XX_MADCTL_ML 0x10 + protected: + // Send all the init commands + virtual void sendInitCommands() + { + sendCommand(ST77XX_SWRESET); // 1: Software reset, no args, w/delay + delay(150); + + sendCommand(ST77XX_SLPOUT); // 2: Out of sleep mode, no args, w/delay + delay(10); + + sendCommand(ST77XX_COLMOD); // 3: Set color mode, 16-bit color + WriteData(0x55); + delay(10); + + // Initialize with landscape orientation rotated 180 degrees + uint8_t madctl = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MX; + sendCommand(ST77XX_MADCTL); // 4: Mem access ctrl (directions) + WriteData(madctl); + delay(10); + + // Set column address range for landscape orientation (240 pixels wide) + sendCommand(ST77XX_CASET); // 5: Column addr set + WriteData(0x00); + WriteData(0x00); // XSTART = 0 + WriteData(0x00); + WriteData(240); // XEND = 240 + + // Set row address range for landscape orientation (135 pixels tall) + sendCommand(ST77XX_RASET); // 6: Row addr set + WriteData(0x00); + WriteData(0x00); // YSTART = 0 + WriteData(0x00); + WriteData(135); // YEND = 135 + + sendCommand(ST77XX_SLPOUT); // 7: hack + delay(10); + + sendCommand(ST77XX_NORON); // 8: Normal display on, no args, w/delay + delay(10); + + sendCommand(ST77XX_DISPON); // 9: Main screen turn on, no args, delay + delay(10); + + sendCommand(ST77XX_INVON); // 10: invert + delay(10); + + // Use landscape mode instead of portrait + landscapeScreen(); + setRGB(ST77XX_GREEN); + } + + + private: + + void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + x += (320-displayWidth)/2; + y += (240-displayHeight)/2; + + uint32_t xa = ((uint32_t)x << 16) | (x + w - 1); + uint32_t ya = ((uint32_t)y << 16) | (y + h - 1); + + writeCommand(ST77XX_CASET); // Column addr set + SPI_WRITE32(xa); + + writeCommand(ST77XX_RASET); // Row addr set + SPI_WRITE32(ya); + + writeCommand(ST77XX_RAMWR); // write to RAM + } + int getBufferOffset(void) { + return 0; + } + inline void set_CS(bool level) { + if (_cs != (uint8_t) -1) { + digitalWrite(_cs, level); + } + }; + inline void sendCommand(uint8_t com) __attribute__((always_inline)){ + set_CS(HIGH); + digitalWrite(_dc, LOW); + set_CS(LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(com); + _spi->endTransaction(); + set_CS(HIGH); + digitalWrite(_dc, HIGH); + } + + inline void WriteData(uint8_t data) __attribute__((always_inline)){ + digitalWrite(_cs, LOW); + _spi->beginTransaction(_spiSettings); + _spi->transfer(data); + _spi->endTransaction(); + digitalWrite(_cs, HIGH); + } + void SPI_WRITE32(uint32_t l) + { + _spi->transfer(l >> 24); + _spi->transfer(l >> 16); + _spi->transfer(l >> 8); + _spi->transfer(l); + } + void writeCommand(uint8_t cmd) { + digitalWrite(_dc, LOW); + _spi->transfer(cmd); + digitalWrite(_dc, HIGH); + } + +// Private functions + void setGeometry(OLEDDISPLAY_GEOMETRY g, uint16_t width, uint16_t height) { + this->geometry = g; + + switch (g) { + case GEOMETRY_128_128: + this->displayWidth = 128; + this->displayHeight = 128; + break; + case GEOMETRY_128_64: + this->displayWidth = 128; + this->displayHeight = 64; + break; + case GEOMETRY_128_32: + this->displayWidth = 128; + this->displayHeight = 32; + break; + case GEOMETRY_64_48: + this->displayWidth = 64; + this->displayHeight = 48; + break; + case GEOMETRY_64_32: + this->displayWidth = 64; + this->displayHeight = 32; + break; + case GEOMETRY_RAWMODE: + this->displayWidth = width > 0 ? width : 128; + this->displayHeight = height > 0 ? height : 64; + break; + } + uint8_t tmp=displayHeight % 8; + uint8_t _buffheight=displayHeight / 8; + + if(tmp!=0) + _buffheight++; + this->displayBufferSize = displayWidth * _buffheight ; + } + + + +}; + +#endif diff --git a/src/helpers/ui/buzzer.cpp b/src/helpers/ui/buzzer.cpp new file mode 100644 index 00000000..ccc18cd3 --- /dev/null +++ b/src/helpers/ui/buzzer.cpp @@ -0,0 +1,54 @@ +#ifdef PIN_BUZZER +#include "buzzer.h" + +void genericBuzzer::begin() { +// Serial.print("DBG: Setting up buzzer on pin "); +// Serial.println(PIN_BUZZER); + #ifdef PIN_BUZZER_EN + pinMode(PIN_BUZZER_EN, OUTPUT); + digitalWrite(PIN_BUZZER_EN, HIGH); + #endif + + quiet(false); + pinMode(PIN_BUZZER, OUTPUT); + startup(); +} + +void genericBuzzer::play(const char *melody) { + if (isPlaying()) // interrupt existing + { + rtttl::stop(); + } + + if (_is_quiet) return; + + rtttl::begin(PIN_BUZZER,melody); +// Serial.print("DBG: Playing melody - isQuiet: "); +// Serial.println(isQuiet()); +} + +bool genericBuzzer::isPlaying() { + return rtttl::isPlaying(); +} + +void genericBuzzer::loop() { + if (!rtttl::done()) rtttl::play(); +} + +void genericBuzzer::startup() { + play(startup_song); +} + +void genericBuzzer::shutdown() { + play(shutdown_song); +} + +void genericBuzzer::quiet(bool buzzer_state) { + _is_quiet = buzzer_state; +} + +bool genericBuzzer::isQuiet() { + return _is_quiet; +} + +#endif // ifdef PIN_BUZZER \ No newline at end of file diff --git a/src/helpers/ui/buzzer.h b/src/helpers/ui/buzzer.h new file mode 100644 index 00000000..0a500552 --- /dev/null +++ b/src/helpers/ui/buzzer.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +/* class abstracts underlying RTTTL library + + Just a simple imlementation to start. At the moment use same + melody for message and discovery + Suggest enum type for different sounds + - on message + - on discovery + + TODO + - make message ring tone configurable + +*/ + +class genericBuzzer +{ + public: + void begin(); // set up buzzer port + void play(const char *melody); // Generic play function + void loop(); // loop driven-nonblocking + void startup(); // play startup sound + void shutdown(); // play shutdown sound + bool isPlaying(); // returns true if a sound is still playing else false + void quiet(bool buzzer_state); // enables or disables the buzzer + bool isQuiet(); // get buzzer state on/off + + private: + // gemini's picks: + const char *startup_song = "Startup:d=4,o=5,b=160:16c6,16e6,8g6"; + const char *shutdown_song = "Shutdown:d=4,o=5,b=100:8g5,16e5,16c5"; + + bool _is_quiet = true; +}; diff --git a/variants/generic-e22/platformio.ini b/variants/generic-e22/platformio.ini new file mode 100644 index 00000000..8b2c293b --- /dev/null +++ b/variants/generic-e22/platformio.ini @@ -0,0 +1,68 @@ +[Generic_E22] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -I variants/generic-e22 + -D GENERIC_E22 + -D P_LORA_TX_LED=2 + -D PIN_VBAT_READ=35 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=18 + -D P_LORA_RESET=RADIOLIB_NC + -D P_LORA_BUSY=32 + -D P_LORA_SCLK=5 + -D P_LORA_MOSI=27 + -D P_LORA_MISO=19 + -D SX126X_TXEN=13 + -D SX126X_RXEN=14 + -D PIN_BOARD_SDA=21 + -D PIN_BOARD_SCL=22 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/generic-e22> +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Generic_E22_sx1262_repeater] +extends = Generic_E22 +build_src_filter = ${Generic_E22.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = + ${Generic_E22.build_flags} + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"E22 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Generic_E22.lib_deps} + ${esp32_ota.lib_deps} + +[env:Generic_E22_sx1268_repeater] +extends = Generic_E22 +build_src_filter = ${Generic_E22.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = + ${Generic_E22.build_flags} + -D RADIO_CLASS=CustomSX1268 + -D WRAPPER_CLASS=CustomSX1268Wrapper + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"E22 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +lib_deps = + ${Generic_E22.lib_deps} + ${esp32_ota.lib_deps} diff --git a/variants/generic-e22/target.cpp b/variants/generic-e22/target.cpp new file mode 100644 index 00000000..d9b70c3a --- /dev/null +++ b/variants/generic-e22/target.cpp @@ -0,0 +1,80 @@ +#include +#include "target.h" + +ESP32Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + +#if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#if defined(SX126X_RXEN) && defined(SX126X_TXEN) + radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); +#endif + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/generic-e22/target.h b/variants/generic-e22/target.h new file mode 100644 index 00000000..e5c116e1 --- /dev/null +++ b/variants/generic-e22/target.h @@ -0,0 +1,21 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include + +extern ESP32Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/generic-e22/variant.h b/variants/generic-e22/variant.h new file mode 100644 index 00000000..6c850af2 --- /dev/null +++ b/variants/generic-e22/variant.h @@ -0,0 +1,44 @@ +// For OLED LCD +#define I2C_SDA 21 +#define I2C_SCL 22 + +// For GPS, 'undef's not needed +#define GPS_TX_PIN 15 +#define GPS_RX_PIN 12 +#define PIN_GPS_EN 4 +#define GPS_POWER_TOGGLE // Moved definition from platformio.ini to here + +#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) +#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards +#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). +#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) + +// Radio +#define USE_SX1262 // E22-900M30S uses SX1262 +#define USE_SX1268 // E22-400M30S uses SX1268 +#define SX126X_MAX_POWER 22 // Outputting 22dBm from SX1262 results in ~30dBm E22-900M30S output (module only uses last stage of the YP2233W PA) +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // E22 series TCXO reference voltage is 1.8V + +#define SX126X_CS 18 // EBYTE module's NSS pin +#define SX126X_SCK 5 // EBYTE module's SCK pin +#define SX126X_MOSI 27 // EBYTE module's MOSI pin +#define SX126X_MISO 19 // EBYTE module's MISO pin +#define SX126X_RESET 23 // EBYTE module's NRST pin +#define SX126X_BUSY 32 // EBYTE module's BUSY pin +#define SX126X_DIO1 33 // EBYTE module's DIO1 pin + +// The E22's TXEN pin is connected to MCU pin, E22's RXEN pin is connected to MCU pin (allows for ramping up PA before transmission +// Don't define DIO2_AS_RF_SWITCH because we only use DIO2 or an MCU pin mutually exclusively to connect to E22's TXEN (to prevent +// a short if they are both connected at the same time and there's a slight non-neglibible delay and/or voltage difference between +// DIO2 and TXEN). +#define SX126X_TXEN 13 // Schematic connects EBYTE module's TXEN pin to MCU +#define SX126X_RXEN 14 // Schematic connects EBYTE module's RXEN pin to MCU + +#define LORA_CS SX126X_CS // Compatibility with variant file configuration structure +#define LORA_SCK SX126X_SCK // Compatibility with variant file configuration structure +#define LORA_MOSI SX126X_MOSI // Compatibility with variant file configuration structure +#define LORA_MISO SX126X_MISO // Compatibility with variant file configuration structure +#define LORA_DIO1 SX126X_DIO1 // Compatibility with variant file configuration structure diff --git a/variants/generic_espnow/platformio.ini b/variants/generic_espnow/platformio.ini index 802902be..8a033a62 100644 --- a/variants/generic_espnow/platformio.ini +++ b/variants/generic_espnow/platformio.ini @@ -19,7 +19,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${esp32_base.build_src_filter} - + + + +<../variants/generic_espnow> [env:Generic_ESPNOW_terminal_chat] @@ -42,6 +42,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 build_src_filter = ${Generic_ESPNOW.build_src_filter} +<../examples/simple_repeater/main.cpp> lib_deps = diff --git a/variants/generic_espnow/target.cpp b/variants/generic_espnow/target.cpp index a92b88ac..6b5d4e44 100644 --- a/variants/generic_espnow/target.cpp +++ b/variants/generic_espnow/target.cpp @@ -7,11 +7,13 @@ ESP32Board board; ESPNOWRadio radio_driver; ESP32RTCClock rtc_clock; +SensorManager sensors; bool radio_init() { rtc_clock.begin(); - // NOTE: radio_driver.begin() is called by Dispatcher::begin(), so not needed here + radio_driver.init(); + return true; // success } @@ -27,7 +29,16 @@ void radio_set_tx_power(uint8_t dbm) { radio_driver.setTxPower(dbm); } +// NOTE: as we are using the WiFi radio, the ESP_IDF will have enabled hardware RNG: +// https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/random.html +class ESP_RNG : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + esp_fill_random(dest, sz); + } +}; + mesh::LocalIdentity radio_new_identity() { - StdRNG rng; // TODO: need stronger True-RNG here + ESP_RNG rng; return mesh::LocalIdentity(&rng); // create new random identity } diff --git a/variants/generic_espnow/target.h b/variants/generic_espnow/target.h index 16eeaa2e..99b6f577 100644 --- a/variants/generic_espnow/target.h +++ b/variants/generic_espnow/target.h @@ -2,10 +2,12 @@ #include #include +#include extern ESP32Board board; extern ESPNOWRadio radio_driver; extern ESP32RTCClock rtc_clock; +extern SensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini new file mode 100644 index 00000000..d62771f4 --- /dev/null +++ b/variants/heltec_tracker/platformio.ini @@ -0,0 +1,100 @@ +[Heltec_tracker_base] +extends = esp32_base +board = esp32-s3-devkitc-1 +build_flags = + ${esp32_base.build_flags} + -I variants/heltec_tracker + -D HELTEC_LORA_V3 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D P_LORA_TX_LED=18 + -D PIN_BOARD_SDA=45 + -D PIN_BOARD_SCL=46 + -D PIN_USER_BTN=0 + -D PIN_ADC_CTRL=2 + -D PIN_TFT_SDA=42 ; SDIN + -D PIN_TFT_SCL=41 ; SCLK + -D PIN_TFT_DC=40 ; RS (register select) + -D PIN_TFT_RST=39 ; RES + -D PIN_TFT_CS=38 + -D USE_PIN_TFT=1 + -D PIN_VEXT_EN=3 ; Vext is connected to VDD which is also connected to OLED & GPS + -D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground) + -D PIN_GPS_RX=33 + -D PIN_GPS_TX=34 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/heltec_tracker> +lib_deps = + ${esp32_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + +[env:Heltec_Wireless_Tracker_companion_radio_ble] +extends = Heltec_tracker_base +build_flags = + ${Heltec_tracker_base.build_flags} + -I src/helpers/ui + ; -D ARDUINO_USB_CDC_ON_BOOT=1 ; need for debugging + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 ; HWT will use display for pin + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + +<../examples/companion_radio> + + +lib_deps = + ${Heltec_tracker_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Heltec_Wireless_Tracker_repeater] +extends = Heltec_tracker_base +build_flags = + ${Heltec_tracker_base.build_flags} + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D ADVERT_NAME='"Heltec Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${Heltec_tracker_base.lib_deps} + ${esp32_ota.lib_deps} + +[env:Heltec_Wireless_Tracker_room_server] +extends = Heltec_tracker_base +build_flags = + ${Heltec_tracker_base.build_flags} + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D ADVERT_NAME='"Heltec Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${Heltec_tracker_base.lib_deps} + ${esp32_ota.lib_deps} diff --git a/variants/heltec_tracker/target.cpp b/variants/heltec_tracker/target.cpp new file mode 100644 index 00000000..042c7c0d --- /dev/null +++ b/variants/heltec_tracker/target.cpp @@ -0,0 +1,152 @@ +#include +#include "target.h" + +#include + +HeltecV3Board board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +HWTSensorManager sensors = HWTSensorManager(nmea); + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display(&board.periph_power); // peripheral power pin is shared +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + +#if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} + +void HWTSensorManager::start_gps() { + if (!gps_active) { + board.periph_power.claim(); + + gps_active = true; + Serial1.println("$CFGSYS,h35155*68"); + } +} + +void HWTSensorManager::stop_gps() { + if (gps_active) { + gps_active = false; + + board.periph_power.release(); + } +} + +bool HWTSensorManager::begin() { + // init GPS port + Serial1.begin(115200, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); + return true; +} + +bool HWTSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void HWTSensorManager::loop() { + static long next_gps_update = 0; + + _location->loop(); + + if (millis() > next_gps_update) { + if (gps_active && _location->isValid()) { + node_lat = ((double)_location->getLatitude())/1000000.; + node_lon = ((double)_location->getLongitude())/1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + } + next_gps_update = millis() + 1000; + } +} + +int HWTSensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char* HWTSensorManager::getSettingName(int i) const { + return i == 0 ? "gps" : NULL; +} +const char* HWTSensorManager::getSettingValue(int i) const { + if (i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} +bool HWTSensorManager::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + if (strcmp(value, "0") == 0) { + stop_gps(); + } else { + start_gps(); + } + return true; + } + return false; // not supported +} diff --git a/variants/heltec_tracker/target.h b/variants/heltec_tracker/target.h new file mode 100644 index 00000000..3184bec9 --- /dev/null +++ b/variants/heltec_tracker/target.h @@ -0,0 +1,45 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif + +class HWTSensorManager : public SensorManager { + bool gps_active = false; + LocationProvider * _location; + + void start_gps(); + void stop_gps(); +public: + HWTSensorManager(LocationProvider &location): _location(&location) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; +}; + +extern HeltecV3Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern HWTSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index bb1a8038..495f20f8 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -5,13 +5,14 @@ build_flags = ${esp32_base.build_flags} -I variants/heltec_v2 -D HELTEC_LORA_V2 + -D RADIO_CLASS=CustomSX1276 + -D WRAPPER_CLASS=CustomSX1276Wrapper + -D SX127X_CURRENT_LIMIT=120 + -D LORA_TX_POWER=20 -D PIN_BOARD_SDA=4 -D PIN_BOARD_SCL=15 -D PIN_USER_BTN=0 -D PIN_OLED_RESET=16 - -D RADIO_CLASS=CustomSX1276 - -D WRAPPER_CLASS=CustomSX1276Wrapper - -D LORA_TX_POWER=20 -D P_LORA_TX_LED=25 build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_v2> @@ -28,6 +29,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} @@ -96,6 +98,7 @@ build_flags = -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=0 -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_v2/target.cpp b/variants/heltec_v2/target.cpp index 284fa600..a7e9fa67 100644 --- a/variants/heltec_v2/target.cpp +++ b/variants/heltec_v2/target.cpp @@ -14,6 +14,11 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 @@ -22,7 +27,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); - + #if defined(P_LORA_SCLK) spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); #endif @@ -33,8 +38,12 @@ bool radio_init() { return false; // fail } +#ifdef SX127X_CURRENT_LIMIT + radio.setCurrentLimit(SX127X_CURRENT_LIMIT); +#endif + radio.setCRC(1); - + return true; // success } diff --git a/variants/heltec_v2/target.h b/variants/heltec_v2/target.h index bfdb6132..f758c193 100644 --- a/variants/heltec_v2/target.h +++ b/variants/heltec_v2/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern HeltecV2Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 49fa7434..0814ad4d 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -12,9 +12,10 @@ build_flags = -D PIN_BOARD_SDA=17 -D PIN_BOARD_SCL=18 -D PIN_USER_BTN=0 + -D PIN_VEXT_EN=36 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 - -D SX126X_CURRENT_LIMIT=130.0f ; for best TX power! + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${esp32_base.build_src_filter} +<../variants/heltec_v3> @@ -31,7 +32,8 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' - -D MESH_PACKET_LOGGING=1 + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + @@ -100,6 +102,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D BLE_PIN_CODE=0 ; dynamic, random PIN -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 @@ -142,7 +145,8 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' - -D MESH_PACKET_LOGGING=1 + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_repeater> @@ -175,6 +179,7 @@ build_flags = -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_v3/target.cpp b/variants/heltec_v3/target.cpp index 768cd84c..ab9b709f 100644 --- a/variants/heltec_v3/target.cpp +++ b/variants/heltec_v3/target.cpp @@ -14,6 +14,11 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/heltec_v3/target.h b/variants/heltec_v3/target.h index 04fec40c..76ad58a7 100644 --- a/variants/heltec_v3/target.h +++ b/variants/heltec_v3/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern HeltecV3Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index c857d51d..94ec87af 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -21,7 +21,7 @@ build_flags = -D PIN_OLED_RESET=21 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 @@ -42,6 +42,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} @@ -111,6 +112,7 @@ build_flags = -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/lilygo_t3s3/target.cpp b/variants/lilygo_t3s3/target.cpp index 8e8b8e75..7fa45e54 100644 --- a/variants/lilygo_t3s3/target.cpp +++ b/variants/lilygo_t3s3/target.cpp @@ -14,6 +14,11 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/lilygo_t3s3/target.h b/variants/lilygo_t3s3/target.h index 8eed467f..609248ae 100644 --- a/variants/lilygo_t3s3/target.h +++ b/variants/lilygo_t3s3/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern ESP32Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/lilygo_tbeam/platformio.ini b/variants/lilygo_tbeam/platformio.ini index 129d2d32..c471e44c 100644 --- a/variants/lilygo_tbeam/platformio.ini +++ b/variants/lilygo_tbeam/platformio.ini @@ -7,6 +7,7 @@ build_flags = -D LILYGO_TBEAM -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper + -D SX127X_CURRENT_LIMIT=120 -D LORA_TX_POWER=20 -D P_LORA_TX_LED=4 -D PIN_BOARD_SDA=21 @@ -30,9 +31,8 @@ build_flags = -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 - -D RADIOLIB_DEBUG_BASIC=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D RADIOLIB_DEBUG_BASIC=1 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 @@ -54,7 +54,8 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' - -D MESH_PACKET_LOGGING=1 + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam.build_src_filter} + diff --git a/variants/lilygo_tbeam/target.cpp b/variants/lilygo_tbeam/target.cpp index d5889533..0e0b91c6 100644 --- a/variants/lilygo_tbeam/target.cpp +++ b/variants/lilygo_tbeam/target.cpp @@ -39,6 +39,11 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif static void setPMUIntFlag(){ pmuIntFlag = true; @@ -157,7 +162,7 @@ bool TBeamBoard::power_init() bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); - + #if defined(P_LORA_SCLK) spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); #endif @@ -168,10 +173,12 @@ bool radio_init() { return false; // fail } +#ifdef SX127X_CURRENT_LIMIT + radio.setCurrentLimit(SX127X_CURRENT_LIMIT); +#endif radio.setCRC(1); - return true; // success } diff --git a/variants/lilygo_tbeam/target.h b/variants/lilygo_tbeam/target.h index 1fd68a84..4a9f0b3c 100644 --- a/variants/lilygo_tbeam/target.h +++ b/variants/lilygo_tbeam/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern TBeamBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini new file mode 100644 index 00000000..517fc2e0 --- /dev/null +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -0,0 +1,70 @@ +[LilyGo_TBeam_SX1262] +extends = esp32_base +board = ttgo-t-beam +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_tbeam_SX1262 + -D LILYGO_TBEAM_SX1262 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D P_LORA_TX_LED=4 + -D PIN_BOARD_SDA=21 + -D PIN_BOARD_SCL=22 + -D PIN_USER_BTN=38 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/lilygo_tbeam_SX1262> +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit +lib_deps = + ${esp32_base.lib_deps} + lewisxhe/XPowersLib@^0.2.7 + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:Tbeam_SX1262_companion_radio_ble] +extends = LilyGo_TBeam_SX1262 +board_build.upload.maximum_ram_size=2000000 +build_flags = + ${LilyGo_TBeam_SX1262.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +; -D RADIOLIB_DEBUG_BASIC=1 +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} + + + + + +<../examples/companion_radio> +lib_deps = + ${LilyGo_TBeam_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Tbeam_SX1262_repeater] +extends = LilyGo_TBeam_SX1262 +build_flags = + ${LilyGo_TBeam_SX1262.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"Tbeam SX1262 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam_SX1262.lib_deps} + ${esp32_ota.lib_deps} \ No newline at end of file diff --git a/variants/lilygo_tbeam_SX1262/target.cpp b/variants/lilygo_tbeam_SX1262/target.cpp new file mode 100644 index 00000000..2f6666c5 --- /dev/null +++ b/variants/lilygo_tbeam_SX1262/target.cpp @@ -0,0 +1,70 @@ +#include +#include "target.h" + +TBeamBoardSX1262 board; + +#if defined(P_LORA_SCLK) + static SPIClass spi; + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else + RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + +#if defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/lilygo_tbeam_SX1262/target.h b/variants/lilygo_tbeam_SX1262/target.h new file mode 100644 index 00000000..3f4d77fa --- /dev/null +++ b/variants/lilygo_tbeam_SX1262/target.h @@ -0,0 +1,27 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif + +extern TBeamBoardSX1262 board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 57cc68db..e618613c 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -5,18 +5,22 @@ build_flags = ${esp32_base.build_flags} -I variants/lilygo_tbeam_supreme_SX1262 -D LORA_TX_POWER=22 + -D P_LORA_TX_LED=6 + -D PIN_BOARD_SDA=17 + -D PIN_BOARD_SCL=18 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper ;-D DISPLAY_CLASS=SSD1306Display ;Needs to be modified for SH1106 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_CURRENT_LIMIT=140 build_src_filter = ${esp32_base.build_src_filter} +<../variants/lilygo_tbeam_supreme_SX1262> board_build.partitions = min_spiffs.csv ; get around 4mb flash limit lib_deps = ${esp32_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 lewisxhe/XPowersLib @ ^0.2.7 ;adafruit/Adafruit SSD1306 @ ^2.5.13 + stevemarple/MicroNMEA @ ^2.0.6 ; === LILYGO T-Beam S3 Supreme with SX1262 environments === [env:T_Beam_S3_Supreme_SX1262_repeater] @@ -27,6 +31,7 @@ build_flags = -D ADVERT_LAT=0 -D ADVERT_LON=0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} @@ -57,16 +62,17 @@ extends = T_Beam_S3_Supreme_SX1262 build_flags = ${T_Beam_S3_Supreme_SX1262.build_flags} -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=1 + -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 - -D MESH_PACKET_LOGGING=8 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=8 +; -D MESH_DEBUG=1 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + +<../examples/companion_radio> lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} - densaugeo/base64 @ ~1.4.0 \ No newline at end of file + densaugeo/base64 @ ~1.4.0 diff --git a/variants/lilygo_tbeam_supreme_SX1262/target.cpp b/variants/lilygo_tbeam_supreme_SX1262/target.cpp index c7ab2984..f5b7080b 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/target.cpp +++ b/variants/lilygo_tbeam_supreme_SX1262/target.cpp @@ -1,11 +1,9 @@ #include #include "target.h" +#include TBeamS3SupremeBoard board; -// Using PMU AXP2102 -XPowersAXP2101 PMU; - bool pmuIntFlag; #ifndef LORA_CR @@ -23,114 +21,273 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); - +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +TbeamSupSensorManager sensors = TbeamSupSensorManager(nmea); static void setPMUIntFlag(){ pmuIntFlag = true; } -bool power_init() { - //Start up Wire1 with PMU address - //Serial.println("Starting Wire1 for PMU"); - //Wire1.begin(I2C_PMU_ADD); - //Wire1.begin(PIN_BOARD_SDA1,PIN_BOARD_SCL1); - - //Set LED to indicate charge state - Serial.println("Setting charge led"); - PMU.setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); - - //Set up PMU interrupts - Serial.println("Setting up PMU interrupts"); - pinMode(PIN_PMU_IRQ,INPUT_PULLUP); - attachInterrupt(PIN_PMU_IRQ,setPMUIntFlag,FALLING); +#ifdef MESH_DEBUG +uint32_t deviceOnline = 0x00; +void scanDevices(TwoWire *w) +{ + uint8_t err, addr; + int nDevices = 0; + uint32_t start = 0; - //GPS - Serial.println("Setting and enabling a-ldo4 for GPS"); - PMU.setALDO4Voltage(3300); - PMU.enableALDO4(); //disable to save power - - //Lora - Serial.println("Setting and enabling a-ldo3 for LoRa"); - PMU.setALDO3Voltage(3300); - PMU.enableALDO3(); + Serial.println("Scanning I2C for Devices"); + for (addr = 1; addr < 127; addr++) { + start = millis(); + w->beginTransmission(addr); delay(2); + err = w->endTransmission(); + if (err == 0) { + nDevices++; + switch (addr) { + case 0x77: + case 0x76: + Serial.println("\tFound BMX280 Sensor"); + deviceOnline |= BME280_ONLINE; + break; + case 0x34: + Serial.println("\tFound AXP192/AXP2101 PMU"); + deviceOnline |= POWERMANAGE_ONLINE; + break; + case 0x3C: + Serial.println("\tFound SSD1306/SH1106 dispaly"); + deviceOnline |= DISPLAY_ONLINE; + break; + case 0x51: + Serial.println("\tFound PCF8563 RTC"); + deviceOnline |= PCF8563_ONLINE; + break; + case 0x1C: + Serial.println("\tFound QMC6310 MAG Sensor"); + deviceOnline |= QMC6310_ONLINE; + break; + default: + Serial.print("\tI2C device found at address 0x"); + if (addr < 16) { + Serial.print("0"); + } + Serial.print(addr, HEX); + Serial.println(" !"); + break; + } - //To avoid SPI bus issues during power up, reset OLED, sensor, and SD card supplies - Serial.println("Reset a-ldo1&2 and b-ldo1"); - if(ESP_SLEEP_WAKEUP_UNDEFINED == esp_sleep_get_wakeup_cause()){ - PMU.enableALDO1(); - PMU.enableALDO2(); - PMU.enableBLDO1(); - delay(250); + } else if (err == 4) { + Serial.print("Unknow error at address 0x"); + if (addr < 16) { + Serial.print("0"); + } + Serial.println(addr, HEX); + } + } + if (nDevices == 0) + Serial.println("No I2C devices found\n"); + + Serial.println("Scan for devices is complete."); + Serial.println("\n"); +} +void TBeamS3SupremeBoard::printPMU() +{ + Serial.print("isCharging:"); Serial.println(PMU.isCharging() ? "YES" : "NO"); + Serial.print("isDischarge:"); Serial.println(PMU.isDischarge() ? "YES" : "NO"); + Serial.print("isVbusIn:"); Serial.println(PMU.isVbusIn() ? "YES" : "NO"); + Serial.print("getBattVoltage:"); Serial.print(PMU.getBattVoltage()); Serial.println("mV"); + Serial.print("getVbusVoltage:"); Serial.print(PMU.getVbusVoltage()); Serial.println("mV"); + Serial.print("getSystemVoltage:"); Serial.print(PMU.getSystemVoltage()); Serial.println("mV"); + + // The battery percentage may be inaccurate at first use, the PMU will automatically + // learn the battery curve and will automatically calibrate the battery percentage + // after a charge and discharge cycle + if (PMU.isBatteryConnect()) { + Serial.print("getBatteryPercent:"); Serial.print(PMU.getBatteryPercent()); Serial.println("%"); + } + + Serial.println(); +} +#endif + +bool TBeamS3SupremeBoard::power_init() +{ + bool result = PMU.begin(PMU_WIRE_PORT, I2C_PMU_ADD, PIN_BOARD_SDA1, PIN_BOARD_SCL1); + if (result == false) { + MESH_DEBUG_PRINTLN("power is not online..."); while (1)delay(50); } - - //BME280 and OLED - Serial.println("Setting and enabling a-ldo1 for oled"); + MESH_DEBUG_PRINTLN("Setting charge led"); + PMU.setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); + + // Set up PMU interrupts + // MESH_DEBUG_PRINTLN("Setting up PMU interrupts"); + // pinMode(PIN_PMU_IRQ, INPUT_PULLUP); + // attachInterrupt(PIN_PMU_IRQ, setPMUIntFlag, FALLING); + + // GPS + MESH_DEBUG_PRINTLN("Setting and enabling a-ldo4 for GPS"); + PMU.setALDO4Voltage(3300); + PMU.enableALDO4(); // disable to save power + + // Lora + MESH_DEBUG_PRINTLN("Setting and enabling a-ldo3 for LoRa"); + PMU.setALDO3Voltage(3300); + PMU.enableALDO3(); + + // To avoid SPI bus issues during power up, reset OLED, sensor, and SD card supplies + // MESH_DEBUG_PRINTLN("Reset a-ldo1&2 and b-ldo1"); + // if (ESP_SLEEP_WAKEUP_UNDEFINED == esp_sleep_get_wakeup_cause()) + // { + // PMU.disableALDO1(); + // PMU.disableALDO2(); + // PMU.disableBLDO1(); + // delay(250); + // } + + // m.2 interface + MESH_DEBUG_PRINTLN("Setting and enabling dcdc3 for m.2 interface"); + PMU.setDC3Voltage(3300); // doesn't go anywhere in the schematic?? + PMU.enableDC3(); + + // QMC6310U + MESH_DEBUG_PRINTLN("Setting and enabling a-ldo2 for QMC"); + PMU.setALDO2Voltage(3300); + PMU.enableALDO2(); // disable to save power + + // BME280 and OLED + MESH_DEBUG_PRINTLN("Setting and enabling a-ldo1 for oled"); PMU.setALDO1Voltage(3300); PMU.enableALDO1(); - //QMC6310U - Serial.println("Setting and enabling a-ldo2 for QMC"); - PMU.setALDO2Voltage(3300); - PMU.enableALDO2(); //disable to save power - - //SD card - Serial.println("Setting and enabling b-ldo2 for SD card"); + // SD card + MESH_DEBUG_PRINTLN("Setting and enabling b-ldo2 for SD card"); PMU.setBLDO1Voltage(3300); PMU.enableBLDO1(); - //Out to header pins - Serial.println("Setting and enabling b-ldo2 for output to header"); - PMU.setBLDO2Voltage(3300); - PMU.enableBLDO2(); + // Out to header pins + // MESH_DEBUG_PRINTLN("Setting and enabling b-ldo2 for output to header"); + // PMU.setBLDO2Voltage(3300); + // PMU.enableBLDO2(); - Serial.println("Setting and enabling dcdc4 for output to header"); - PMU.setDC4Voltage(XPOWERS_AXP2101_DCDC4_VOL2_MAX); //1.8V - PMU.enableDC4(); + // MESH_DEBUG_PRINTLN("Setting and enabling dcdc4 for output to header"); + // PMU.setDC4Voltage(XPOWERS_AXP2101_DCDC4_VOL2_MAX); // 1.8V + // PMU.enableDC4(); - Serial.println("Setting and enabling dcdc5 for output to header"); - PMU.setDC5Voltage(3300); - PMU.enableDC5(); + // MESH_DEBUG_PRINTLN("Setting and enabling dcdc5 for output to header"); + // PMU.setDC5Voltage(3300); + // PMU.enableDC5(); - //Other power rails - Serial.println("Setting and enabling dcdc3 for ?"); - PMU.setDC3Voltage(3300); //doesn't go anywhere in the schematic?? - PMU.enableDC3(); - - //Unused power rails - Serial.println("Disabling unused supplies dcdc2, dldo1 and dldo2"); + // Unused power rails + MESH_DEBUG_PRINTLN("Disabling unused supplies dcdc2, dldo1 and dldo2"); PMU.disableDC2(); + PMU.disableDC5(); PMU.disableDLDO1(); - PMU.disableDLDO2(); + PMU.disableDLDO2(); - //Set charge current to 300mA - Serial.println("Setting battery charge current limit and voltage"); - PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_300MA); + PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + + // Set charge current to 500mA + MESH_DEBUG_PRINTLN("Setting battery charge current limit and voltage"); + PMU.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); PMU.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); - //enable battery voltage measurement - Serial.println("Enabling battery measurement"); - PMU.enableBattVoltageMeasure(); - - //Reset and re-enable PMU interrupts - Serial.println("Re-enable interrupts"); - PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ); PMU.clearIrqStatus(); - PMU.enableIRQ( - XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_BAT_REMOVE_IRQ | //Battery interrupts - XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | //VBUS interrupts - XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_LONG_IRQ | //Power Key interrupts - XPOWERS_AXP2101_BAT_CHG_DONE_IRQ | XPOWERS_AXP2101_BAT_CHG_START_IRQ //Charging interrupts - ); + PMU.disableTSPinMeasure(); - //Set the power key off press time + // enable battery voltage measurement + MESH_DEBUG_PRINTLN("Enabling battery measurement"); + PMU.enableBattVoltageMeasure(); + PMU.enableVbusVoltageMeasure(); + + // Reset and re-enable PMU interrupts + // MESH_DEBUG_PRINTLN("Re-enable interrupts"); + // PMU.disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + // PMU.clearIrqStatus(); + // PMU.enableIRQ( + // XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_BAT_REMOVE_IRQ | // Battery interrupts + // XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | // VBUS interrupts + // XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_LONG_IRQ | // Power Key interrupts + // XPOWERS_AXP2101_BAT_CHG_DONE_IRQ | XPOWERS_AXP2101_BAT_CHG_START_IRQ // Charging interrupts + // ); +#ifdef MESH_DEBUG + // scanDevices(&Wire); + // scanDevices(&Wire1); + printPMU(); +#endif + + // Set the power key off press time PMU.setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S); - return true; } +static bool readStringUntil(Stream& s, char dest[], size_t max_len, char term, unsigned int timeout_millis) { + unsigned long timeout = millis() + timeout_millis; + char *dp = dest; + while (millis() < timeout && dp - dest < max_len - 1) { + if (s.available()) { + char c = s.read(); + if (c == term) break; + *dp++ = c; // append to dest[] + } else { + delay(1); + } + } + *dp = 0; // null terminator + return millis() < timeout; // false, if timed out +} + +static bool l76kProbe() +{ + bool result = false; + uint32_t startTimeout ; + Serial1.write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); + delay(5); + // Get version information + startTimeout = millis() + 3000; + MESH_DEBUG_PRINTLN("Trying to init L76K GPS"); + // Serial1.flush(); + while (Serial1.available()) { + int c = Serial1.read(); + // Serial.write(c); + // Serial.print("."); + // Serial.flush(); + // Serial1.flush(); + if (millis() > startTimeout) { + MESH_DEBUG_PRINTLN("L76K NMEA timeout!"); + return false; + } + }; + + Serial1.flush(); + delay(200); + + Serial1.write("$PCAS06,0*1B\r\n"); + + char ver[100]; + if (!readStringUntil(Serial1, ver, sizeof(ver), '\n', 500)) { + MESH_DEBUG_PRINTLN("Get L76K timeout!"); + return false; + } + + if (memcmp(ver, "$GPTXT,01,01,02", 15) == 0) { + MESH_DEBUG_PRINTLN("L76K GNSS init succeeded, using L76K GNSS Module\n"); + result = true; + } + delay(500); + + // Initialize the L76K Chip, use GPS + GLONASS + Serial1.write("$PCAS04,5*1C\r\n"); + delay(250); + // only ask for RMC and GGA + Serial1.write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n"); + delay(250); + // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g + Serial1.write("$PCAS11,3*1E\r\n"); + return result; +} + bool radio_init() { fallback_clock.begin(); - Wire1.begin(PIN_BOARD_SDA1,PIN_BOARD_SCL1); + rtc_clock.begin(Wire1); #ifdef SX126X_DIO3_TCXO_VOLTAGE @@ -154,13 +311,6 @@ bool radio_init() { return true; // success } -uint16_t getBattPercent() { - //Read the PMU fuel guage for battery % - uint16_t battPercent = PMU.getBatteryPercent(); - - return battPercent; -} - uint32_t radio_get_rng_seed() { return radio.random(0x7FFFFFFF); } @@ -176,6 +326,83 @@ void radio_set_tx_power(uint8_t dbm) { radio.setOutputPower(dbm); } +void TbeamSupSensorManager::start_gps() +{ + gps_active = true; + pinMode(P_GPS_WAKE, OUTPUT); + digitalWrite(P_GPS_WAKE, HIGH); +} + +void TbeamSupSensorManager::sleep_gps() { + gps_active = false; + pinMode(P_GPS_WAKE, OUTPUT); + digitalWrite(P_GPS_WAKE, LOW); +} + +bool TbeamSupSensorManager::begin() { + // init GPS port + + Serial1.begin(GPS_BAUD_RATE, SERIAL_8N1, P_GPS_RX, P_GPS_TX); + + bool result = false; + for ( int i = 0; i < 3; ++i) { + result = l76kProbe(); + if (result) { + gps_active = true; + return result; + } + } + return result; +} + +bool TbeamSupSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void TbeamSupSensorManager::loop() { + static long next_gps_update = 0; + + _nmea->loop(); + + if (millis() > next_gps_update) { + if (_nmea->isValid()) { + node_lat = ((double)_nmea->getLatitude())/1000000.; + node_lon = ((double)_nmea->getLongitude())/1000000.; + node_altitude = ((double)_nmea->getAltitude()) / 1000.0; + //Serial.printf("lat %f lon %f\r\n", _lat, _lon); + } + next_gps_update = millis() + 1000; + } +} + +int TbeamSupSensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char* TbeamSupSensorManager::getSettingName(int i) const { + return i == 0 ? "gps" : NULL; +} + +const char* TbeamSupSensorManager::getSettingValue(int i) const { + if (i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} + +bool TbeamSupSensorManager::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + if (strcmp(value, "0") == 0) { + sleep_gps(); + } else { + start_gps(); + } + return true; + } + return false; // not supported +} + mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity diff --git a/variants/lilygo_tbeam_supreme_SX1262/target.h b/variants/lilygo_tbeam_supreme_SX1262/target.h index 2ee3cb01..107e2950 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/target.h +++ b/variants/lilygo_tbeam_supreme_SX1262/target.h @@ -1,15 +1,54 @@ #pragma once +#define RADIOLIB_STATIC_ONLY 1 #include #include #include #include #include +#include +#include + +class TbeamSupSensorManager: public SensorManager { + bool gps_active = false; + LocationProvider * _nmea; + + void start_gps(); + void sleep_gps(); + public: + TbeamSupSensorManager(LocationProvider &nmea): _nmea(&nmea) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; + }; extern TBeamS3SupremeBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern TbeamSupSensorManager sensors; +enum { + POWERMANAGE_ONLINE = _BV(0), + DISPLAY_ONLINE = _BV(1), + RADIO_ONLINE = _BV(2), + GPS_ONLINE = _BV(3), + PSRAM_ONLINE = _BV(4), + SDCARD_ONLINE = _BV(5), + AXDL345_ONLINE = _BV(6), + BME280_ONLINE = _BV(7), + BMP280_ONLINE = _BV(8), + BME680_ONLINE = _BV(9), + QMC6310_ONLINE = _BV(10), + QMI8658_ONLINE = _BV(11), + PCF8563_ONLINE = _BV(12), + OSC32768_ONLINE = _BV(13), +}; + +bool power_init(); bool radio_init(); uint32_t radio_get_rng_seed(); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 078efc9c..5591a400 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -19,10 +19,11 @@ build_flags = -D P_LORA_TX_LED=2 ; LED pin for TX indication -D PIN_VBAT_READ=35 ; Battery voltage reading (analog pin) -D PIN_USER_BTN=0 - -D RADIO_CLASS=CustomSX1276 -D ARDUINO_LOOP_STACK_SIZE=16384 -D DISPLAY_CLASS=SSD1306Display + -D RADIO_CLASS=CustomSX1276 -D WRAPPER_CLASS=CustomSX1276Wrapper + -D SX127X_CURRENT_LIMIT=120 -D LORA_TX_POWER=20 build_src_filter = ${esp32_base.build_src_filter} +<../variants/lilygo_tlora_v2_1> @@ -42,6 +43,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 ; -D CORE_DEBUG_LEVEL=3 @@ -88,6 +90,7 @@ build_flags = -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 diff --git a/variants/lilygo_tlora_v2_1/target.cpp b/variants/lilygo_tlora_v2_1/target.cpp index 740e0561..4d513ed9 100644 --- a/variants/lilygo_tlora_v2_1/target.cpp +++ b/variants/lilygo_tlora_v2_1/target.cpp @@ -10,6 +10,11 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 @@ -18,7 +23,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); - + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8); if (status != RADIOLIB_ERR_NONE) { @@ -27,8 +32,12 @@ bool radio_init() { return false; // fail } +#ifdef SX127X_CURRENT_LIMIT + radio.setCurrentLimit(SX127X_CURRENT_LIMIT); +#endif + radio.setCRC(1); - + return true; // success } diff --git a/variants/lilygo_tlora_v2_1/target.h b/variants/lilygo_tlora_v2_1/target.h index 5c0910cb..edf28eb4 100644 --- a/variants/lilygo_tlora_v2_1/target.h +++ b/variants/lilygo_tlora_v2_1/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern LilyGoTLoraBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/nano_g2_ultra/nano-g2.cpp b/variants/nano_g2_ultra/nano-g2.cpp new file mode 100644 index 00000000..8088ddd0 --- /dev/null +++ b/variants/nano_g2_ultra/nano-g2.cpp @@ -0,0 +1,102 @@ +#include +#include "nano-g2.h" + +#ifdef NANO_G2_ULTRA + +#include +#include + +static BLEDfu bledfu; + +static void connect_callback(uint16_t conn_handle) +{ + (void)conn_handle; + MESH_DEBUG_PRINTLN("BLE client connected"); +} + +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) +{ + (void)conn_handle; + (void)reason; + + MESH_DEBUG_PRINTLN("BLE client disconnected"); +} + +void NanoG2Ultra::begin() +{ + // for future use, sub-classes SHOULD call this from their begin() + startup_reason = BD_STARTUP_NORMAL; + + // set user button + pinMode(PIN_BUTTON1, INPUT); + + // the external notification circuit is shared for both buzzer and led + pinMode(EXT_NOTIFY_OUT, OUTPUT); + digitalWrite(EXT_NOTIFY_OUT, LOW); + + Wire.begin(); + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + + delay(10); +} + +uint16_t NanoG2Ultra::getBattMilliVolts() +{ + int adcvalue = 0; + + analogReference(AR_INTERNAL_3_0); + analogReadResolution(12); + delay(10); + + // ADC range is 0..3000mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); +} + +bool NanoG2Ultra::startOTAUpdate(const char *id, char reply[]) +{ + // Config the peripheral connection with maximum bandwidth + // more SRAM required by SoftDevice + // Note: All config***() function must be called before begin() + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); + + Bluefruit.begin(1, 0); + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 + Bluefruit.setTxPower(4); + // Set the BLE device name + Bluefruit.setName("TECHO_OTA"); + + Bluefruit.Periph.setConnectCallback(connect_callback); + Bluefruit.Periph.setDisconnectCallback(disconnect_callback); + + // To be consistent OTA DFU should be added first if it exists + bledfu.begin(); + + // Set up and start advertising + // Advertising packet + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addName(); + + /* Start Advertising + - Enable auto advertising if disconnected + - Interval: fast mode = 20 ms, slow mode = 152.5 ms + - Timeout for fast mode is 30 seconds + - Start(timeout) with timeout = 0 will advertise forever (until connected) + + For recommended advertising interval + https://developer.apple.com/library/content/qa/qa1931/_index.html + */ + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms + Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode + Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds + + strcpy(reply, "OK - started"); + return true; +} +#endif diff --git a/variants/nano_g2_ultra/nano-g2.h b/variants/nano_g2_ultra/nano-g2.h new file mode 100644 index 00000000..99dc75fa --- /dev/null +++ b/variants/nano_g2_ultra/nano-g2.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +// LoRa radio module pins +#define P_LORA_DIO_1 (32 + 10) +#define P_LORA_NSS (32 + 13) +#define P_LORA_RESET (32 + 15) +#define P_LORA_BUSY (32 + 11) +#define P_LORA_SCLK (0 + 12) +#define P_LORA_MISO (32 + 9) +#define P_LORA_MOSI (0 + 11) + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define SX126X_POWER_EN 37 + +// buttons +#define PIN_BUTTON1 (32 + 6) +#define BUTTON_PIN PIN_BUTTON1 +#define PIN_USER_BTN BUTTON_PIN + +// built-ins +#define VBAT_MV_PER_LSB (0.73242188F) // 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 + +#define VBAT_DIVIDER (0.5F) // 150K + 150K voltage divider on VBAT, actually 100K + 100K +#define VBAT_DIVIDER_COMP (2.0F) // Compensation factor for the VBAT divider + +#define PIN_VBAT_READ (0 + 2) +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) + +class NanoG2Ultra : public mesh::MainBoard +{ +protected: + uint8_t startup_reason; + +public: + void begin(); + uint16_t getBattMilliVolts() override; + bool startOTAUpdate(const char *id, char reply[]) override; + + uint8_t getStartupReason() const override + { + return startup_reason; + } + + const char *getManufacturerName() const override + { + return "Nano G2 Ultra"; + } + + void reboot() override + { + NVIC_SystemReset(); + } +}; diff --git a/variants/nano_g2_ultra/platformio.ini b/variants/nano_g2_ultra/platformio.ini new file mode 100644 index 00000000..98feb35c --- /dev/null +++ b/variants/nano_g2_ultra/platformio.ini @@ -0,0 +1,59 @@ +[nrf52840_g2_ultra] +extends = nrf52_base +platform_packages = framework-arduinoadafruitnrf52 +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 +lib_deps = + ${nrf52_base.lib_deps} + rweather/Crypto @ ^0.4.0 + lewisxhe/PCF8563_Library@^1.0.1 + +[Nano_G2_Ultra] +extends = nrf52840_g2_ultra +board = nano-g2-ultra +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52840_g2_ultra.build_flags} + -I variants/nano_g2_ultra + -D NANO_G2_ULTRA + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_USER_BTN=38 +build_src_filter = ${nrf52840_g2_ultra.build_src_filter} + + + +<../variants/nano_g2_ultra> +debug_tool = jlink +upload_protocol = nrfutil + +[env:Nano_G2_Ultra_companion_radio_ble] +extends = Nano_G2_Ultra +build_flags = + ${Nano_G2_Ultra.build_flags} + -I src/helpers/ui + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=0 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SH1106Display + -D PIN_BUZZER=4 +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Nano_G2_Ultra.build_src_filter} + + + + + + + +<../examples/companion_radio> +lib_deps = + ${Nano_G2_Ultra.lib_deps} + densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SH110X @ ~2.1.13 + adafruit/Adafruit GFX Library @ ^1.12.1 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 diff --git a/variants/nano_g2_ultra/target.cpp b/variants/nano_g2_ultra/target.cpp new file mode 100644 index 00000000..33824f62 --- /dev/null +++ b/variants/nano_g2_ultra/target.cpp @@ -0,0 +1,183 @@ +#include +#include "target.h" +#include +#include + +NanoG2Ultra board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +NanoG2UltraSensorManager sensors = NanoG2UltraSensorManager(nmea); + +#ifdef DISPLAY_CLASS +DISPLAY_CLASS display; +#endif + +#ifndef LORA_CR +#define LORA_CR 5 +#endif + +bool radio_init() +{ + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) + { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() +{ + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) +{ + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) +{ + radio.setOutputPower(dbm); +} + +void NanoG2UltraSensorManager::start_gps() +{ + if (!gps_active) + { + MESH_DEBUG_PRINTLN("starting GPS"); + digitalWrite(PIN_GPS_STANDBY, HIGH); + gps_active = true; + } +} + +void NanoG2UltraSensorManager::stop_gps() +{ + if (gps_active) + { + MESH_DEBUG_PRINTLN("stopping GPS"); + digitalWrite(PIN_GPS_STANDBY, LOW); + gps_active = false; + } +} + +bool NanoG2UltraSensorManager::begin() +{ + Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); // be sure to tx into rx and rx into tx + Serial1.begin(115200); + + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); // Wake GPS from standby + delay(500); + + // We'll consider GPS detected if we see any data on Serial1 + if (Serial1.available() > 0) + { + MESH_DEBUG_PRINTLN("GPS detected"); + } + else + { + MESH_DEBUG_PRINTLN("No GPS detected"); + } + digitalWrite(GPS_EN, LOW); // Put GPS back into standby mode + return true; +} + +bool NanoG2UltraSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) +{ + if (requester_permissions & TELEM_PERM_LOCATION) + { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void NanoG2UltraSensorManager::loop() +{ + static long next_gps_update = 0; + _location->loop(); + if (millis() > next_gps_update && gps_active) // don't bother if gps position is not enabled + { + if (_location->isValid()) + { + node_lat = ((double)_location->getLatitude()) / 1000000.; + node_lon = ((double)_location->getLongitude()) / 1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + } + next_gps_update = millis() + (1000 * 60); // after initial update, only check every minute TODO: should be configurable + } +} + +int NanoG2UltraSensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char *NanoG2UltraSensorManager::getSettingName(int i) const +{ + return i == 0 ? "gps" : NULL; +} + +const char *NanoG2UltraSensorManager::getSettingValue(int i) const +{ + if (i == 0) + { + return gps_active ? "1" : "0"; + } + return NULL; +} + +bool NanoG2UltraSensorManager::setSettingValue(const char *name, const char *value) +{ + if (strcmp(name, "gps") == 0) + { + if (strcmp(value, "0") == 0) + { + stop_gps(); + } + else + { + start_gps(); + } + return true; + } + return false; // not supported +} + +mesh::LocalIdentity radio_new_identity() +{ + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/nano_g2_ultra/target.h b/variants/nano_g2_ultra/target.h new file mode 100644 index 00000000..e0d891e9 --- /dev/null +++ b/variants/nano_g2_ultra/target.h @@ -0,0 +1,47 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include "nano-g2.h" +#include +#include +#include +#include +#ifdef DISPLAY_CLASS +#include +#endif +#include + +class NanoG2UltraSensorManager : public SensorManager +{ + bool gps_active = false; + LocationProvider *_location; + + void start_gps(); + void stop_gps(); + +public: + NanoG2UltraSensorManager(LocationProvider &location) : _location(&location) {} + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP &telemetry) override; + void loop() override; + int getNumSettings() const override; + const char *getSettingName(int i) const override; + const char *getSettingValue(int i) const override; + bool setSettingValue(const char *name, const char *value) override; +}; + +extern NanoG2Ultra board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern NanoG2UltraSensorManager sensors; + +#ifdef DISPLAY_CLASS +extern DISPLAY_CLASS display; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/nano_g2_ultra/variant.cpp b/variants/nano_g2_ultra/variant.cpp new file mode 100644 index 00000000..4ad554cd --- /dev/null +++ b/variants/nano_g2_ultra/variant.cpp @@ -0,0 +1,36 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // Nothing need to be inited for now +} \ No newline at end of file diff --git a/variants/nano_g2_ultra/variant.h b/variants/nano_g2_ultra/variant.h new file mode 100644 index 00000000..b8a53fcf --- /dev/null +++ b/variants/nano_g2_ultra/variant.h @@ -0,0 +1,193 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_Nano_G2_ +#define _VARIANT_Nano_G2_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// #define USE_LFRC // Board uses 32khz RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (-1) +#define PIN_LED2 (-1) +#define PIN_LED3 (-1) + +#define LED_RED PIN_LED3 +#define LED_BLUE PIN_LED1 +#define LED_GREEN PIN_LED2 + +#define LED_BUILTIN LED_BLUE +#define LED_CONN PIN_GREEN + +#define LED_STATE_ON 0 // State when LED is lit + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 6) + +#define EXT_NOTIFY_OUT (0 + 4) // Default pin to use for Ext Notify Module. + +/* + * Analog pins + */ +#define PIN_A4 (0 + 2) // Battery ADC + +#define BATTERY_PIN PIN_A4 + + static const uint8_t A4 = PIN_A4; + +#define ADC_RESOLUTION 14 + +/* + * Serial interfaces + */ +#define PIN_SERIAL2_RX (0 + 22) +#define PIN_SERIAL2_TX (0 + 20) + +/** + Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 17) +#define PIN_WIRE_SCL (0 + 15) + +#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC + +/* +External serial flash W25Q16JV_IQ +*/ + +// QSPI Pins +#define PIN_QSPI_SCK (0 + 8) +#define PIN_QSPI_CS (32 + 7) +#define PIN_QSPI_IO0 (0 + 6) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (0 + 26) // MISO if using two bit interface +#define PIN_QSPI_IO2 (32 + 4) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (32 + 2) // HOLD if using two bit interface (i.e. not used) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES W25Q16JV_IQ +#define EXTERNAL_FLASH_USE_QSPI + + /* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (32 + 13) // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 (32 + 10) +// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching +// #define SX1262_DIO3 (0 + 21) +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main CPU? +#define SX126X_BUSY (32 + 11) +#define SX126X_RESET (32 + 15) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + + // #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) + + // #undef SX126X_CS + + /* + * GPS pins + */ + +#define HAS_GPS 1 +#define GPS_L76K + +#define PIN_GPS_STANDBY (0 + 13) // An output to wake GPS, low means allow sleep, high means force wake STANDBY +#define PIN_GPS_TX (0 + 9) // This is for bits going TOWARDS the CPU +#define PIN_GPS_RX (0 + 10) // This is for bits going TOWARDS the GPS +#define GPS_RX_PIN PIN_GPS_RX +#define GPS_TX_PIN PIN_GPS_TX + + + // #define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +// For LORA, spi 0 +#define PIN_SPI_MISO (32 + 9) +#define PIN_SPI_MOSI (0 + 11) +#define PIN_SPI_SCK (0 + 12) + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (2) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +#define HAS_RTC 1 + +/** + OLED Screen Model + */ +#define ARDUINO_ARCH_AVR +#define USE_SH1107_128_64 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/picow/platformio.ini b/variants/picow/platformio.ini new file mode 100644 index 00000000..ec27e6ee --- /dev/null +++ b/variants/picow/platformio.ini @@ -0,0 +1,104 @@ +[picow] +extends = rp2040_base +platform = https://github.com/maxgerhardt/platform-raspberrypi.git +board = rpipicow +board_build.core = earlephilhower +board_build.filesystem_size = 0.5m +build_flags = ${rp2040_base.build_flags} + -I variants/picow +; -D PICOW +; -D HW_SPI1_DEVICE + -D SX126X_CURRENT_LIMIT=130 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${rp2040_base.build_src_filter} + + + +<../variants/picow> +lib_deps = ${rp2040_base.lib_deps} + +[env:PicoW_Repeater] +extends = picow +build_flags = ${picow.build_flags} + -D ADVERT_NAME='"PicoW Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${picow.build_src_filter} + +<../examples/simple_repeater> + +[env:PicoW_room_server] +extends = picow +build_flags = ${picow.build_flags} + -D ADVERT_NAME='"Test Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${picow.build_src_filter} + +<../examples/simple_room_server> + +[env:PicoW_companion_radio_usb] +extends = picow +build_flags = ${picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${picow.build_src_filter} + +<../examples/companion_radio> +lib_deps = ${picow.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; [env:PicoW_companion_radio_ble] +; extends = picow +; build_flags = ${picow.build_flags} +; -D MAX_CONTACTS=100 +; -D MAX_GROUP_CHANNELS=8 +; -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; ; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; ; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; ; -D MESH_PACKET_LOGGING=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${picow.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${picow.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +; [env:PicoW_companion_radio_wifi] +; extends = picow +; build_flags = ${picow.build_flags} +; -D MAX_CONTACTS=100 +; -D MAX_GROUP_CHANNELS=8 +; -D WIFI_DEBUG_LOGGING=1 +; -D WIFI_SSID='"myssid"' +; -D WIFI_PWD='"mypwd"' +; ; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; ; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; ; -D MESH_PACKET_LOGGING=1 +; ; -D MESH_DEBUG=1 +; build_src_filter = ${picow.build_src_filter} +; +<../examples/companion_radio> +; lib_deps = ${picow.lib_deps} +; densaugeo/base64 @ ~1.4.0 + +[env:PicoW_terminal_chat] +extends = picow +build_flags = ${picow.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${picow.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${picow.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/picow/target.cpp b/variants/picow/target.cpp new file mode 100644 index 00000000..22d2d854 --- /dev/null +++ b/variants/picow/target.cpp @@ -0,0 +1,74 @@ +#include +#include "target.h" +#include + +PicoWBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI1); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI1.setMISO(P_LORA_MISO); + //SPI1.setCS(P_LORA_NSS); // Setting CS results in freeze + SPI1.setSCK(P_LORA_SCLK); + SPI1.setMOSI(P_LORA_MOSI); + + SPI1.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/picow/target.h b/variants/picow/target.h new file mode 100644 index 00000000..a89b5ba8 --- /dev/null +++ b/variants/picow/target.h @@ -0,0 +1,20 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +extern PicoWBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index f9f74feb..11f73d81 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -7,94 +7,113 @@ build_flags = ${nrf52840_base.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D DISPLAY_CLASS=SSD1306Display -D PIN_BOARD_SCL=7 -D PIN_BOARD_SDA=8 -D PIN_OLED_RESET=-1 -D PIN_USER_BTN=6 + -D PIN_GPS_RX=3 + -D PIN_GPS_TX=4 + -D PIN_GPS_EN=5 + -D ENV_INCLUDE_GPS=1 + -D ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52840_base.build_src_filter} + + + +<../variants/promicro> -lib_deps= - ${nrf52840_base.lib_deps} +lib_deps= ${nrf52840_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 - + adafruit/Adafruit INA3221 Library @ ^1.0.1 + adafruit/Adafruit INA219 @ ^1.2.3 + adafruit/Adafruit AHTX0 @ ^2.0.5 + adafruit/Adafruit BME280 Library @ ^2.3.0 + stevemarple/MicroNMEA @ ^2.0.6 + [env:Faketec_Repeater] extends = Faketec -build_src_filter = ${Faketec.build_src_filter} +<../examples/simple_repeater> + +build_src_filter = ${Faketec.build_src_filter} + +<../examples/simple_repeater> + + build_flags = ${Faketec.build_flags} - -D ADVERT_NAME="\"Faketec Repeater\"" + -D ADVERT_NAME='"Faketec Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD="\"password\"" + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D DISPLAY_CLASS=SSD1306Display ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -lib_deps = - ${Faketec.lib_deps} +lib_deps = ${Faketec.lib_deps} adafruit/RTClib @ ^2.1.3 [env:Faketec_room_server] extends = Faketec -build_src_filter = ${Faketec.build_src_filter} +<../examples/simple_room_server> + -build_flags = - ${Faketec.build_flags} - -D ADVERT_NAME="\"Test Room\"" +build_src_filter = ${Faketec.build_src_filter} + +<../examples/simple_room_server> + + +build_flags = ${Faketec.build_flags} + -D ADVERT_NAME='"Faketec Room"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD="\"password\"" - -D ROOM_PASSWORD="\"hello\"" + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D DISPLAY_CLASS=SSD1306Display ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -lib_deps = - ${Faketec.lib_deps} +lib_deps = ${Faketec.lib_deps} adafruit/RTClib @ ^2.1.3 [env:Faketec_terminal_chat] extends = Faketec -build_flags = - ${Faketec.build_flags} +build_flags = ${Faketec.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${Faketec.build_src_filter} +<../examples/simple_secure_chat/main.cpp> -lib_deps = - ${Faketec.lib_deps} +build_src_filter = ${Faketec.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${Faketec.lib_deps} densaugeo/base64 @ ~1.4.0 adafruit/RTClib @ ^2.1.3 [env:Faketec_companion_radio_usb] extends = Faketec -build_flags = - ${Faketec.build_flags} +build_flags = ${Faketec.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D DISPLAY_CLASS=SSD1306Display ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -build_src_filter = ${Faketec.build_src_filter} +<../examples/companion_radio> +<../examples/companion_radio> + -lib_deps = - ${Faketec.lib_deps} +build_src_filter = ${Faketec.build_src_filter} + +<../examples/companion_radio> + + +lib_deps = ${Faketec.lib_deps} adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 [env:Faketec_companion_radio_ble] extends = Faketec -build_flags = - ${Faketec.build_flags} +build_flags = ${Faketec.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D ENABLE_PRIVATE_KEY_EXPORT=1 -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=SSD1306Display ; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${Faketec.build_src_filter} + +<../examples/companion_radio> + -lib_deps = - ${Faketec.lib_deps} + -D MESH_DEBUG=1 +build_src_filter = ${Faketec.build_src_filter} + + + +<../examples/companion_radio> + + +lib_deps = ${Faketec.lib_deps} adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 @@ -107,81 +126,86 @@ build_flags = ${nrf52840_base.build_flags} -D RADIO_CLASS=CustomLLCC68 -D WRAPPER_CLASS=CustomLLCC68Wrapper -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -build_src_filter = ${nrf52840_base.build_src_filter} +build_src_filter = + ${nrf52840_base.build_src_filter} + + + +<../variants/promicro> +lib_deps= ${nrf52840_base.lib_deps} + adafruit/Adafruit INA3221 Library @ ^1.0.1 + adafruit/Adafruit INA219 @ ^1.2.3 + adafruit/Adafruit AHTX0 @ ^2.0.5 + adafruit/Adafruit BME280 Library @ ^2.3.0 [env:ProMicroLLCC68_Repeater] extends = ProMicroLLCC68 -build_src_filter = ${ProMicroLLCC68.build_src_filter} +<../examples/simple_repeater/main.cpp> -build_flags = - ${ProMicroLLCC68.build_flags} - -D ADVERT_NAME="\"ProMicroLLCC68 Repeater\"" - -D ADMIN_PASSWORD="\"password\"" +build_src_filter = ${ProMicroLLCC68.build_src_filter} + +<../examples/simple_repeater/main.cpp> +build_flags = ${ProMicroLLCC68.build_flags} + -D ADVERT_NAME='"ProMicroLLCC68 Repeater"' + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -lib_deps = - ${ProMicroLLCC68.lib_deps} +lib_deps = ${ProMicroLLCC68.lib_deps} adafruit/RTClib @ ^2.1.3 [env:ProMicroLLCC68_room_server] extends = ProMicroLLCC68 -build_src_filter = ${ProMicroLLCC68.build_src_filter} +<../examples/simple_room_server/main.cpp> -build_flags = - ${ProMicroLLCC68.build_flags} - -D ADVERT_NAME="\"ProMicroLLCC68 Room\"" - -D ADMIN_PASSWORD="\"password\"" - -D ROOM_PASSWORD="\"hello\"" +build_src_filter = ${ProMicroLLCC68.build_src_filter} + +<../examples/simple_room_server/main.cpp> +build_flags = ${ProMicroLLCC68.build_flags} + -D ADVERT_NAME='"ProMicroLLCC68 Room"' + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -lib_deps = - ${ProMicroLLCC68.lib_deps} +lib_deps = ${ProMicroLLCC68.lib_deps} adafruit/RTClib @ ^2.1.3 [env:ProMicroLLCC68_terminal_chat] extends = ProMicroLLCC68 -build_flags = - ${ProMicroLLCC68.build_flags} +build_flags = ${ProMicroLLCC68.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${ProMicroLLCC68.build_src_filter} +<../examples/simple_secure_chat/main.cpp> -lib_deps = - ${ProMicroLLCC68.lib_deps} +build_src_filter = ${ProMicroLLCC68.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = ${ProMicroLLCC68.lib_deps} densaugeo/base64 @ ~1.4.0 adafruit/RTClib @ ^2.1.3 [env:ProMicroLLCC68_companion_radio_usb] extends = ProMicroLLCC68 -build_flags = - ${ProMicroLLCC68.build_flags} +build_flags = ${ProMicroLLCC68.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -build_src_filter = ${ProMicroLLCC68.build_src_filter} +<../examples/companion_radio/main.cpp> -lib_deps = - ${ProMicroLLCC68.lib_deps} +build_src_filter = ${ProMicroLLCC68.build_src_filter} + +<../examples/companion_radio/main.cpp> +lib_deps = ${ProMicroLLCC68.lib_deps} adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 [env:ProMicroLLCC68_companion_radio_ble] extends = ProMicroLLCC68 -build_flags = - ${ProMicroLLCC68.build_flags} +build_flags = ${ProMicroLLCC68.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D ENABLE_PRIVATE_KEY_EXPORT=1 -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${ProMicroLLCC68.build_src_filter} + +<../examples/companion_radio/main.cpp> -lib_deps = - ${ProMicroLLCC68.lib_deps} +build_src_filter = ${ProMicroLLCC68.build_src_filter} + + + +<../examples/companion_radio/main.cpp> +lib_deps = ${ProMicroLLCC68.lib_deps} adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 diff --git a/variants/promicro/target.cpp b/variants/promicro/target.cpp index a3ea1879..2369bd7a 100644 --- a/variants/promicro/target.cpp +++ b/variants/promicro/target.cpp @@ -2,6 +2,9 @@ #include "target.h" #include +#if ENV_INCLUDE_GPS +#endif + PromicroBoard board; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); @@ -10,6 +13,17 @@ WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 @@ -73,3 +87,4 @@ mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity } + diff --git a/variants/promicro/target.h b/variants/promicro/target.h index 79d0862f..c634d18a 100644 --- a/variants/promicro/target.h +++ b/variants/promicro/target.h @@ -7,13 +7,24 @@ #include #include #include +#ifdef DISPLAY_CLASS + #include +#endif + +#include extern PromicroBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_tx_power(uint8_t dbm); mesh::LocalIdentity radio_new_identity(); + diff --git a/variants/rak3x72/platformio.ini b/variants/rak3x72/platformio.ini new file mode 100644 index 00000000..94e8f595 --- /dev/null +++ b/variants/rak3x72/platformio.ini @@ -0,0 +1,33 @@ +[rak3x72] +extends = stm32_base +board = rak3172 +board_upload.maximum_size = 229376 ; 32kb for FS +build_flags = ${stm32_base.build_flags} + -D RADIO_CLASS=CustomSTM32WLx + -D WRAPPER_CLASS=CustomSTM32WLxWrapper + -D SPI_INTERFACES_COUNT=0 + -D RX_BOOSTED_GAIN=true + -I variants/rak3x72 +build_src_filter = ${stm32_base.build_src_filter} + +<../variants/rak3x72> + +[env:rak3x72-repeater] +extends = rak3x72 +build_flags = ${rak3x72.build_flags} + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"RAK3x72 Repeater"' + -D ADMIN_PASSWORD='"password"' +build_src_filter = ${rak3x72.build_src_filter} + +<../examples/simple_repeater/main.cpp> + +[env:rak3x72_companion_radio_usb] +extends = rak3x72 +build_flags = ${rak3x72.build_flags} +; -D FORMAT_FS=true + -D LORA_TX_POWER=22 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +build_src_filter = ${rak3x72.build_src_filter} + +<../examples/companion_radio/*.cpp> +lib_deps = ${rak3x72.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/rak3x72/target.cpp b/variants/rak3x72/target.cpp new file mode 100644 index 00000000..4cb5929a --- /dev/null +++ b/variants/rak3x72/target.cpp @@ -0,0 +1,67 @@ +#include +#include "target.h" +#include + +RAK3x72Board board; + +RADIO_CLASS radio = new STM32WLx_Module(); + +WRAPPER_CLASS radio_driver(radio, board); + +static const uint32_t rfswitch_pins[] = {LORAWAN_RFSWITCH_PINS, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; +static const Module::RfSwitchMode_t rfswitch_table[] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, + {STM32WLx::MODE_RX, {HIGH, LOW}}, + {STM32WLx::MODE_TX_LP, {LOW, HIGH}}, + {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, + END_OF_MODE_TABLE, +}; + +VolatileRTCClock rtc_clock; +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { +// rtc_clock.begin(Wire); + + radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); + + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 0, 0); // TCXO set to 0 for RAK3172 + + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + #ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); + #endif + + radio.setCRC(1); + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/rak3x72/target.h b/variants/rak3x72/target.h new file mode 100644 index 00000000..c39b69b1 --- /dev/null +++ b/variants/rak3x72/target.h @@ -0,0 +1,35 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +#define PIN_VBAT_READ A0 +#define ADC_MULTIPLIER (5 * 1.73 * 1000) + +class RAK3x72Board : public STM32Board { +public: + const char* getManufacturerName() const override { + return "RAK 3x72"; + } + + uint16_t getBattMilliVolts() override { + uint32_t raw = analogRead(PIN_VBAT_READ); + return (ADC_MULTIPLIER * raw) / 1024; + } +}; + +extern RAK3x72Board board; +extern WRAPPER_CLASS radio_driver; +extern VolatileRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/rak3x72/variant.h b/variants/rak3x72/variant.h new file mode 100644 index 00000000..4405be0b --- /dev/null +++ b/variants/rak3x72/variant.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +#undef RNG diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 3f40447f..c2845fd4 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -7,13 +7,14 @@ build_flags = ${nrf52840_base.build_flags} -I variants/rak4631 -D RAK_4631 -D PIN_USER_BTN=9 + -D PIN_USER_BTN_ANA=31 -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52840_base.build_src_filter} + @@ -31,6 +32,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} @@ -80,6 +82,7 @@ build_flags = -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 @@ -98,8 +101,8 @@ build_flags = ${rak4631.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_secure_chat/main.cpp> lib_deps = diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index 17e76532..9b23af83 100644 --- a/variants/rak4631/target.cpp +++ b/variants/rak4631/target.cpp @@ -10,6 +10,11 @@ WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/rak4631/target.h b/variants/rak4631/target.h index cd32c12d..a50d6f2c 100644 --- a/variants/rak4631/target.h +++ b/variants/rak4631/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern RAK4631Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 49e97b4b..d3f45eb0 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -14,7 +14,7 @@ build_flags = -D PIN_USER_BTN=0 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 - -D SX126X_CURRENT_LIMIT=130.0f ; for best TX power! + -D SX126X_CURRENT_LIMIT=140 ; -D SX126X_RX_BOOSTED_GAIN=1 - DO NOT ENABLE THIS! ; https://wiki.uniteng.com/en/meshtastic/station-g2#impact-of-lora-node-dense-areashigh-noise-environments-on-rf-performance build_src_filter = ${esp32_base.build_src_filter} @@ -30,7 +30,8 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' - -D MESH_PACKET_LOGGING=1 + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} +<../examples/simple_repeater> diff --git a/variants/station_g2/target.cpp b/variants/station_g2/target.cpp index 988ac4fc..e279b139 100644 --- a/variants/station_g2/target.cpp +++ b/variants/station_g2/target.cpp @@ -14,6 +14,7 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/station_g2/target.h b/variants/station_g2/target.h index 96fd5095..e44c2ebe 100644 --- a/variants/station_g2/target.h +++ b/variants/station_g2/target.h @@ -6,10 +6,12 @@ #include #include #include +#include extern StationG2Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/t1000-e/NullDisplayDriver.h b/variants/t1000-e/NullDisplayDriver.h new file mode 100644 index 00000000..2a9670bd --- /dev/null +++ b/variants/t1000-e/NullDisplayDriver.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +class NullDisplayDriver : public DisplayDriver { +public: + NullDisplayDriver() : DisplayDriver(128, 64) { } + bool begin() { return false; } // not present + + bool isOn() override { return false; } + void turnOn() override { } + void turnOff() override { } + void clear() override { } + void startFrame(Color bkg = DARK) override { } + void setTextSize(int sz) override { } + void setColor(Color c) override { } + void setCursor(int x, int y) override { } + void print(const char* str) override { } + void fillRect(int x, int y, int w, int h) override { } + void drawRect(int x, int y, int w, int h) override { } + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } + uint16_t getTextWidth(const char* str) override { return 0; } + void endFrame() { } +}; diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index e5c0d11c..00974208 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -40,14 +40,20 @@ build_flags = ${t1000-e.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 + -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE - -D HAS_UI + -D DISPLAY_CLASS=NullDisplayDriver + -D PIN_BUZZER=25 + -D PIN_BUZZER_EN=37 ; P1/5 - required for T1000-E build_src_filter = ${t1000-e.build_src_filter} + + + +<../examples/companion_radio/*.cpp> lib_deps = ${t1000-e.lib_deps} densaugeo/base64 @ ~1.4.0 + stevemarple/MicroNMEA @ ^2.0.6 + end2endzone/NonBlockingRTTTL@^1.3.0 \ No newline at end of file diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index cf51cc91..be82ca76 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -1,5 +1,6 @@ #include #include "target.h" +#include T1000eBoard board; @@ -8,6 +9,12 @@ RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BU WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock rtc_clock; +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +T1000SensorManager sensors = T1000SensorManager(nmea); + +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif #ifndef LORA_CR #define LORA_CR 5 @@ -84,3 +91,113 @@ mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity } + +void T1000SensorManager::start_gps() { + gps_active = true; + //_nmea->begin(); + // this init sequence should be better + // comes from seeed examples and deals with all gps pins + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); + delay(10); + pinMode(GPS_VRTC_EN, OUTPUT); + digitalWrite(GPS_VRTC_EN, HIGH); + delay(10); + + pinMode(GPS_RESET, OUTPUT); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); + + pinMode(GPS_SLEEP_INT, OUTPUT); + digitalWrite(GPS_SLEEP_INT, HIGH); + pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB, INPUT_PULLUP); +} + +void T1000SensorManager::sleep_gps() { + gps_active = false; + digitalWrite(GPS_VRTC_EN, HIGH); + digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_RESET, HIGH); + digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB, OUTPUT); + digitalWrite(GPS_RESETB, LOW); + //_nmea->stop(); +} + +void T1000SensorManager::stop_gps() { + gps_active = false; + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_EN, LOW); + digitalWrite(GPS_RESET, HIGH); + digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB, OUTPUT); + digitalWrite(GPS_RESETB, LOW); + //_nmea->stop(); +} + + +bool T1000SensorManager::begin() { + // init GPS + Serial1.begin(115200); + + // make sure gps pin are off + digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_RESET, LOW); + digitalWrite(GPS_SLEEP_INT, LOW); + digitalWrite(GPS_RTC_INT, LOW); + pinMode(GPS_RESETB, OUTPUT); + digitalWrite(GPS_RESETB, LOW); + + return true; +} + +bool T1000SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void T1000SensorManager::loop() { + static long next_gps_update = 0; + + _nmea->loop(); + + if (millis() > next_gps_update) { + if (_nmea->isValid()) { + node_lat = ((double)_nmea->getLatitude())/1000000.; + node_lon = ((double)_nmea->getLongitude())/1000000.; + node_altitude = ((double)_nmea->getAltitude()) / 1000.0; + //Serial.printf("lat %f lon %f\r\n", _lat, _lon); + } + next_gps_update = millis() + 1000; + } +} + +int T1000SensorManager::getNumSettings() const { return 1; } // just one supported: "gps" (power switch) + +const char* T1000SensorManager::getSettingName(int i) const { + return i == 0 ? "gps" : NULL; +} +const char* T1000SensorManager::getSettingValue(int i) const { + if (i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} +bool T1000SensorManager::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + if (strcmp(value, "0") == 0) { + sleep_gps(); // sleep for faster fix ! + } else { + start_gps(); + } + return true; + } + return false; // not supported +} diff --git a/variants/t1000-e/target.h b/variants/t1000-e/target.h index 83bd3eae..1b569481 100644 --- a/variants/t1000-e/target.h +++ b/variants/t1000-e/target.h @@ -6,10 +6,38 @@ #include #include #include +#include +#include +#ifdef DISPLAY_CLASS + #include "NullDisplayDriver.h" +#endif + +class T1000SensorManager: public SensorManager { + bool gps_active = false; + LocationProvider * _nmea; + + void start_gps(); + void sleep_gps(); + void stop_gps(); +public: + T1000SensorManager(LocationProvider &nmea): _nmea(&nmea) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; +}; + +#ifdef DISPLAY_CLASS + extern NullDisplayDriver display; +#endif extern T1000eBoard board; extern WRAPPER_CLASS radio_driver; extern VolatileRTCClock rtc_clock; +extern T1000SensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/t114/platformio.ini b/variants/t114/platformio.ini index 2abdfc69..9e72bbd6 100644 --- a/variants/t114/platformio.ini +++ b/variants/t114/platformio.ini @@ -20,12 +20,15 @@ build_flags = ${nrf52840_t114.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52840_t114.build_src_filter} + + +<../variants/t114> +lib_deps = + ${nrf52840_t114.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 debug_tool = jlink upload_protocol = nrfutil @@ -39,6 +42,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -60,32 +64,30 @@ build_flags = extends = Heltec_t114 build_flags = ${Heltec_t114.build_flags} + -I src/helpers/ui + -D ST7789 + -D DISPLAY_CLASS=ST7789Display -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t114.build_src_filter} - + + + + + +<../examples/companion_radio/main.cpp> -lib_deps = - ${Heltec_t114.lib_deps} - densaugeo/base64 @ ~1.4.0 - -[env:Heltec_t114_companion_radio_ble_screen] -extends = env:Heltec_t114_companion_radio_ble -build_flags = ${env:Heltec_t114_companion_radio_ble.build_flags} - -I src/helpers/ui - -D ST7789 - -D DISPLAY_CLASS=ST7789Display -build_src_filter = ${env:Heltec_t114_companion_radio_ble.build_src_filter} +<../examples/companion_radio/UITask.cpp> + -lib_deps = ${env:Heltec_t114_companion_radio_ble.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + + + + +lib_deps = + adafruit/Adafruit GFX Library @ ^1.12.1 + ${Heltec_t114.lib_deps} + densaugeo/base64 @ ~1.4.0 [env:Heltec_t114_companion_radio_usb] extends = Heltec_t114 diff --git a/variants/t114/target.cpp b/variants/t114/target.cpp index 07a60688..a57a55e2 100644 --- a/variants/t114/target.cpp +++ b/variants/t114/target.cpp @@ -1,6 +1,7 @@ #include #include "target.h" #include +#include T114Board board; @@ -10,6 +11,12 @@ WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +T114SensorManager sensors = T114SensorManager(nmea); + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 @@ -67,3 +74,91 @@ mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng); // create new random identity } + +void T114SensorManager::start_gps() { + if (!gps_active) { + gps_active = true; + _location->begin(); + } +} + +void T114SensorManager::stop_gps() { + if (gps_active) { + gps_active = false; + _location->stop(); + } +} + +bool T114SensorManager::begin() { + Serial1.begin(9600); + + // Try to detect if GPS is physically connected to determine if we should expose the setting + pinMode(GPS_EN, OUTPUT); + digitalWrite(GPS_EN, HIGH); // Power on GPS + + // Give GPS a moment to power up and send data + delay(1500); + + // We'll consider GPS detected if we see any data on Serial1 + gps_detected = (Serial1.available() > 0); + + if (gps_detected) { + MESH_DEBUG_PRINTLN("GPS detected"); + digitalWrite(GPS_EN, LOW); // Power off GPS until the setting is changed + } else { + MESH_DEBUG_PRINTLN("No GPS detected"); + digitalWrite(GPS_EN, LOW); + } + + return true; +} + +bool T114SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { + if (requester_permissions & TELEM_PERM_LOCATION) { // does requester have permission? + telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); + } + return true; +} + +void T114SensorManager::loop() { + static long next_gps_update = 0; + + _location->loop(); + + if (millis() > next_gps_update) { + if (_location->isValid()) { + node_lat = ((double)_location->getLatitude())/1000000.; + node_lon = ((double)_location->getLongitude())/1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + } + next_gps_update = millis() + 1000; + } +} + +int T114SensorManager::getNumSettings() const { + return gps_detected ? 1 : 0; // only show GPS setting if GPS is detected +} + +const char* T114SensorManager::getSettingName(int i) const { + return (gps_detected && i == 0) ? "gps" : NULL; +} + +const char* T114SensorManager::getSettingValue(int i) const { + if (gps_detected && i == 0) { + return gps_active ? "1" : "0"; + } + return NULL; +} + +bool T114SensorManager::setSettingValue(const char* name, const char* value) { + if (gps_detected && strcmp(name, "gps") == 0) { + if (strcmp(value, "0") == 0) { + stop_gps(); + } else { + start_gps(); + } + return true; + } + return false; // not supported +} diff --git a/variants/t114/target.h b/variants/t114/target.h index 8d80c0ec..0f6ebaa1 100644 --- a/variants/t114/target.h +++ b/variants/t114/target.h @@ -6,10 +6,38 @@ #include #include #include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif + +class T114SensorManager : public SensorManager { + bool gps_active = false; + bool gps_detected = false; + LocationProvider* _location; + + void start_gps(); + void stop_gps(); +public: + T114SensorManager(LocationProvider &location): _location(&location) { } + bool begin() override; + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + void loop() override; + int getNumSettings() const override; + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; +}; extern T114Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern T114SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/t114/variant.cpp b/variants/t114/variant.cpp index 4d07d1ae..2bca56a1 100644 --- a/variants/t114/variant.cpp +++ b/variants/t114/variant.cpp @@ -11,4 +11,5 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { + pinMode(PIN_USER_BTN, INPUT); } diff --git a/variants/t114/variant.h b/variants/t114/variant.h index 56fc536d..a0fd2e4f 100644 --- a/variants/t114/variant.h +++ b/variants/t114/variant.h @@ -41,8 +41,8 @@ //////////////////////////////////////////////////////////////////////////////// // UART pin definition -#define PIN_SERIAL1_RX (39) -#define PIN_SERIAL1_TX (37) +#define PIN_SERIAL1_RX (37) +#define PIN_SERIAL1_TX (39) #define PIN_SERIAL2_RX (9) #define PIN_SERIAL2_TX (10) @@ -69,10 +69,10 @@ #define LED_BUILTIN (35) #define PIN_LED LED_BUILTIN #define LED_RED LED_BUILTIN -#define LED_BLUE LED_BUILTIN +#define LED_BLUE (-1) // No blue led, prevents Bluefruit flashing the green LED during advertising #define LED_PIN LED_BUILTIN -#define LED_STATE_ON HIGH +#define LED_STATE_ON LOW #define PIN_NEOPIXEL (14) #define NEOPIXEL_NUM (2) @@ -109,9 +109,15 @@ //////////////////////////////////////////////////////////////////////////////// // Buzzer -#define PIN_BUZZER (46) +// #define PIN_BUZZER (46) +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define GPS_EN (21) +#define GPS_RESET (38) + //////////////////////////////////////////////////////////////////////////////// // TFT #define PIN_TFT_SCL (40) diff --git a/variants/techo/platformio.ini b/variants/techo/platformio.ini index f3c2f973..c4bc8f85 100644 --- a/variants/techo/platformio.ini +++ b/variants/techo/platformio.ini @@ -19,7 +19,7 @@ build_flags = ${nrf52840_techo.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 build_src_filter = ${nrf52840_techo.build_src_filter} + @@ -37,6 +37,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -62,7 +63,7 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D DISPLAY_CLASS=GxEPDDisplay - -D HAS_GxEPD + -D OFFLINE_QUEUE_SIZE=256 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/techo/target.cpp b/variants/techo/target.cpp index ecffc37a..880af612 100644 --- a/variants/techo/target.cpp +++ b/variants/techo/target.cpp @@ -10,6 +10,11 @@ WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/techo/target.h b/variants/techo/target.h index 4a66f24e..15524111 100644 --- a/variants/techo/target.h +++ b/variants/techo/target.h @@ -6,10 +6,20 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif + extern TechoBoard board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini new file mode 100644 index 00000000..9bad98e2 --- /dev/null +++ b/variants/thinknode_m1/platformio.ini @@ -0,0 +1,86 @@ +[nrf52840_thinknode_m1] +extends = nrf52_base +platform_packages = framework-arduinoadafruitnrf52 +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 +lib_deps = + ${nrf52_base.lib_deps} + rweather/Crypto @ ^0.4.0 + +[ThinkNode_M1] +extends = nrf52840_thinknode_m1 +board = thinknode_m1 +board_build.ldscript = boards/nrf52840_s140_v6.ld +build_flags = ${nrf52840_thinknode_m1.build_flags} + -I variants/thinknode_m1 + -D THINKNODE_M1=1 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${nrf52840_thinknode_m1.build_src_filter} + + + + + +<../variants/thinknode_m1> +debug_tool = jlink +upload_protocol = nrfutil + +[env:ThinkNode_M1_repeater] +extends = ThinkNode_M1 +build_flags = + ${ThinkNode_M1.build_flags} + -D ADVERT_NAME='"ThinkNode Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M1.build_src_filter} + +<../examples/simple_repeater/main.cpp> +lib_deps = + ${ThinkNode_M1.lib_deps} + +[env:ThinkNode_M1_room_server] +extends = ThinkNode_M1 +build_flags = + ${ThinkNode_M1.build_flags} + -D ADVERT_NAME='"ThinkNode Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M1.build_src_filter} + +<../examples/simple_room_server/main.cpp> +lib_deps = + ${ThinkNode_M1.lib_deps} + +[env:ThinkNode_M1_companion_radio_ble] +extends = ThinkNode_M1 +build_flags = + ${ThinkNode_M1.build_flags} + -I src/helpers/ui + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D DISPLAY_ROTATION=4 + -D DISPLAY_CLASS=GxEPDDisplay + -D OFFLINE_QUEUE_SIZE=256 +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D ENABLE_PRIVATE_KEY_EXPORT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${ThinkNode_M1.build_src_filter} + + + + + + + +<../examples/companion_radio> +lib_deps = + ${ThinkNode_M1.lib_deps} + densaugeo/base64 @ ~1.4.0 + zinggjm/GxEPD2 @ 1.6.2 diff --git a/variants/thinknode_m1/target.cpp b/variants/thinknode_m1/target.cpp new file mode 100644 index 00000000..5a09eb9a --- /dev/null +++ b/variants/thinknode_m1/target.cpp @@ -0,0 +1,74 @@ +#include +#include "target.h" +#include + +ThinkNodeM1Board board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + radio.setCRC(1); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/thinknode_m1/target.h b/variants/thinknode_m1/target.h new file mode 100644 index 00000000..c958d0e3 --- /dev/null +++ b/variants/thinknode_m1/target.h @@ -0,0 +1,27 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#ifdef DISPLAY_CLASS + #include +#endif + +extern ThinkNodeM1Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/thinknode_m1/variant.cpp b/variants/thinknode_m1/variant.cpp new file mode 100644 index 00000000..155aa42d --- /dev/null +++ b/variants/thinknode_m1/variant.cpp @@ -0,0 +1,34 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const int MISO = PIN_SPI1_MISO; +const int MOSI = PIN_SPI1_MOSI; +const int SCK = PIN_SPI1_SCK; + +const uint32_t g_ADigitalPinMap[] = { + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() { + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + + pinMode(PIN_BUTTON1, INPUT_PULLUP); + pinMode(PIN_BUTTON2, INPUT_PULLUP); + + pinMode(LED_RED, OUTPUT); + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, HIGH); + + pinMode(PIN_TXCO, OUTPUT); + digitalWrite(PIN_TXCO, HIGH); + + // shutdown gps + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, LOW); +} diff --git a/variants/thinknode_m1/variant.h b/variants/thinknode_m1/variant.h new file mode 100644 index 00000000..3f35ace0 --- /dev/null +++ b/variants/thinknode_m1/variant.h @@ -0,0 +1,133 @@ +/* + * variant.h + * Copyright (C) 2023 Seeed K.K. + * MIT License + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal oscillator +#define VARIANT_MCK (64000000ul) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_TXCO (21) +//////////////////////////////////////////////////////////////////////////////// +// Power + +#define PIN_PWR_EN (12) + +#define BATTERY_PIN (4) +#define ADC_MULTIPLIER (4.90F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) + +#define AREF_VOLTAGE (3.0) + +//////////////////////////////////////////////////////////////////////////////// +// Number of pins + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART pin definition + +#define PIN_SERIAL1_RX (41) // GPS TX +#define PIN_SERIAL1_TX (40) // GPS RX +//////////////////////////////////////////////////////////////////////////////// +// I2C pin definition + +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 + +//////////////////////////////////////////////////////////////////////////////// +// SPI pin definition + +#define SPI_INTERFACES_COUNT (2) + +#define PIN_SPI_MISO (23) +#define PIN_SPI_MOSI (22) +#define PIN_SPI_SCK (19) +#define PIN_SPI_NSS (24) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin LEDs + +#define LED_RED (38) +#define LED_GREEN (36) +#define LED_BLUE (14) + +#define PIN_STATUS_LED LED_GREEN +#define LED_BUILTIN LED_GREEN +#define PIN_LED LED_BUILTIN +#define LED_PIN LED_BUILTIN +#define LED_STATE_ON LOW + +#define PIN_NEOPIXEL (14) +#define NEOPIXEL_NUM (2) + +//////////////////////////////////////////////////////////////////////////////// +// Builtin buttons + +#define PIN_BUTTON1 (42) +#define BUTTON_PIN PIN_BUTTON1 +#define PIN_USER_BTN BUTTON_PIN + +#define PIN_BUTTON2 (11) +#define BUTTON_PIN2 PIN_BUTTON2 + +#define EXTERNAL_FLASH_DEVICES MX25R1635F +#define EXTERNAL_FLASH_USE_QSPI + +//////////////////////////////////////////////////////////////////////////////// +// Lora + +#define USE_SX1262 +#define LORA_CS (24) +#define SX126X_DIO1 (20) +#define SX126X_BUSY (17) +#define SX126X_RESET (25) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +//////////////////////////////////////////////////////////////////////////////// +// SPI1 + +#define PIN_SPI1_MISO (38) +#define PIN_SPI1_MOSI (29) +#define PIN_SPI1_SCK (31) + +// GxEPD2 needs that for a panel that is not even used ! +extern const int MISO; +extern const int MOSI; +extern const int SCK; + +//////////////////////////////////////////////////////////////////////////////// +// Display + +#define DISP_MISO (38) +#define DISP_MOSI (29) +#define DISP_SCLK (31) +#define DISP_CS (30) +#define DISP_DC (28) +#define DISP_RST (2) +#define DISP_BUSY (3) +#define DISP_BACKLIGHT (43) + +//////////////////////////////////////////////////////////////////////////////// +// GPS + +#define PIN_GPS_RX (41) +#define PIN_GPS_TX (40) +#define PIN_GPS_WAKEUP (34) +#define PIN_GPS_RESET (37) +#define PIN_GPS_PPS (36) +#define PIN_GPS_STANDBY (34) diff --git a/variants/wio-e5/platformio.ini b/variants/wio-e5/platformio.ini new file mode 100644 index 00000000..d8f7954f --- /dev/null +++ b/variants/wio-e5/platformio.ini @@ -0,0 +1,32 @@ +[lora_e5] +extends = stm32_base +board = lora_e5_dev_board +board_upload.maximum_size = 229376 ; 32kb for FS +build_flags = ${stm32_base.build_flags} + -D RADIO_CLASS=CustomSTM32WLx + -D WRAPPER_CLASS=CustomSTM32WLxWrapper + -D SPI_INTERFACES_COUNT=0 + -D RX_BOOSTED_GAIN=true + -I variants/wio-e5 +build_src_filter = ${stm32_base.build_src_filter} + +<../variants/wio-e5> + +[env:wio-e5-repeater] +extends = lora_e5 +build_flags = ${lora_e5.build_flags} + -D LORA_TX_POWER=22 + -D ADVERT_NAME='"WIO-E5 Repeater"' + -D ADMIN_PASSWORD='"password"' +build_src_filter = ${lora_e5.build_src_filter} + +<../examples/simple_repeater/main.cpp> + +[env:wio-e5_companion_radio_usb] +extends = lora_e5 +build_flags = ${lora_e5.build_flags} + -D LORA_TX_POWER=22 + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +build_src_filter = ${lora_e5.build_src_filter} + +<../examples/companion_radio/*.cpp> +lib_deps = ${lora_e5.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/wio-e5/target.cpp b/variants/wio-e5/target.cpp new file mode 100644 index 00000000..8ccbe384 --- /dev/null +++ b/variants/wio-e5/target.cpp @@ -0,0 +1,73 @@ +#include +#include "target.h" +#include + +WIOE5Board board; + +RADIO_CLASS radio = new STM32WLx_Module(); + +WRAPPER_CLASS radio_driver(radio, board); + +static const uint32_t rfswitch_pins[] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; +static const Module::RfSwitchMode_t rfswitch_table[] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, + {STM32WLx::MODE_RX, {HIGH, LOW}}, + {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, // for LoRa-E5 mini +// {STM32WLx::MODE_TX_LP, {HIGH, HIGH}}, // for LoRa-E5-LE mini + END_OF_MODE_TABLE, +}; + +VolatileRTCClock rtc_clock; +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { +// rtc_clock.begin(Wire); + +// #ifdef SX126X_DIO3_TCXO_VOLTAGE +// float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +// #else +// float tcxo = 1.6f; +// #endif + + radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); + + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, 1.7, 0); + + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + + #ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); + #endif + + radio.setCRC(1); + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/wio-e5/target.h b/variants/wio-e5/target.h new file mode 100644 index 00000000..cc6131cf --- /dev/null +++ b/variants/wio-e5/target.h @@ -0,0 +1,27 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include + +class WIOE5Board : public STM32Board { +public: + const char* getManufacturerName() const override { + return "Seeed Wio E5"; + } +}; + +extern WIOE5Board board; +extern WRAPPER_CLASS radio_driver; +extern VolatileRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/wio-e5/variant.h b/variants/wio-e5/variant.h new file mode 100644 index 00000000..84b9cd16 --- /dev/null +++ b/variants/wio-e5/variant.h @@ -0,0 +1,10 @@ +#pragma once + +// UART Definitions +#ifndef SERIAL_UART_INSTANCE + #define SERIAL_UART_INSTANCE 101 +#endif + +#include + +#undef RNG diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index e790e74e..6024905f 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -15,7 +15,7 @@ build_flags = -D PIN_BOARD_SCL=D7 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 - -D SX126X_CURRENT_LIMIT=130.0f ; for best TX power! + -D SX126X_CURRENT_LIMIT=140 build_src_filter = ${esp32_base.build_src_filter} +<../variants/xiao_c3> @@ -33,6 +33,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = @@ -52,6 +53,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = diff --git a/variants/xiao_c3/target.cpp b/variants/xiao_c3/target.cpp index bab602cc..0ed1609d 100644 --- a/variants/xiao_c3/target.cpp +++ b/variants/xiao_c3/target.cpp @@ -14,6 +14,7 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/xiao_c3/target.h b/variants/xiao_c3/target.h index c0a1eb32..0aea87dd 100644 --- a/variants/xiao_c3/target.h +++ b/variants/xiao_c3/target.h @@ -7,10 +7,12 @@ #include #include #include +#include extern XiaoC3Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 0947d86e..2f468f4f 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -16,6 +16,11 @@ lib_ignore = lib_deps = ${nrf52_base.lib_deps} rweather/Crypto @ ^0.4.0 + adafruit/Adafruit INA3221 Library @ ^1.0.1 + adafruit/Adafruit INA219 @ ^1.2.3 + adafruit/Adafruit AHTX0 @ ^2.0.5 + adafruit/Adafruit BME280 Library @ ^2.3.0 + [Xiao_nrf52] extends = nrf52840_xiao @@ -27,10 +32,23 @@ build_flags = ${nrf52840_xiao.build_flags} -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 - -D SX126X_CURRENT_LIMIT=130 + -D P_LORA_DIO_1=D1 + -D P_LORA_RESET=D2 + -D P_LORA_BUSY=D3 + -D P_LORA_NSS=D4 + -D SX126X_DIO2_AS_RF_SWITCH=1 + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D PIN_WIRE_SCL=6 + -D PIN_WIRE_SDA=7 + -D ENV_INCLUDE_AHTX0=1 + -D ENV_INCLUDE_BME280=1 + -D ENV_INCLUDE_INA3221=1 + -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52840_xiao.build_src_filter} + + + + +<../variants/xiao_nrf52> debug_tool = jlink @@ -43,11 +61,27 @@ build_flags = -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_nrf52.build_src_filter} + + + +<../examples/companion_radio/main.cpp> +lib_deps = + ${Xiao_nrf52.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Xiao_nrf52_companion_radio_usb] +extends = Xiao_nrf52 +build_flags = + ${Xiao_nrf52.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 +; -D ENABLE_PRIVATE_KEY_IMPORT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} + +<../examples/companion_radio/main.cpp> @@ -57,7 +91,7 @@ lib_deps = [env:Xiao_nrf52_alt_pinout_companion_radio_ble] extends = env:Xiao_nrf52_companion_radio_ble -build_flags = +build_flags = ${env:Xiao_nrf52_companion_radio_ble.build_flags} -D SX1262_XIAO_S3_VARIANT @@ -69,6 +103,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} @@ -76,6 +111,19 @@ build_src_filter = ${Xiao_nrf52.build_src_filter} [env:Xiao_nrf52_alt_pinout_repeater] extends = env:Xiao_nrf52_repeater -build_flags = +build_flags = ${env:Xiao_nrf52_repeater.build_flags} - -D SX1262_XIAO_S3_VARIANT \ No newline at end of file + -D SX1262_XIAO_S3_VARIANT + +[env:Xiao_nrf52_room_server] +extends = Xiao_nrf52 +build_flags = + ${Xiao_nrf52.build_flags} + -D ADVERT_NAME='"Xiao_nrf52 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Xiao_nrf52.build_src_filter} + +<../examples/simple_room_server/main.cpp> \ No newline at end of file diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index 0707aa39..c78ea159 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -9,6 +9,7 @@ RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BU WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock rtc_clock; +EnvironmentSensorManager sensors; #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index 85f1ce1c..ec298a43 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -6,10 +6,12 @@ #include #include #include +#include extern XiaoNrf52Board board; extern WRAPPER_CLASS radio_driver; extern VolatileRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); diff --git a/variants/xiao_nrf52/variant.h b/variants/xiao_nrf52/variant.h index 5d68d270..d941463e 100644 --- a/variants/xiao_nrf52/variant.h +++ b/variants/xiao_nrf52/variant.h @@ -98,20 +98,20 @@ static const uint8_t A5 = PIN_A5; #define PIN_SPI_MOSI (10) #define PIN_SPI_SCK (8) -static const uint8_t SS = D3; // NSS for sx ? -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK ; - #define PIN_SPI1_MISO (25) #define PIN_SPI1_MOSI (26) #define PIN_SPI1_SCK (29) +// Lora SPI is on SPI0 +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI + // Wire Interfaces #define WIRE_INTERFACES_COUNT (1) -#define PIN_WIRE_SDA (17) // 4 and 5 are used for the sx1262 ! -#define PIN_WIRE_SCL (16) // use WIRE1_SDA +// #define PIN_WIRE_SDA (17) // 4 and 5 are used for the sx1262 ! +// #define PIN_WIRE_SCL (16) // use WIRE1_SDA static const uint8_t SDA = PIN_WIRE_SDA; static const uint8_t SCL = PIN_WIRE_SCL; diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index ca043f1f..841a50c3 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -17,7 +17,7 @@ build_flags = ${esp32_base.build_flags} -D PIN_STATUS_LED=48 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 - -D SX126X_CURRENT_LIMIT=130 + -D SX126X_CURRENT_LIMIT=140 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 @@ -35,6 +35,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = @@ -79,17 +80,21 @@ build_flags = -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 + -D DISPLAY_CLASS=SSD1306Display + -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} + + + - +<../examples/companion_radio/main.cpp> + +<../examples/companion_radio> lib_deps = ${Xiao_S3_WIO.lib_deps} densaugeo/base64 @ ~1.4.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 [env:Xiao_S3_WIO_companion_radio_serial] extends = Xiao_S3_WIO @@ -107,25 +112,3 @@ build_src_filter = ${Xiao_S3_WIO.build_src_filter} lib_deps = ${Xiao_S3_WIO.lib_deps} densaugeo/base64 @ ~1.4.0 - -[env:Xiao_S3_WIO_expansion_companion_radio_ble] -extends = Xiao_S3_WIO -build_flags = - ${Xiao_S3_WIO.build_flags} - -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=8 - -D BLE_PIN_CODE=123456 - -D DISPLAY_CLASS=SSD1306Display -; -D BLE_DEBUG_LOGGING=1 -; -D ENABLE_PRIVATE_KEY_IMPORT=1 -; -D ENABLE_PRIVATE_KEY_EXPORT=1 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -build_src_filter = ${Xiao_S3_WIO.build_src_filter} - + - + - +<../examples/companion_radio> -lib_deps = - ${Xiao_S3_WIO.lib_deps} - densaugeo/base64 @ ~1.4.0 - adafruit/Adafruit SSD1306 @ ^2.5.13 diff --git a/variants/xiao_s3_wio/target.cpp b/variants/xiao_s3_wio/target.cpp index ddb700b7..09ee5daa 100644 --- a/variants/xiao_s3_wio/target.cpp +++ b/variants/xiao_s3_wio/target.cpp @@ -14,6 +14,11 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; +#endif #ifndef LORA_CR #define LORA_CR 5 diff --git a/variants/xiao_s3_wio/target.h b/variants/xiao_s3_wio/target.h index 8eed467f..609248ae 100644 --- a/variants/xiao_s3_wio/target.h +++ b/variants/xiao_s3_wio/target.h @@ -6,10 +6,19 @@ #include #include #include +#include +#ifdef DISPLAY_CLASS + #include +#endif extern ESP32Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; +#endif bool radio_init(); uint32_t radio_get_rng_seed();