mirror of
https://github.com/usetrmnl/trmnl_lib.git
synced 2026-04-29 13:45:30 -07:00
initial commit
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
Copyright 2020 BitBank Software, Inc. All rights reserved.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020 BitBank Software, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# TRMNL library
|
||||
A link library to easily add TRMNL functionality to any project.<br>
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// TRMNL demo for the Crowpanel Advance ESP32-P4 5" 800x480
|
||||
//
|
||||
#include <trmnl_lib.h>
|
||||
#include <bb_spi_lcd.h>
|
||||
//#define PNG_MAX_BUFFERED_PIXELS (801 * 4 * 2)
|
||||
#include <PNGdec.h>
|
||||
PNG png;
|
||||
BB_SPI_LCD lcd;
|
||||
TRMNL trmnl;
|
||||
|
||||
// Enter your WiFi credentials and TRMNL Device Key
|
||||
const char *ssid = "your_ssid";
|
||||
const char *pw = "your_password";
|
||||
const char *szAPIKey = "your_api_key"; // N.B. Don't share this publicly
|
||||
uint16_t *pTemp;
|
||||
|
||||
int png_draw(PNGDRAW *pDraw)
|
||||
{
|
||||
int iWidth = pDraw->iWidth;
|
||||
|
||||
if (pDraw->y == 0) {
|
||||
lcd.setAddrWindow(0, 0, lcd.width(), lcd.height());
|
||||
}
|
||||
if (pDraw->y >= lcd.height()) {
|
||||
return 0; // stop decoding if we'll go past the bottom
|
||||
}
|
||||
if (iWidth > lcd.width()) iWidth = lcd.width(); // crop image width to display size if it's larger
|
||||
png.getLineAsRGB565(pDraw, pTemp, PNG_RGB565_BIG_ENDIAN, 0xffffff);
|
||||
lcd.pushPixels(pTemp, iWidth);
|
||||
return 1;
|
||||
} /* png_draw() */
|
||||
|
||||
void displayImage(uint8_t *pImage, int iImageSize)
|
||||
{
|
||||
int rc = png.openRAM(pImage, iImageSize, png_draw);
|
||||
if (rc == PNG_SUCCESS) {
|
||||
Serial.printf("PNG opened: %d x %d, %d-bpp\n", png.getWidth(), png.getHeight(), png.getBpp());
|
||||
pTemp = (uint16_t *)malloc(png.getWidth() * 2); // temporary RGB565 buffer
|
||||
png.decode(NULL, 0);
|
||||
free(pTemp);
|
||||
}
|
||||
png.close();
|
||||
} /* displayImage() */
|
||||
|
||||
void setup() {
|
||||
int rc, iSize;
|
||||
uint8_t *pImg;
|
||||
|
||||
Serial.begin(115200);
|
||||
delay(3000);
|
||||
Serial.println("Starting TRMNL demo");
|
||||
lcd.begin(DISPLAY_ELECROW_S3_800x480);
|
||||
lcd.fillScreen(TFT_BLACK); // clear to black and sets memory window for next pass
|
||||
|
||||
trmnl.setDisplaySize(800, 480); // dynamic display size is not supported yet; for future use
|
||||
if (trmnl.connectWiFi(ssid, pw)) {
|
||||
rc = trmnl.getAPI(szAPIKey); // also reads and sends the sensor values if valid
|
||||
if (rc == TRMNL_SUCCESS) {
|
||||
rc = trmnl.getImage(&pImg, &iSize);
|
||||
if (rc == TRMNL_SUCCESS) {
|
||||
displayImage(pImg, iSize);
|
||||
trmnl.freeImage();
|
||||
}
|
||||
}
|
||||
trmnl.disconnectWiFi();
|
||||
} // connected
|
||||
}
|
||||
|
||||
void loop() {
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
#include <PNGdec.h>
|
||||
#include <trmnl_lib.h>
|
||||
#define USE_EPAPER
|
||||
#ifdef USE_EPAPER
|
||||
#include <bb_epaper.h>
|
||||
#include "../Fonts/Roboto_Black_16.h"
|
||||
extern TRMNL trmnl;
|
||||
BBEPAPER bbep;
|
||||
#else
|
||||
#include <bb_spi_lcd.h>
|
||||
BB_SPI_LCD lcd;
|
||||
#endif // !BB_EPAPER
|
||||
int iImageHeight;
|
||||
PNG *png;
|
||||
uint16_t *pTemp;
|
||||
|
||||
//
|
||||
// Draw callback (per scan line) from PNGdec
|
||||
//
|
||||
#ifdef USE_EPAPER
|
||||
void ReduceBpp(int iDestBpp, int iPixelType, uint8_t *pPalette, uint8_t *pSrc, uint8_t *pDest, int w, int iSrcBpp)
|
||||
{
|
||||
int g = 0, x, iDelta;
|
||||
uint8_t *s, *d, *pPal, u8, count;
|
||||
const uint8_t u8G2ToG8[4] = {0x00, 0x55, 0xaa, 0xff}; // 2-bit to 8-bit gray
|
||||
|
||||
if (iPixelType == PNG_PIXEL_TRUECOLOR) iSrcBpp = 24;
|
||||
else if (iPixelType == PNG_PIXEL_TRUECOLOR_ALPHA) iSrcBpp = 32;
|
||||
iDelta = iSrcBpp/8; // bytes per pixel
|
||||
count = 8; // bits in a byte
|
||||
u8 = 0; // start with all black
|
||||
d = pDest;
|
||||
s = pSrc;
|
||||
for (x=0; x<w; x++) {
|
||||
u8 <<= iDestBpp;
|
||||
switch (iSrcBpp) {
|
||||
case 24:
|
||||
case 32:
|
||||
g = (s[0] + s[1]*2 + s[2])/4; // convert color to gray value
|
||||
s += iDelta;
|
||||
break;
|
||||
case 8:
|
||||
if (iPixelType == PNG_PIXEL_INDEXED) {
|
||||
pPal = &pPalette[s[0] * 3];
|
||||
g = (pPal[0] + pPal[1]*2 + pPal[2])/4;
|
||||
} else { // must be grayscale
|
||||
g = s[0];
|
||||
}
|
||||
s++;
|
||||
break;
|
||||
case 4:
|
||||
if (x & 1) {
|
||||
if (iPixelType == PNG_PIXEL_INDEXED) {
|
||||
pPal = &pPalette[(s[0] & 0xf) * 3];
|
||||
g = (pPal[0] + pPal[1]*2 + pPal[2])/4;
|
||||
} else {
|
||||
g = (s[0] & 0xf) | (s[0] << 4);
|
||||
}
|
||||
s++;
|
||||
} else {
|
||||
if (iPixelType == PNG_PIXEL_INDEXED) {
|
||||
pPal = &pPalette[(s[0]>>4) * 3];
|
||||
g = (pPal[0] + pPal[1]*2 + pPal[2])/4;
|
||||
} else {
|
||||
g = (s[0] & 0xf0) | (s[0] >> 4);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2: // We need to handle this case for 2-bit images with (random) palettes
|
||||
g = s[0] >> (6-((x & 3) * 2));
|
||||
if (iPixelType == PNG_PIXEL_INDEXED) {
|
||||
pPal = &pPalette[(g & 3)*3];
|
||||
g = (pPal[0] + pPal[1]*2 + pPal[2])/4;
|
||||
} else {
|
||||
g = u8G2ToG8[g & 3];
|
||||
}
|
||||
if ((x & 3) == 3) {
|
||||
s++;
|
||||
}
|
||||
break;
|
||||
} // switch on bpp
|
||||
if (iDestBpp == 1) {
|
||||
u8 |= (g >> 7); // B/W
|
||||
} else if (iDestBpp == 2) { // generate 4 gray levels (2 bits)
|
||||
u8 |= (3 ^ (g >> 6)); // 4 gray levels (inverted relative to 1-bit)
|
||||
} else { // must be 4-bpp output
|
||||
u8 |= (g >> 4);
|
||||
}
|
||||
count -= iDestBpp;
|
||||
if (count == 0) { // byte is full, move on
|
||||
*d++ = u8;
|
||||
u8 = 0;
|
||||
count = 8;
|
||||
}
|
||||
} // for x
|
||||
if (count != 8) { // partial byte remaining
|
||||
u8 <<= count;
|
||||
*d++ = u8;
|
||||
}
|
||||
} /* ReduceBpp() */
|
||||
enum {
|
||||
PNG_1_BIT = 0,
|
||||
PNG_1_BIT_INVERTED,
|
||||
PNG_2_BIT_0,
|
||||
PNG_2_BIT_1,
|
||||
PNG_2_BIT_BOTH,
|
||||
PNG_2_BIT_INVERTED,
|
||||
};
|
||||
|
||||
int png_draw(PNGDRAW *pDraw)
|
||||
{
|
||||
int x;
|
||||
uint8_t ucBppChanged = 0, ucInvert = 0;
|
||||
uint8_t uc, ucMask, src, *s, *d, *pTemp = bbep.getCache(); // get some scratch memory (not from the stack)
|
||||
int iPlane = *(int *)pDraw->pUser;
|
||||
int iWidth;
|
||||
|
||||
iWidth = pDraw->iWidth;
|
||||
if (pDraw->y >= bbep.height()) return 0; // stop decoding if we'll go past the bottom
|
||||
if (iWidth > bbep.width()) iWidth = bbep.width(); // crop image width to display size if it's larger
|
||||
|
||||
if (pDraw->iPixelType == PNG_PIXEL_INDEXED || pDraw->iBpp > 2) {
|
||||
if (pDraw->iBpp == 1) { // 1-bit output, just see which color is brighter
|
||||
uint32_t u32Gray0, u32Gray1;
|
||||
u32Gray0 = pDraw->pPalette[0] + (pDraw->pPalette[1]<<2) + pDraw->pPalette[2];
|
||||
u32Gray1 = pDraw->pPalette[3] + (pDraw->pPalette[4]<<2) + pDraw->pPalette[5];
|
||||
if (u32Gray0 < u32Gray1) {
|
||||
ucInvert = 0xff;
|
||||
}
|
||||
} else {
|
||||
// Reduce the source image to 1-bpp or 2-bpp
|
||||
ReduceBpp((pDraw->pUser) ? 2:1, pDraw->iPixelType, pDraw->pPalette, pDraw->pPixels, pTemp, iWidth, pDraw->iBpp);
|
||||
ucBppChanged = 1;
|
||||
}
|
||||
} else if (pDraw->iBpp == 2) {
|
||||
ucInvert = 0xff; // 2-bit non-palette images need to be inverted colors for 4-gray mode
|
||||
}
|
||||
s = (ucBppChanged) ? pTemp : (uint8_t *)pDraw->pPixels;
|
||||
d = pTemp;
|
||||
if (iPlane == PNG_1_BIT || iPlane == PNG_1_BIT_INVERTED) {
|
||||
// 1-bit output, decode the single plane and write it
|
||||
if (iPlane == PNG_1_BIT_INVERTED) ucInvert = ~ucInvert; // to do PLANE_FALSE_DIFF
|
||||
if (iPlane == PNG_1_BIT_INVERTED && (bbep.capabilities() & BBEP_3COLOR)) { // write the red plane as 0's for this case
|
||||
memset(d, 0, iWidth/8);
|
||||
} else {
|
||||
for (x=0; x<iWidth; x+= 8) {
|
||||
d[0] = s[0] ^ ucInvert;
|
||||
d++; s++;
|
||||
}
|
||||
}
|
||||
} else { // we need to split the 2-bit data into plane 0 and 1
|
||||
src = *s++;
|
||||
src ^= ucInvert;
|
||||
uc = 0; // suppress warning/error
|
||||
if (iPlane == PNG_2_BIT_BOTH || iPlane == PNG_2_BIT_INVERTED) { // draw 2bpp data as 1-bit to use for partial update
|
||||
if (iPlane == PNG_2_BIT_BOTH) {
|
||||
ucInvert = ~ucInvert; // the invert rule is backwards for grayscale data
|
||||
}
|
||||
src = ~src;
|
||||
for (x=0; x<iWidth; x++) {
|
||||
uc <<= 1;
|
||||
if (src & 0xc0) { // non-white -> black
|
||||
uc |= 1; // high bit of source pair
|
||||
}
|
||||
src <<= 2;
|
||||
if ((x & 3) == 3) { // new input byte
|
||||
src = *s++;
|
||||
src ^= ucInvert;
|
||||
}
|
||||
if ((x & 7) == 7) { // new output byte
|
||||
*d++ = uc;
|
||||
}
|
||||
} // for x
|
||||
} else { // normal 0/1 split plane
|
||||
ucMask = (iPlane == PNG_2_BIT_0) ? 0x40 : 0x80; // lower or upper source bit
|
||||
for (x=0; x<iWidth; x++) {
|
||||
uc <<= 1;
|
||||
if (src & ucMask) {
|
||||
uc |= 1; // high bit of source pair
|
||||
}
|
||||
src <<= 2;
|
||||
if ((x & 3) == 3) { // new input byte
|
||||
src = *s++;
|
||||
src ^= ucInvert;
|
||||
}
|
||||
if ((x & 7) == 7) { // new output byte
|
||||
*d++ = uc;
|
||||
}
|
||||
} // for x
|
||||
}
|
||||
}
|
||||
bbep.writeData(pTemp, (iWidth+7)/8);
|
||||
if (iWidth < bbep.width()) {
|
||||
// the image is narrower than the display, fill in the right edge with white
|
||||
int w = (bbep.width() - iWidth)/8;
|
||||
if (w) {
|
||||
memset(pTemp, 0xff, w); // white
|
||||
bbep.writeData(pTemp, w);
|
||||
}
|
||||
}
|
||||
// If we're at the last line of the PNG image, but it's shorter than the display,
|
||||
// fill the remaining lines with white
|
||||
if (pDraw->y == iImageHeight-1 && iImageHeight < bbep.height()) {
|
||||
int i, w = (bbep.width() + 7)/8;
|
||||
memset(pTemp, 0xff, w);
|
||||
for (i=pDraw->y; i<bbep.height(); i++) {
|
||||
// write the remaing lines as white
|
||||
bbep.writeData(pTemp, w);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
} /* png_draw() */
|
||||
#else // color LCD
|
||||
int png_draw(PNGDRAW *pDraw)
|
||||
{
|
||||
int iWidth = pDraw->iWidth;
|
||||
|
||||
if (pDraw->y == 0) {
|
||||
lcd.setAddrWindow(0, 0, lcd.width(), lcd.height());
|
||||
}
|
||||
if (pDraw->y >= lcd.height()) {
|
||||
return 0; // stop decoding if we'll go past the bottom
|
||||
}
|
||||
if (iWidth > lcd.width()) iWidth = lcd.width(); // crop image width to display size if it's larger
|
||||
png->getLineAsRGB565(pDraw, pTemp, PNG_RGB565_BIG_ENDIAN, 0xffffff);
|
||||
lcd.pushPixels(pTemp, iWidth);
|
||||
return 1;
|
||||
} /* png_draw() */
|
||||
#endif // USE_EPAPER
|
||||
//
|
||||
// Display a PNG image on the given display
|
||||
// crop it if it's too large
|
||||
//
|
||||
void displayImage(uint8_t *pImage, int iImageSize)
|
||||
{
|
||||
int iPlane;
|
||||
png = new PNG();
|
||||
int rc = png->openRAM(pImage, iImageSize, png_draw);
|
||||
if (rc == PNG_SUCCESS) {
|
||||
bbep.begin(EPD_XTEINK_X3);
|
||||
iImageHeight = png->getHeight();
|
||||
Serial.printf("PNG opened: %d x %d, %d-bpp\n", png->getWidth(), png->getHeight(), png->getBpp());
|
||||
#ifdef USE_EPAPER
|
||||
bbep.setAddrWindow(0, 0, bbep.width(), bbep.height());
|
||||
if (png->getBpp() == 1) { // 1-bit image (single plane)
|
||||
png->close(); // use a different PNGDraw callback for color matching
|
||||
bbep.startWrite(PLANE_0); // start writing image data to plane 0
|
||||
iPlane = PNG_1_BIT;
|
||||
png->decode(&iPlane, 0);
|
||||
} else { // 2-bpp (or greater, but reduced to 2-bpp)
|
||||
bbep.setPanelType(EP368_792x528_4GRAY);
|
||||
bbep.startWrite(PLANE_0); // start writing image data to plane 0
|
||||
iPlane = PNG_2_BIT_0;
|
||||
png->decode(&iPlane, 0); // tell PNGDraw to use bits for plane 0
|
||||
png->close(); // start over for plane 1
|
||||
iPlane = PNG_2_BIT_1;
|
||||
png->openRAM((uint8_t *)pImage, iImageSize, png_draw);
|
||||
bbep.startWrite(PLANE_1); // start writing image data to plane 1
|
||||
png->decode(&iPlane, 0); // decode it again to get plane 1 data
|
||||
}
|
||||
bbep.refresh(REFRESH_FULL);
|
||||
bbep.sleep(LIGHT_SLEEP);
|
||||
#else // must be color LCD
|
||||
pTemp = (uint16_t *)malloc(png->getWidth() * 2); // temporary RGB565 buffer
|
||||
lcd.begin(DISPLAY_WS_AMOLED_18); // Waveshare ESP32-S3 AMOLED 1.8" 368x448
|
||||
lcd.fillScreen(TFT_BLACK); // clear to black and sets memory window for next pass
|
||||
png->decode(NULL, 0);
|
||||
free(pTemp);
|
||||
#endif
|
||||
} else {
|
||||
Serial.println("Error opening the image!");
|
||||
}
|
||||
png->close();
|
||||
free(png);
|
||||
} /* displayImage() */
|
||||
|
||||
void displaySensorValues(void)
|
||||
{
|
||||
time_t now;
|
||||
struct tm *thetime;
|
||||
#ifdef USE_EPAPER
|
||||
bbep.begin(EPD_WAVESHARE_154); // pre-configured for the Waveshare ESP32-S3 1.54" e-Paper
|
||||
bbep.allocBuffer();
|
||||
bbep.fillScreen(BBEP_WHITE);
|
||||
time(&now);
|
||||
thetime = gmtime(&now);
|
||||
bbep.setTextColor(BBEP_BLACK, BBEP_WHITE);
|
||||
bbep.setFont(Roboto_Black_16);
|
||||
bbep.setCursor(0, 24); // baseline, not top of font
|
||||
bbep.print("TRMNL");
|
||||
bbep.setCursor(0,50);
|
||||
bbep.print("Sensor node");
|
||||
bbep.setFont(FONT_12x16);
|
||||
bbep.setCursor(0, 64);
|
||||
bbep.printf("Last: %02d:%02d UTC\n", thetime->tm_hour, thetime->tm_min);
|
||||
now += trmnl.getSleepTime();
|
||||
thetime = gmtime(&now);
|
||||
bbep.printf("Next: %02d:%02d UTC\n", thetime->tm_hour, thetime->tm_min);
|
||||
bbep.printf("Temp: %.1f C\n", trmnl.getTemperature());
|
||||
bbep.printf("Hum: %d %%\n", trmnl.getHumidity());
|
||||
if (trmnl.getCo2()) {
|
||||
bbep.printf("CO2: %d ppm\n", trmnl.getCo2());
|
||||
}
|
||||
bbep.writePlane();
|
||||
bbep.refresh(REFRESH_FAST);
|
||||
bbep.sleep(LIGHT_SLEEP);
|
||||
#else // LCD
|
||||
lcd.begin(DISPLAY_M5STACK_ATOMS3); // pre-configured for the M5Stack AtomS3
|
||||
lcd.fillScreen(TFT_BLACK);
|
||||
time(&now);
|
||||
thetime = gmtime(&now);
|
||||
lcd.setTextColor(TFT_GREEN, TFT_BLACK)
|
||||
lcd.setCursor(0,0);
|
||||
lcd.setFont(FONT_12x16);
|
||||
lcd.println("TRMNL\nSensor node");
|
||||
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
lcd.printf("L: %02d:%02d UTC\n", thetime->tm_hour, thetime->tm_min);
|
||||
now += trmnl.getSleepTime();
|
||||
thetime = gmtime(&now);
|
||||
lcd.printf("N: %02d:%02d UTC\n", thetime->tm_hour, thetime->tm_min);
|
||||
if (trmnl.temperature()) {
|
||||
lcd.printf("Temp: %.1f C\n", trmnl.temperature());
|
||||
}
|
||||
if (trmnl.humidity()) {
|
||||
lcd.printf("Hum: %d %%\n", trmnl.humidity());
|
||||
}
|
||||
if (trmnl.co2()) {
|
||||
lcd.printf("CO2: %d ppm\n", trmnl.co2());
|
||||
}
|
||||
#endif
|
||||
} /* displaySensorValues() */
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// A minimal Arduino example to show how to interface
|
||||
// your own code to the TRMNL back-end
|
||||
// written by Larry Bank (bitbank@pobox.com)
|
||||
// March 25, 2025
|
||||
//
|
||||
#include "trmnl_lib.h"
|
||||
#include "esp_task_wdt.h"
|
||||
void displayImage(uint8_t *pImage, int iImageSize);
|
||||
void displaySensorValues(void);
|
||||
TRMNL trmnl;
|
||||
|
||||
// Enter your WiFi credentials and TRMNL Device Key
|
||||
const char *ssid = "your_ssid";
|
||||
const char *pw = "your_password";
|
||||
const char *szAPIKey = "your_api_key"; // N.B. Don't share this publicly
|
||||
|
||||
void setup()
|
||||
{
|
||||
int rc;
|
||||
uint8_t *pImage;
|
||||
int iImageSize;
|
||||
|
||||
Serial.begin(115200);
|
||||
delay(3000);
|
||||
// esp_task_wdt_config_t wdt_config = {30, 3, false};
|
||||
// esp_task_wdt_init(&wdt_config); // disable watchdog timer
|
||||
|
||||
Serial.println("Starting TRMNL demo");
|
||||
trmnl.setDisplaySize(800, 480); // dynamic display size is not supported yet; for future use
|
||||
trmnl.setSensorBus(2, 1); // M5Stack AtomS3 GROVE I2C
|
||||
// trmnl.setSensorBus(47, 48); // Waveshare ESP32-S3 ePaper 1.54 internal I2C temp/humidity sensor
|
||||
if (trmnl.connectWiFi(ssid, pw)) {
|
||||
rc = trmnl.getAPI(szAPIKey); // also reads and sends the sensor values if valid
|
||||
if (rc == TRMNL_SUCCESS) {
|
||||
// displaySensorValues(); // instead of showing the TRMNL image
|
||||
rc = trmnl.getImage(&pImage, &iImageSize);
|
||||
if (rc == TRMNL_SUCCESS) {
|
||||
displayImage(pImage, iImageSize);
|
||||
trmnl.freeImage();
|
||||
}
|
||||
}
|
||||
trmnl.disconnectWiFi();
|
||||
}
|
||||
// Special setup for the Xteink X3 to keep the battery on, but sleep with the lowest power
|
||||
pinMode(13, OUTPUT);
|
||||
digitalWrite(13, HIGH);
|
||||
gpio_hold_en(GPIO_NUM_13); // MOSFET enabling the battery power
|
||||
gpio_hold_en(GPIO_NUM_5); // hold EPD reset high in deep sleep
|
||||
gpio_deep_sleep_hold_en(); // Needed to keep the battery power enabled during RTC sleep
|
||||
|
||||
trmnl.sleep();
|
||||
} /* setup() */
|
||||
|
||||
void loop()
|
||||
{
|
||||
} /* loop() */
|
||||
@@ -0,0 +1,29 @@
|
||||
#include <trmnl_lib.h>
|
||||
extern TRMNL trmnl;
|
||||
#include <bb_spi_lcd.h>
|
||||
extern BB_SPI_LCD lcd;
|
||||
|
||||
void displaySensorValues(void)
|
||||
{
|
||||
time_t now;
|
||||
struct tm *thetime;
|
||||
time(&now);
|
||||
thetime = gmtime(&now);
|
||||
lcd.setCursor(0,16);
|
||||
lcd.setFont(FONT_12x16);
|
||||
lcd.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
lcd.printf("%02d:%02d UTC\n", thetime->tm_hour, thetime->tm_min);
|
||||
// now += trmnl.getSleepTime();
|
||||
// thetime = gmtime(&now);
|
||||
// lcd.printf("N: %02d:%02d UTC\n", thetime->tm_hour, thetime->tm_min);
|
||||
lcd.setTextColor(TFT_MAGENTA, TFT_BLACK);
|
||||
if (trmnl.getCo2()) {
|
||||
lcd.printf("CO2: %d ppm\n", trmnl.getCo2());
|
||||
}
|
||||
if (trmnl.getTemperature()) {
|
||||
lcd.printf("Temp: %.1f C\n", trmnl.getTemperature());
|
||||
}
|
||||
if (trmnl.getHumidity()) {
|
||||
lcd.printf("Hum: %d %%\n", trmnl.getHumidity());
|
||||
}
|
||||
} /* displaySensorValues() */
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// Example sketch for the TRMNL library
|
||||
// written by Larry Bank
|
||||
// April 23, 2026
|
||||
//
|
||||
// This sketch shows how to use the LilyGo T-Dongle C5 as a TRMNL sensor node
|
||||
// What this means is that an I2C sensor attached to the T-Dongle will push
|
||||
// Sensor readings to your TRMNL account for use in your plugins. The values
|
||||
// will also be displayed on the color LCD of the T-Dongle
|
||||
//
|
||||
#include <trmnl_lib.h>
|
||||
#include <bb_spi_lcd.h>
|
||||
BB_SPI_LCD lcd;
|
||||
TRMNL trmnl;
|
||||
void displaySensorValues(void);
|
||||
|
||||
// Enter your WiFi credentials and TRMNL Device Key
|
||||
const char *ssid = "your_ssid";
|
||||
const char *pw = "your_password";
|
||||
const char *szAPIKey = "your_api_key"; // N.B. Don't share this publicly
|
||||
|
||||
void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
delay(3000);
|
||||
Serial.println("Starting TRMNL demo");
|
||||
|
||||
lcd.begin(DISPLAY_T_DONGLE_C5); // pre-configured for the LilyGo T-Dongle C5
|
||||
lcd.fillScreen(TFT_BLACK);
|
||||
lcd.setTextColor(TFT_GREEN, TFT_BLACK);
|
||||
lcd.setCursor(0,0);
|
||||
lcd.setFont(FONT_12x16);
|
||||
lcd.println("TRMNL Sensor");
|
||||
|
||||
trmnl.setSensorBus(11, 12); // T-Dongle C5 QWIIC I2C Bus
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (trmnl.connectWiFi(ssid, pw)) {
|
||||
rc = trmnl.getAPI(szAPIKey); // also reads and sends the sensor values if valid
|
||||
if (rc == TRMNL_SUCCESS) {
|
||||
displaySensorValues(); // instead of showing the TRMNL image
|
||||
}
|
||||
trmnl.disconnectWiFi();
|
||||
}
|
||||
delay(15 * 60 * 1000); // push new values every 15 minutes
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
name=trmnl_lib
|
||||
version=1.0.0
|
||||
author=Larry Bank
|
||||
maintainer=Larry Bank
|
||||
sentence=Easily integrate TRMNL functionality into your project.
|
||||
paragraph=This library contains the core functionality of TRMNL and allows you to make image requests and display them in your own projects.
|
||||
category=Data Processing
|
||||
url=https://github.com/bitbank2/trmnl_lib/
|
||||
architectures=*
|
||||
includes=trmnl_lib.h
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
//
|
||||
// TRMNL library
|
||||
// Written by Larry Bank
|
||||
// project started March 25, 2026
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2026 TRMNL LLC
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===========================================================================
|
||||
//
|
||||
#include "trmnl_lib.h"
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <Preferences.h>
|
||||
#include "esp_sntp.h"
|
||||
#include <bb_scd41.h>
|
||||
#include <bb_temperature.h>
|
||||
SCD41 scd41;
|
||||
BBTemp bbt;
|
||||
static HTTPClient https;
|
||||
// 10 seconds WIFI connection timeout
|
||||
#define TIMEOUT 20
|
||||
const char *szDevices[] = {"None", "AHT20", "BMP180", "BME280", "BMP388", "SHT3X", "HDC1080", "HTS221", "MCP9808","BME68x","SHTC3"};
|
||||
const char *szMakers[] = {"None", "ASAIR", "Bosch", "Bosch", "Bosch", "Sensirion", "TI", "STMicro","MicroChip","Bosch","Sensirion"};
|
||||
//
|
||||
// Initialize the I2C bus and find+start any sensors that are found
|
||||
//
|
||||
void TRMNL::setSensorBus(uint8_t sda, uint8_t scl)
|
||||
{
|
||||
_bCO2 = false;
|
||||
_bTimeSync = true; // time stamp needs to have the correct time
|
||||
_iSensorType = -1; // assume no supported sensors
|
||||
|
||||
// check if there is a SCD41 or supported temperature sensor attached
|
||||
if (bbt.init(sda, scl) == BBT_SUCCESS) {
|
||||
_iSensorType = bbt.type();
|
||||
Serial.printf("%s [%d]: supported sensor found! (%d)\r\n", __FILE__, __LINE__, _iSensorType);
|
||||
bbt.start(); // start the sensor
|
||||
}
|
||||
if (scd41.init(sda, scl) == SCD41_SUCCESS) {
|
||||
_bCO2 = true;
|
||||
Serial.printf("%s [%d]: SCD41 sensor found!\r\n", __FILE__, __LINE__);
|
||||
scd41.wakeup();
|
||||
// The SCD41 needs to be re-initialized after big Vcc variations from the last wakeup
|
||||
// put it in a 'confused' state. If we don't re-initialize it, it won't generate more samples
|
||||
scd41.sendCMD(SCD41_CMD_REINIT);
|
||||
vTaskDelay(3); // allow time to reinitialize
|
||||
scd41.triggerSample(); // trigger a 'one-shot' sample that takes about 5 seconds to complete
|
||||
}
|
||||
if (!_bCO2 && _iSensorType < 0) {
|
||||
Serial.printf("%s [%d]: No sensor found on I2C bus %d/%d\r\n", __FILE__, __LINE__, sda, scl);
|
||||
}
|
||||
_lSensorTime = millis(); // mark the time when we triggered sensor samples
|
||||
} /* setSensorBus() */
|
||||
|
||||
bool TRMNL::connectWiFi(const char *ssid, const char *pw)
|
||||
{
|
||||
int iTimeout = 0;
|
||||
|
||||
Serial.print("Connecting to wifi");
|
||||
|
||||
WiFi.begin(ssid, pw);
|
||||
while (WiFi.status() != WL_CONNECTED && WiFi.status() != WL_CONNECT_FAILED && iTimeout < TIMEOUT) {
|
||||
delay(500); // allow up to 10 seconds to connect
|
||||
iTimeout++;
|
||||
Serial.print(".");
|
||||
}
|
||||
if (iTimeout == TIMEOUT || WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("\nConnection timed out!");
|
||||
} else {
|
||||
Serial.println("\nConnected!");
|
||||
return true;
|
||||
}
|
||||
return false; // failed
|
||||
|
||||
} /* connectWiFi() */
|
||||
|
||||
bool TRMNL::setClock()
|
||||
{
|
||||
bool sync_status = false;
|
||||
struct tm timeinfo;
|
||||
time_t now;
|
||||
int iDeltaTime;
|
||||
Preferences prefs;
|
||||
|
||||
prefs.begin("data");
|
||||
uint32_t u32Epoch = prefs.getUInt("last_sync", 0); // Get the last time sync time
|
||||
time(&now);
|
||||
iDeltaTime = (uint32_t)now - u32Epoch; // Number of seconds since the last sync
|
||||
Serial.printf("%s [%d]: epoch time: %lu iDelta: %d\r\n", __FILE__, __LINE__, (uint32_t)now, iDeltaTime);
|
||||
if (u32Epoch != 0 && iDeltaTime > 0 && iDeltaTime < 24*60*60) { // Less than 24h, no need to sync the time
|
||||
Serial.printf("%s [%d]: Skipping time sync\r\n", __FILE__, __LINE__);
|
||||
prefs.end();
|
||||
return true;
|
||||
}
|
||||
String ntp = prefs.getString("ntp_server", "time.google.com");
|
||||
|
||||
Serial.printf("%s [%d]: Using NTP: %s, fallback: time.cloudflare.com\r\n", __FILE__, __LINE__, ntp.c_str());
|
||||
configTime(0, 0, ntp.c_str(), "time.cloudflare.com");
|
||||
|
||||
for (int i = 0; i < SNTP_MAX_SERVERS; i++) {
|
||||
const char *srv = esp_sntp_getservername(i);
|
||||
if (srv && strlen(srv) > 0) {
|
||||
Serial.printf("%s [%d]: SNTP server[%d]: %s\r\n", __FILE__, __LINE__, i, srv);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("%s [%d]: Time synchronization...\r\n", __FILE__, __LINE__);
|
||||
|
||||
// Wait for time to be set
|
||||
if (getLocalTime(&timeinfo)) {
|
||||
sync_status = true;
|
||||
Serial.printf("%s [%d]: Time synchronization succeed!\r\n", __FILE__, __LINE__);
|
||||
prefs.putUInt("last_sync", (uint32_t)now); // save epoch time of last sync
|
||||
} else {
|
||||
Serial.printf("%s [%d]: Time synchronization failed...\r\n", __FILE__, __LINE__);
|
||||
}
|
||||
|
||||
Serial.printf("%s [%d]: Current time - %s\r\n", __FILE__, __LINE__, asctime(&timeinfo));
|
||||
|
||||
prefs.end();
|
||||
return sync_status;
|
||||
} /* setClock() */
|
||||
|
||||
void TRMNL::getSensorSamples()
|
||||
{
|
||||
long lSleepTime;
|
||||
lSleepTime = (_bCO2) ? 5000 : 1000; // sleep for 5 or 1 seconds depending on the sensor type
|
||||
lSleepTime -= (millis() - _lSensorTime); // how much time passed since we triggered samples?
|
||||
if (lSleepTime > 0) {
|
||||
delay(lSleepTime);
|
||||
}
|
||||
if (_bCO2 && scd41.getSample() == SCD41_SUCCESS) {
|
||||
time((time_t *)&_u32SensorEpoch); // get the UTC epoch time that the sample was captured
|
||||
_iCO2 = scd41.co2();
|
||||
_iTemperature = scd41.temperature();
|
||||
_iHumidity = scd41.humidity();
|
||||
}
|
||||
if (_iSensorType >= 0) {
|
||||
BBT_SAMPLE bbts;
|
||||
if (bbt.getSample(&bbts) == BBT_SUCCESS) {
|
||||
uint32_t u32Caps = bbt.caps();
|
||||
time((time_t *)&_u32SensorEpoch); // get the UTC epoch time that the same was captured
|
||||
if (u32Caps & BBT_CAP_TEMPERATURE) {
|
||||
_iTemperature = bbts.temperature;
|
||||
}
|
||||
if (u32Caps & BBT_CAP_HUMIDITY) {
|
||||
_iHumidity = bbts.humidity;
|
||||
}
|
||||
if (u32Caps & BBT_CAP_PRESSURE) {
|
||||
_iPressure = bbts.pressure;
|
||||
}
|
||||
Serial.printf("%s [%d]: Got bb_temperature sample: Temp = %d.%dC\r\n", __FILE__, __LINE__, _iTemperature/10, _iTemperature % 10);
|
||||
} else {
|
||||
Serial.printf("%s [%d]: bb_temperature sample failed\r\n", __FILE__, __LINE__);
|
||||
}
|
||||
bbt.stop(); // turn off the sensor to conserve power
|
||||
}
|
||||
} /* getSensorSamples() */
|
||||
|
||||
int TRMNL::getAPI(const char *szAPIKey, const char *szURL, float fVoltage)
|
||||
{
|
||||
int rc = TRMNL_ERROR;
|
||||
bool bTimeValid = false;
|
||||
|
||||
if (_bTimeSync) {
|
||||
bTimeValid = setClock(); // Get the current time if not already set
|
||||
}
|
||||
https.begin(szURL);
|
||||
https.setTimeout(15000);
|
||||
https.setConnectTimeout(15000);
|
||||
if ((_bCO2 || _iSensorType >= 0) && bTimeValid) { // Add HTTP headers for the sensor samples
|
||||
char *szTemp, szPart[128];
|
||||
// Sensors are detected and active, get the latest sample value(s)
|
||||
uint32_t u32SampleTime;
|
||||
time((time_t *)&u32SampleTime); // get the UTC epoch time that the samples were captured
|
||||
getSensorSamples();
|
||||
szTemp = (char *)malloc(1024); // make sure we have enough space, but don't use the stack because it's small
|
||||
if (_iCO2 != 0) { // valid data from SCD4x for CO2, Temperature and Humidity
|
||||
// create the multi-value string to pass as a HTTP header
|
||||
sprintf(szTemp, "make=Sensirion;model=SCD41;kind=carbon_dioxide;value=%d;unit=parts_per_million;created_at=%lu,make=Sensirion;model=SCD41;kind=temperature;value=%f;unit=celsius;created_at=%lu,make=Sensirion;model=SCD41;kind=humidity;value=%d;unit=percent;created_at=%lu", _iCO2, u32SampleTime, (float)_iTemperature / 10.0f, u32SampleTime, _iHumidity, u32SampleTime);
|
||||
Serial.printf("%s [%d] Adding SCD41 data to api request: CO2: %d, Temp: %d.%dC, Humidity: %d%%", __FILE__, __LINE__, _iCO2, _iTemperature/10, _iTemperature % 10, _iHumidity);
|
||||
}
|
||||
if (_iSensorType >= 0) { // we have data from another bb_temperature supported sensor too; add it
|
||||
if (_iCO2 != 0) {
|
||||
strcat(szTemp, ","); // separate from CO2 data
|
||||
} else {
|
||||
szTemp[0] = 0;
|
||||
}
|
||||
Serial.printf("%s [%d] Adding bb_temperature data to api request: pressure: %d, Temp: %d.%dC, Humidity: %d%%", __FILE__, __LINE__, _iPressure, _iTemperature/10, _iTemperature % 10, _iHumidity);
|
||||
sprintf(szPart, "make=%s;model=%s;kind=temperature;value=%f;unit=celsius;created_at=%lu",szMakers[_iSensorType], szDevices[_iSensorType], (float)_iTemperature / 10.0f, u32SampleTime);
|
||||
strcat(szTemp, szPart);
|
||||
if (_iHumidity > 0) { // add humidity
|
||||
sprintf(szPart, ",make=%s;model=%s;kind=humidity;value=%d;unit=percent;created_at=%lu",szMakers[_iSensorType], szDevices[_iSensorType], _iHumidity, u32SampleTime);
|
||||
strcat(szTemp, szPart);
|
||||
}
|
||||
if (_iPressure > 0) {
|
||||
sprintf(szPart, ",make=%s;model=%s;kind=pressure;value=%d;unit=hectopascal;created_at=%lu",szMakers[_iSensorType], szDevices[_iSensorType], _iPressure, u32SampleTime);
|
||||
strcat(szTemp, szPart);
|
||||
}
|
||||
}
|
||||
if (_iCO2 != 0 || _iSensorType >= 0) {
|
||||
https.addHeader("SENSORS", szTemp);
|
||||
} else {
|
||||
Serial.printf("%s [%d] Sensor data not available", __FILE__, __LINE__);
|
||||
}
|
||||
free(szTemp);
|
||||
} // add sensor info to HTTP headers
|
||||
// add the HTTP headers
|
||||
// https.addHeader("ID", inputs.macAddress);
|
||||
https.addHeader("Content-Type", "application/json");
|
||||
https.addHeader("Access-Token", szAPIKey);
|
||||
// https.addHeader("Refresh-Rate", String(inputs.refreshRate));
|
||||
https.addHeader("Battery-Voltage", String(fVoltage));
|
||||
// https.addHeader("FW-Version", inputs.firmwareVersion);
|
||||
https.addHeader("Model", "byod");
|
||||
https.addHeader("RSSI", String(WiFi.RSSI()));
|
||||
https.addHeader("Width", String(_iWidth));
|
||||
https.addHeader("Height", String(_iHeight));
|
||||
_httpCode = https.GET();
|
||||
// httpCode will be negative on error
|
||||
Serial.printf("https GET returned: %d\n", _httpCode);
|
||||
if (_httpCode >= 200 && _httpCode < 300) { // success, get payload
|
||||
String payload = https.getString();
|
||||
size_t size = https.getSize();
|
||||
Serial.printf("Content size: %d\n", size);
|
||||
Serial.printf("Payload: %s\n", payload.c_str());
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, payload);
|
||||
_status = doc["status"];
|
||||
_image_url = doc["image_url"] | "";
|
||||
_refresh_rate = doc["refresh_rate"];
|
||||
rc = TRMNL_SUCCESS;
|
||||
} else {
|
||||
_refresh_rate = 900; // default to 15 minutes
|
||||
_status = API_STATUS_INVALID;
|
||||
}
|
||||
https.end();
|
||||
return rc;
|
||||
} /* getAPI() */
|
||||
|
||||
int TRMNL::getImage(uint8_t **pBuffer, int *pSize)
|
||||
{
|
||||
int rc = TRMNL_ERROR;
|
||||
|
||||
Serial.println("TRMNL getImage()");
|
||||
|
||||
if (_status == API_STATUS_OK && _image_url.length() > 0) {
|
||||
https.begin(_image_url);
|
||||
https.setTimeout(15000);
|
||||
https.setConnectTimeout(15000);
|
||||
_httpCode = https.GET();
|
||||
Serial.printf("%s [%d]: [HTTPS] GET... code: %d\r\n", __FILE__, __LINE__, _httpCode);
|
||||
Serial.printf("%s [%d]: RSSI: %d\r\n", __FILE__, __LINE__, WiFi.RSSI());
|
||||
if (_httpCode == HTTP_CODE_OK) {
|
||||
Serial.printf("%s [%d]: Content size: %d\r\n", __FILE__, __LINE__, https.getSize());
|
||||
String payload = https.getString();
|
||||
*pSize = payload.length();
|
||||
if (*pSize) {
|
||||
Serial.println("Got the image!");
|
||||
*pBuffer = _pImage = (uint8_t *)malloc(*pSize);
|
||||
memcpy(*pBuffer, payload.c_str(), *pSize);
|
||||
rc = TRMNL_SUCCESS;
|
||||
}
|
||||
}
|
||||
https.end();
|
||||
}
|
||||
return rc;
|
||||
} /* getImage() */
|
||||
|
||||
void TRMNL::disconnectWiFi(void)
|
||||
{
|
||||
Serial.println("TRMNL disconnectWiFi()");
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
WiFi.disconnect(true, true, 500);
|
||||
}
|
||||
} /* disconnectWiFi() */
|
||||
|
||||
void TRMNL::sleep()
|
||||
{
|
||||
Serial.println("TRMNL sleep()");
|
||||
esp_sleep_enable_timer_wakeup((uint64_t)_refresh_rate * 1000000L);
|
||||
// If the user defined a button to wake us up (besides the timer)
|
||||
if (_wake_gpio != 255) {
|
||||
#if CONFIG_IDF_TARGET_ESP32
|
||||
esp_sleep_enable_ext1_wakeup(1 << _wake_gpio, ESP_EXT1_WAKEUP_ALL_LOW);
|
||||
#elif CONFIG_IDF_TARGET_ESP32C3
|
||||
esp_deep_sleep_enable_gpio_wakeup(1 << _wake_gpio, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
#elif CONFIG_IDF_TARGET_ESP32S3
|
||||
esp_sleep_enable_ext0_wakeup((gpio_num_t)_wake_gpio, 0);
|
||||
#endif
|
||||
}
|
||||
esp_deep_sleep_start();
|
||||
} /* sleep() */
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// TRMNL library
|
||||
// Written by Larry Bank
|
||||
// project started March 25, 2026
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2026 TRMNL LLC
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===========================================================================
|
||||
//
|
||||
#ifndef __TRMNL_LIB__
|
||||
#define __TRMNL_LIB__
|
||||
|
||||
#ifdef __LINUX__
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/spi/spidev.h>
|
||||
#include <linux/i2c-dev.h>
|
||||
#include <i2c/smbus.h>
|
||||
#include <time.h>
|
||||
|
||||
#else // !LINUX
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
#endif // ARDUINO
|
||||
#endif // !__LINUX__
|
||||
|
||||
#define API_STATUS_OK 0
|
||||
#define API_STATUS_NOT_REGISTERED 202
|
||||
#define API_STATUS_RESET 500
|
||||
#define API_STATUS_INVALID 999
|
||||
|
||||
enum {
|
||||
TRMNL_SUCCESS = 0,
|
||||
TRMNL_ERROR
|
||||
};
|
||||
|
||||
class TRMNL
|
||||
{
|
||||
public:
|
||||
TRMNL() {_iTemperature = _iHumidity = _iCO2 = _iPressure = 0;}
|
||||
~TRMNL() {}
|
||||
|
||||
void setSensorBus(uint8_t sda, uint8_t scl);
|
||||
void setDisplaySize(int w, int h) {_iWidth = w; _iHeight = h;}
|
||||
bool connectWiFi(const char *ssid, const char *pw);
|
||||
int getAPI(const char *szAPIKey, const char *szURL = "https://trmnl.app/api/display", float fVoltage = 4.2f);
|
||||
uint64_t getAPIStatus() {return _status;}
|
||||
void setWakeButton(uint8_t u8Wake) {_wake_gpio = u8Wake;}
|
||||
int getImage(uint8_t **pu8Buffer, int *piSize);
|
||||
void disconnectWiFi(void);
|
||||
void freeImage() {free(_pImage); _pImage = nullptr;}
|
||||
int getHTTPCode() {return _httpCode;}
|
||||
void sleep();
|
||||
float getTemperature() {return (float)_iTemperature / 10.0f;}
|
||||
int getPressure() {return _iPressure;}
|
||||
int getHumidity() {return _iHumidity;}
|
||||
int getCo2() {return _iCO2;}
|
||||
int getSleepTime() {return _refresh_rate;}
|
||||
void setTimeSync(bool bSync) {_bTimeSync = bSync;}
|
||||
protected:
|
||||
bool setClock();
|
||||
void getSensorSamples();
|
||||
|
||||
private:
|
||||
bool _bCO2, _bTimeSync = false;
|
||||
int _iSensorType;
|
||||
long _lSensorTime;
|
||||
uint32_t _u32SensorEpoch;
|
||||
int _iWidth = 800; // default to OG size
|
||||
int _iHeight = 480;
|
||||
int _iHumidity, _iTemperature, _iCO2, _iPressure, _httpCode;
|
||||
String _image_url;
|
||||
uint8_t *_pImage;
|
||||
uint64_t _status = API_STATUS_INVALID;
|
||||
uint64_t _refresh_rate = 30; // default to 30 seconds
|
||||
uint8_t _wake_gpio = 255;
|
||||
};
|
||||
|
||||
#endif // __TRMNL_LIB__
|
||||
Reference in New Issue
Block a user