Browse Source

ZeroWatch public repo initial commit

tags/v0.2.0.5
Ryan Joseph 1 year ago
commit
9e7284a348
10 changed files with 1374 additions and 0 deletions
  1. +14
    -0
      README.md
  2. +53
    -0
      scripts/otp-generate.pl
  3. +704
    -0
      zero_watch.ino
  4. +10
    -0
      zw_common.cpp
  5. +27
    -0
      zw_common.h
  6. +21
    -0
      zw_logging.h
  7. +226
    -0
      zw_provision.cpp
  8. +40
    -0
      zw_provision.h
  9. +182
    -0
      zw_redis.cpp
  10. +97
    -0
      zw_redis.h

+ 14
- 0
README.md View File

@@ -0,0 +1,14 @@
# ZeroWatch

A highly-configurable ESP-32-based Redis-watcher and TM1637-displayer, with OTA capability.

Named as it was originally started to monitor "zero", my RPi Zero W.

Currently running things like [this](https://twitter.com/rpjios/status/1155609352528486400) and [this](https://twitter.com/rpjios/status/1100077092287344642):

![version 2](https://pbs.twimg.com/media/EAmMSaPVUAEKYcl?format=jpg&name=small)
![version 1](https://pbs.twimg.com/media/D0RCGcvVAAArx5x?format=jpg&name=small)

## Provisioning

Stuff and stuff...

+ 53
- 0
scripts/otp-generate.pl View File

@@ -0,0 +1,53 @@
#!/usr/bin/perl

# usage, either:
# ./otp-generate.pl [targetHostname] [redisPassword] (redisHost)
# - or -
# ./otp-generate.pl [targetHostname] -D [currentLc]
# (the "direct" version ^)

my $targetHost = shift;
my $redisPassword = shift;
my $redisHost = shift || '192.168.1.252';

my $currentLc = -1;
my $lookAhead = 0;

if ($redisPassword eq '-D' && int($redisHost) > 0) {
$currentLc = int($redisHost);
}
else {
$currentLc = int(`redis-cli -h $redisHost -a '$redisPassword' get $targetHost:heartbeat 2> /dev/null`);
}

if ($currentLc <= 0) {
print "Bad current LC ($currentLc)!\n";
exit(-1);
}

my $detected = undef;
$_=`cat ../zero_watch.ino`;
$detected = $1, if (/OTA_WINDOW_MINUTES\s+(\d+)/g);
my $div = (int(1e6) * 60) * ($detected || 2);

if ($currentLc < $div) {
print "Unit hasn't been running long enough (need $div, have $currentLc), please wait...\n";
exit(0);
}

print "[OTP] I: $currentLc\n";
$currentLc = int($currentLc / $div);
print "[OTP] 0: $currentLc\n";

my @fudgeTable = (42, 69, 3, 18, 25, 12, 51, 93, 54, 76);
$currentLc += $fudgeTable[($currentLc % scalar(@fudgeTable))];
print "[OTP] D: $currentLc\n";

for ($i = 0; $i < length($targetHost); $i++) {
$currentLc += int(ord(substr($targetHost, $i, 1)));
print "[OTP] $i: $currentLc (+= " . (int(ord(substr($targetHost, $i, 1)))) . ")\n";
}

$currentLc = $currentLc & 0xFFFF;
print "[OTP] F: $currentLc\n";
print "$currentLc\n";

+ 704
- 0
zero_watch.ino View File

@@ -0,0 +1,704 @@
// zero_watch.ino
// (C) Ryan Joseph, 2019 <ryan@electricsheep.co>

#include <WiFiClient.h>
#include <WiFi.h>
#include <Update.h>
#include <HTTPClient.h>
#include <Redis.h>
#include <TM1637Display.h>
#include <ArduinoJson.h>
#include <EEPROM.h>

#include "zw_common.h"
#include "zw_logging.h"
#include "zw_redis.h"
#include "zw_provision.h"

#define OTA_RESET_DELAY 5
#define OTA_UPDATE_PRCNT_REPORT 10
#define OTP_WINDOW_MINUTES 2
#define CONTROL_POINT_SEP_CHAR '#'
#define LED_BLTIN_H LOW
#define LED_BLTIN_L HIGH
#define LED_BLTIN 2
#define SER_BAUD 115200
#define DEF_BRIGHT 0
#define HB_CHECKIN 5
#define LAT_FUNC micros
#if DEBUG
#define DEF_REFRESH 20
#else
#define DEF_REFRESH 240
#endif

class DisplaySpec;

#define EXEC_ALL_DISPS(EXEC_ME) \
do \
{ \
for (DisplaySpec *walk = gDisplays; walk->clockPin != -1 && walk->dioPin != -1; walk++) \
walk->disp->EXEC_ME; \
} while (0)

#define EXEC_WITH_EACH_DISP(EFUNC) \
do \
{ \
for (DisplaySpec *walk = gDisplays; walk->clockPin != -1 && walk->dioPin != -1; walk++) \
EFUNC(walk->disp); \
} while (0)

struct InfoSpec
{
const char *listKey;
int startIdx;
int endIdx;
double lastTs;
int lastVal;
std::function<int(int)> adjFunc;
std::function<void(DisplaySpec *)> dispFunc;
};

struct DisplaySpec
{
int clockPin;
int dioPin;
TM1637Display *disp;
InfoSpec spec;
};

ZWAppConfig gConfig = {
.brightness = DEF_BRIGHT,
.refresh = DEF_REFRESH,
.debug = DEBUG,
.publishLogs = false,
.pauseRefresh = false};
ZWRedis *gRedis;
DisplaySpec *gDisplays;
void (*gPublishLogsEmit)(const char *fmt, ...);
unsigned long __lc = 0;
int _last_free = 0;
unsigned long gUDRA = 0;
unsigned long immediateLatency = 0;
StaticJsonBuffer<1024> jsonBuf;

int noop(int a) { return a; }

void d_def(DisplaySpec *d) { d->disp->showNumberDec(d->spec.lastVal); }

static uint8_t degFSegs[] = {99, 113};
static uint8_t fSeg[] = {113};
static uint8_t prcntSeg[] = {99, 92};
void d_tempf(DisplaySpec *d)
{
if (d->spec.lastVal < 10000)
{
d->disp->showNumberDecEx(d->spec.lastVal, 0, false);
d->disp->setSegments(degFSegs, 2, 2);
}
else
{
d->disp->showNumberDecEx(d->spec.lastVal / 100, 0, false, 3);
d->disp->setSegments(fSeg, 1, 3);
}
}

void d_humidPercent(DisplaySpec *d)
{
d->disp->showNumberDecEx(d->spec.lastVal, 0, false);
d->disp->setSegments(prcntSeg, 2, 2);
}

DisplaySpec gDisplays_AMINI[] = {
{33, 32, nullptr, {"zero:sensor:BME280:temperature:.list", 0, 11, 0.0, 0, noop, d_tempf}},
{26, 25, nullptr, {"zero:sensor:BME280:humidity:.list", 0, 11, 0.0, 0, noop, d_humidPercent}},
{18, 19, nullptr, {"zero:sensor:BME280:pressure:.list", 0, 5, 0.0, 0, [](int i) { return i / 100; }, d_def}},
{-1, -1, nullptr, {nullptr, -1, -1, -1.0, -1, noop, d_def}}};

DisplaySpec gDisplays_EZERO[] = {
{33, 32, nullptr, {"zero:sensor:BME280:pressure:.list", 0, 5, 0.0, 0, [](int i) { return i / 100; }, d_def}},
{18, 19, nullptr, {"zero:sensor:BME280:temperature:.list", 0, 11, 0.0, 0, noop, d_tempf}},
{26, 25, nullptr, {"zed:sensor:SPS30:mc_2p5:.list", 0, 5, 0.0, 0, [](int i) { return i / 100; }, d_def}},
{13, 14, nullptr, {"zero:sensor:BME280:humidity:.list", 0, 11, 0.0, 0, noop, d_humidPercent}},
{-1, -1, nullptr, {nullptr, -1, -1, -1.0, -1, noop, d_def}}};

void blink(int d = 50)
{
digitalWrite(LED_BLTIN, LED_BLTIN_H);
delay(d);
digitalWrite(LED_BLTIN, LED_BLTIN_L);
delay(d);
digitalWrite(LED_BLTIN, LED_BLTIN_H);
}

struct AnimStep
{
int digit;
int bits;
};

AnimStep full_loop[] = {{0, 1}, {1, 1}, {2, 1}, {3, 1}, {3, 3}, {3, 7},
{3, 15}, {2, 9}, {1, 9}, {0, 9}, {0, 25}, {0, 57}, {-1, -1}};

AnimStep light_loop[] = {{0, 1}, {1, 1}, {2, 1}, {3, 1}, {3, 2}, {3, 4},
{3, 8}, {2, 8}, {1, 8}, {0, 8}, {0, 16}, {0, 32}, {-1, -1}};

void run_animation(TM1637Display *d, AnimStep *anim, bool cE = false, int s = 0)
{
uint8_t v[] = {0, 0, 0, 0};
for (AnimStep *w = anim; w->bits != -1 && w->digit != -1; w++)
{
if (cE)
bzero(v, 4);
v[w->digit] = w->bits;
d->setSegments(v);
if (s)
delay(s);
}
}

bool tmdisplay_init()
{
zlog("Initializing displays with brightness level %d\n", gConfig.brightness);
DisplaySpec *spec = gDisplays;
for (; spec->clockPin != -1 && spec->dioPin != -1; spec++)
{
zlog("Setting up display #%d with clock=%d DIO=%d\n",
(int)(spec - gDisplays), spec->clockPin, spec->dioPin);
spec->disp = new TM1637Display(spec->clockPin, spec->dioPin);
spec->disp->clear();
spec->disp->setBrightness(gConfig.brightness);
run_animation(spec->disp, full_loop, false, 5);
}

if (gConfig.debug)
{
EXEC_ALL_DISPS(showNumberDecEx((int)(walk - gDisplays), 0,
false, 4 - (int)(walk - gDisplays), (int)(walk - gDisplays)));
delay(2000);
}

return true;
}

bool wifi_init()
{
dprint("Disabling WiFi AP\n");
WiFi.mode(WIFI_MODE_STA);
WiFi.enableAP(false);

auto bstat = WiFi.begin(EEPROMCFG_WiFiSSID, EEPROMCFG_WiFiPass);
dprint("Connecting to to '%s'...\n", EEPROMCFG_WiFiSSID);
dprint("WiFi.begin() -> %d\n", bstat);

run_animation(gDisplays[0].disp, light_loop, true);
gDisplays[0].disp->clear();
gDisplays[0].disp->showNumberHexEx(0xffff, 64, true);
// TODO: timeout!
int _c = 0;
while (WiFi.status() != WL_CONNECTED)
{
gDisplays[0].disp->showNumberHexEx((++_c << 8) + 0xff, 64, true);
}

zlog("WiFi adapter %s connected to '%s' as %s\n", WiFi.macAddress().c_str(),
EEPROMCFG_WiFiSSID, WiFi.localIP().toString().c_str());
gDisplays[0].disp->showNumberHexEx(0xFF00 | WiFi.status(), 64, false);

return true;
}

void updateDisplay(DisplaySpec *disp)
{
if (gConfig.debug)
run_animation(disp->disp, full_loop);

auto __s = LAT_FUNC();
auto lrVec = gRedis->getRange(disp->spec.listKey, disp->spec.startIdx, disp->spec.endIdx);
immediateLatency = LAT_FUNC() - __s;
auto newUDRA = gUDRA == 0 ? immediateLatency : (gUDRA + immediateLatency) / 2;
auto deltaUDRA = newUDRA - gUDRA;
gUDRA = newUDRA;

if (lrVec.size())
{
double acc = 0.0;
for (auto lrStr : lrVec)
{
if (lrStr.length() < 256)
{
jsonBuf.clear();
JsonArray &jsRoot = jsonBuf.parseArray(lrStr.c_str());
disp->spec.lastTs = (double)jsRoot[0];
acc += (double)jsRoot[1];
}
}

if (gConfig.debug)
run_animation(disp->disp, light_loop, true);

disp->spec.lastVal = disp->spec.adjFunc((int)((acc * 100.0) / lrVec.size()));
disp->spec.dispFunc(disp);
zlog("[%s] count %d val %d immLat %lu gUDRA %lu (delta %ld)\n",
disp->spec.listKey, lrVec.size(), disp->spec.lastVal, immediateLatency, gUDRA, deltaUDRA);
}
}

void demoForDisp(TM1637Display *disp)
{
run_animation(disp, full_loop);
run_animation(disp, light_loop);
run_animation(disp, full_loop);
run_animation(disp, light_loop);
run_animation(disp, full_loop);
}

bool processGetValue(String &imEmit, ZWRedisResponder &responder)
{
bool matched = true;
imEmit.toLowerCase();

if (imEmit.startsWith("ip") || imEmit.endsWith("address"))
{
responder.setValue("%s", WiFi.localIP().toString().c_str());
}
else if (imEmit.startsWith("mem"))
{
auto cur_free = ESP.getFreeHeap();
responder.setValue(
"{ \"current\": %d, \"last\": %d, \"delta\": %d, \"heap\": %d }",
cur_free, _last_free, cur_free - _last_free, ESP.getHeapSize());
}
else if (imEmit.startsWith("up"))
{
responder.setExpire(5);
responder.setValue("%s", String(millis() / 1000).c_str());
}
else if (imEmit.startsWith("ver"))
{
responder.setValue(
"{ \"version\": \"%s\", \"sketchMD5\": \"%s\", "
"\"sketchSize\": %d, \"sdk\": \"%s\", \"chipRev\": %d}",
ZEROWATCH_VER, ESP.getSketchMD5().c_str(), ESP.getSketchSize(),
ESP.getSdkVersion(), ESP.getChipRevision());
}
else if (imEmit.equals("demo"))
{
dprint("Demo! Bleep bloop!\n");
zlog("Demo! Bleep bloop!\n");
EXEC_WITH_EACH_DISP(demoForDisp);
}
else if (imEmit.equals("latency"))
{
responder.setValue("{ \"immediate\": %d, \"rollingAvg\": %d }", immediateLatency, gUDRA);
}
else
{
matched = false;
}

return matched;
}

bool ctrlPoint_reset()
{
dprint("[CMD] RESETING!\n");
zlog("[CMD] RESETING!");
gRedis->clearControlPoint();
ESP.restart();
return true; // never reached
}

bool ctrlPoint_clearbootcount()
{
dprint("[CMD] Clearing boot count!\n");
zlog("[CMD] Clearing boot count!\n");
return gRedis->incrementBootcount(true) == 0;
}

bool processControlPoint(String &imEmit, ZWRedisResponder &responder)
{
auto sepIdx = imEmit.indexOf(CONTROL_POINT_SEP_CHAR);

if (sepIdx == -1 && sepIdx < imEmit.length())
{
zlog("ERROR: control point write is malformed ('%s')\n", imEmit.c_str());
return false;
}

auto cmd = imEmit.substring(0, sepIdx);
auto otp = (uint16_t)imEmit.substring(sepIdx + 1).toInt();

zlog("Control point has '%s' with OTP %d...\n", cmd.c_str(), otp);

if (!otpCheck(otp))
{
zlog("ERROR: processControlPoint: Not authorized!\n");
return false;
}

zlog("Control point is authorized.\n");
bool (*ctrlPointFunc)() = NULL;

if (cmd.equals("reset"))
{
ctrlPointFunc = ctrlPoint_reset;
}
else if (cmd.equals("clearbootcount"))
{
ctrlPointFunc = ctrlPoint_clearbootcount;
}

if (ctrlPointFunc)
{
zlog("Control point command '%s' is valid: executing.\n", cmd.c_str());
return ctrlPointFunc();
}
else
{
zlog("WARNING: unknown control point command '%s'\n", cmd.c_str());
}

return true;
}

void updateProg(size_t s1, size_t s2)
{
static int lastUpdate = 0;
auto curPercent = ((double)s1 / s2) * 100.0;
if ((unsigned)curPercent >= lastUpdate + OTA_UPDATE_PRCNT_REPORT)
{
lastUpdate = (unsigned)curPercent;
dprint("%d.. %s", lastUpdate, (lastUpdate == 100 ? "\n" : ""));
zlog("OTA update progress: %0.2f%% (%d)\n", curPercent, s1);
}
}

bool runUpdate(const char *url, const char *md5, size_t sizeInBytes)
{
HTTPClient http;
if (http.begin(url))
{
auto code = http.GET();
if (code > 0)
{
auto dataStream = http.getStream();
auto avail = dataStream.available();
if (avail == 0)
{
zlog("ERROR: no bytes available!\n");
return false;
}

if (Update.begin(sizeInBytes))
{
Update.onProgress(updateProg);
Update.setMD5(md5);
dprint("OTA start szb=%d\n", sizeInBytes);
auto updateTook = Update.writeStream(dataStream);
if (updateTook == sizeInBytes && !Update.hasError())
{
if (!gRedis->postCompletedUpdate())
{
zlog("WARNING: unable to delete update key!\n");
}

if (!Update.end())
{
zlog("UPDATE END FAILED!? WTF mate\n");
Update.abort();
return false;
}

return true;
}
else
{
zlog("UPDATE FAILED: %d\n", Update.getError());
Update.abort();
}
}
else
{
zlog("UPDATE couldn't start: %d\n", Update.getError());
Update.abort();
}
}
else
{
zlog("HTTPClient.get() failed: %d\n", code);
}
}
else
{
zlog("HTTPClient.begin() failed\n");
}

return false;
}

// the fudge table is use strictly to make a linear sequence appear
// non-linear over a short observation time period. security by obscurity ftw!
static const uint8_t fudgeTable[] = {42, 69, 3, 18, 25, 12, 51, 93, 54, 76};
static const uint8_t fudgeTableLen = 10;

// to generate anew, use ./scripts/otp-generate.pl
bool otpCheck(uint16_t otp)
{
dprint("[OTP] A: %d\n", otp);
dprint("[OTP] I: %ld\n", micros());
auto div = (unsigned long)1e6 * 60 * OTP_WINDOW_MINUTES;
auto now = micros();

if (now < div)
{
dprint("Can't calculate OTPs yet, must be running for at least %d minutes\n", OTP_WINDOW_MINUTES);
return false;
}

auto internalChecker = micros() / div;
dprint("[OTP] 0: %ld\n", internalChecker);

internalChecker += fudgeTable[internalChecker % fudgeTableLen];
dprint("[OTP] D: %ld\n", internalChecker);

for (int i = 0; i < gHostname.length(); i++)
{
internalChecker += gHostname.charAt(i);
dprint("[OTP] %d: %ld (+= %d)\n", i, internalChecker, (int)gHostname.charAt(i));
}

dprint("[OTP] F: %d\n", (uint16_t)internalChecker);
return (uint16_t)internalChecker == otp;
}

bool processUpdate(String &updateJson, ZWRedisResponder &responder)
{
JsonObject &updateObj = jsonBuf.parseObject(updateJson.c_str());

if (updateObj.success())
{
auto url = updateObj.get<char *>("url");
auto md5 = updateObj.get<char *>("md5");
auto szb = updateObj.get<int>("size");
auto otp = updateObj.get<unsigned long>("otp");

if (!otpCheck(otp))
{
zlog("ERROR: Not authorized\n");
return false;
}

zlog("Accepted OTA OTP '%ld'\n", otp);

if (url && md5 && szb > 0)
{
auto fqUrlLen = strlen(EEPROMCFG_OTAHost) + strlen(url) + 2;
char *fqUrl = (char *)malloc(fqUrlLen);
bzero(fqUrl, fqUrlLen);

auto fqWrote = snprintf(fqUrl, fqUrlLen, "%s/%s", EEPROMCFG_OTAHost, url);
if (fqWrote != fqUrlLen - 1)
zlog("WARNING: wrote %d of expected %d bytes for fqUrl\n", fqWrote, fqUrlLen);

zlog("Starting OTA update of %0.2fKB\n", (szb / 1024.0));
zlog("Image source (md5=%s):\n\t%s\n", md5, fqUrl);
if (runUpdate(fqUrl, md5, szb))
{
zlog("OTA update wrote successfully! Restarting in %d seconds...\n", OTA_RESET_DELAY);
delay(OTA_RESET_DELAY * 1000);
ESP.restart();
return true; // never reached
}
else
{
zlog("ERROR: OTA failed! %d\n", Update.getError());
}
}
else
{
zlog("ERROR: Bad OTA params (%p, %p, %d)\n", url, md5, szb);
}
}
else
{
zlog("ERROR: failed to parse update\n");
}

return false;
}

void redis_publish_logs_emit(const char *fmt, ...)
{
// this function should never be called before gRedis is valid
zwassert(gRedis != NULL);

char buf[1024];
bzero(buf, 1024);
va_list args;
va_start(args, fmt);
vsprintf(buf, fmt, args);
auto len = strlen(buf);

if (buf[len - 1] == '\n')
buf[len - 1] = '\0';

gRedis->publishLog(buf);
va_end(args);
}

void readConfigAndUserKeys()
{
auto curCfg = gRedis->readConfig();

if (curCfg.brightness >= 0 && curCfg.brightness < 8 &&
curCfg.brightness != gConfig.brightness)
{
zlog("Read new brightness level: %d\n", curCfg.brightness);
gConfig.brightness = curCfg.brightness;
EXEC_ALL_DISPS(setBrightness(gConfig.brightness));
}

if (curCfg.refresh >= 5 && curCfg.refresh != gConfig.refresh)
{
zlog("Read new refresh rate: %d\n", curCfg.refresh);
gConfig.refresh = curCfg.refresh;
}

if (curCfg.debug != gConfig.debug)
{
zlog("Read new debug setting: %sabled\n", curCfg.debug ? "en" : "dis");
gConfig.debug = curCfg.debug;
}

if (curCfg.publishLogs != gConfig.publishLogs)
{
zlog("Read new publishLogs setting: %sabled\n", curCfg.publishLogs ? "en" : "dis");
gConfig.publishLogs = curCfg.publishLogs;
}

if (curCfg.pauseRefresh != gConfig.pauseRefresh)
{
gConfig.pauseRefresh = curCfg.pauseRefresh;
zlog("REFRESH %s\n", gConfig.pauseRefresh ? "PAUSED" : "RESUMED");
dprint("REFRESH %s\n", gConfig.pauseRefresh ? "PAUSED" : "RESUMED");
}

if (!gConfig.pauseRefresh)
{
gRedis->handleUserKey(":config:getValue", processGetValue);
gRedis->handleUserKey(":config:controlPoint", processControlPoint);
gRedis->handleUserKey(":config:update", processUpdate);
}
}

void heartbeat()
{
static uint64_t __hb_count = 0;
if (gRedis)
{
if (!gRedis->heartbeat(gConfig.refresh * 5))
{
zlog("WARNING: heartbeat failed!\n");
}

if ((__hb_count++ % HB_CHECKIN))
{
gRedis->checkin(__lc, WiFi.localIP().toString().c_str(), immediateLatency, gUDRA, gConfig.refresh * 5);
}
}
}

void tick(bool forceUpdate = false)
{
if (gConfig.pauseRefresh)
if (!forceUpdate)
return;

zlog("Awake at us=%lu tick=%ld\n", micros(), __lc);

for (DisplaySpec *w = gDisplays; w->clockPin != -1 && w->dioPin != -1; w++)
updateDisplay(w);

_last_free = ESP.getFreeHeap();
}

void loop()
{
if (!(__lc++ % gConfig.refresh))
{
readConfigAndUserKeys();
heartbeat();
tick();
}

if (!(__lc % 5))
{
blink(15);
delay(5);
blink(15);
}
dprint("%c%s", !(__lc % 5) ? '|' : '.', __lc % gConfig.refresh ? "" : "\n");
delay(900);
}

void setup()
{
pinMode(LED_BLTIN, OUTPUT);
Serial.begin(SER_BAUD);
verifyProvisioning();

if (gHostname.equals("ezero"))
{
gDisplays = gDisplays_EZERO;
}
else if (gHostname.equals("amini"))
{
gDisplays = gDisplays_AMINI;
}

zlog("\n%s v" ZEROWATCH_VER " starting...\n", gHostname.c_str());

if (tmdisplay_init() && wifi_init())
{
auto verNum = String(ZEROWATCH_VER);
verNum.replace(".", "");
gDisplays[0].disp->showNumberDec(verNum.toInt(), true);

delay(2000);

ZWRedisHostConfig redisConfig = {
.host = EEPROMCFG_RedisHost,
.port = EEPROMCFG_RedisPort,
.password = EEPROMCFG_RedisPass};

gRedis = new ZWRedis(gHostname, redisConfig);

if (gRedis->connect())
{
zlog("Redis connection established, reading config...\n");
readConfigAndUserKeys();

if (gConfig.pauseRefresh)
{
zlog("Running tick 0 forcefully because refresh is paused at init\n");
tick(true);
}

zlog("Fully initialized! (debug %sabled)\n", gConfig.debug ? "en" : "dis");

if (gConfig.debug)
delay(5000);

gPublishLogsEmit = redis_publish_logs_emit;

zlog("Boot count: %d\n", gRedis->incrementBootcount());
zlog("%s v" ZEROWATCH_VER " up & running\n", gHostname.c_str());
}
else
{
zlog("ERROR: redis init failed!");
}
}
}

+ 10
- 0
zw_common.cpp View File

@@ -0,0 +1,10 @@
#include "zw_common.h"
#include <Arduino.h>

void __haltOrCatchFire()
{
while (1)
{
delay(1);
}
}

+ 27
- 0
zw_common.h View File

@@ -0,0 +1,27 @@
#ifndef __ZW_COMMON__H__
#define __ZW_COMMON__H__

#define ZEROWATCH_VER "0.2.0.4"
#define DEBUG 1

struct ZWAppConfig {
int brightness;
int refresh;
bool debug;
bool publishLogs;
bool pauseRefresh;
};

void __haltOrCatchFire();

#define zwassert(cond) \
do \
{ \
if (!(cond)) \
{ \
Serial.printf("ZASSERT AT %d\n", __LINE__); \
__haltOrCatchFire(); \
} \
} while (0)

#endif

+ 21
- 0
zw_logging.h View File

@@ -0,0 +1,21 @@
#ifndef __ZW_LOGGING__H__
#define __ZW_LOGGING__H__

#include "zw_common.h"

extern ZWAppConfig gConfig;
extern void (*gPublishLogsEmit)(const char* fmt, ...);

#define dprint(fmt, ...) do { \
if (gConfig.debug) { \
Serial.printf(fmt, ##__VA_ARGS__); \
} } while (0)

#define zlog(fmt, ...) do { \
if (gConfig.publishLogs && gPublishLogsEmit) { \
gPublishLogsEmit(fmt, ##__VA_ARGS__); \
} else { \
Serial.printf(fmt, ##__VA_ARGS__); \
} } while (0)

#endif

+ 226
- 0
zw_provision.cpp View File

@@ -0,0 +1,226 @@
#include "zw_provision.h"
#include "zw_logging.h"

char *EEPROMCFG_WiFiSSID = NULL;
char *EEPROMCFG_WiFiPass = NULL;
char *EEPROMCFG_RedisHost = NULL;
char *EEPROMCFG_RedisPass = NULL;
char *EEPROMCFG_OTAHost = NULL;
uint16_t EEPROMCFG_RedisPort = 0;
String gHostname;
char __hnShadowBuf[ZW_EEPROM_SIZE];

void __debugClearEEPROM()
{
dprint("__debugClearEEPROM()\n");
// not the quickest, but the most memory-efficient, algorithm
for (int i = 0; i < EEPROM_SIZE; i++)
EEPROM.write(i, 0);
EEPROM.commit();
}

void EEPROM_setup()
{
EEPROM.begin(EEPROM_SIZE);
}

#define CFG_ELEMENTS 6
#define CFG_HEADER_SIZE (CFG_ELEMENTS * sizeof(uint16_t))

void CFG_EEPROM_read()
{
uint16_t lengths[CFG_ELEMENTS];
dprint("CFG_EEPROM_read: reading %d for lengths\n", CFG_HEADER_SIZE);
zwassert(EEPROM.readBytes(CFG_EEPROM_ADDR, lengths, CFG_HEADER_SIZE) == CFG_HEADER_SIZE);

dprint("CFG_EEPROM_read: LENGTHS: ");
for (int i = 0; i < CFG_ELEMENTS; i++)
{
dprint("%d ", lengths[i]);
}
dprint("\n");

EEPROMCFG_WiFiSSID = (char *)malloc(lengths[0] + 1);
bzero(EEPROMCFG_WiFiSSID, lengths[0] + 1);
auto offset = CFG_EEPROM_ADDR + CFG_HEADER_SIZE;
zwassert(EEPROM.readBytes(offset, EEPROMCFG_WiFiSSID, lengths[0]) == lengths[0]);
offset += lengths[0];
dprint("CFG_EEPROM_read: WIFI SSID: %s\n", EEPROMCFG_WiFiSSID);

EEPROMCFG_WiFiPass = (char *)malloc(lengths[1] + 1);
bzero(EEPROMCFG_WiFiPass, lengths[1] + 1);
zwassert(EEPROM.readBytes(offset, EEPROMCFG_WiFiPass, lengths[1]) == lengths[1]);
offset += lengths[1];
dprint("CFG_EEPROM_read: WIFI PASS: %s\n", EEPROMCFG_WiFiPass);

EEPROMCFG_RedisHost = (char *)malloc(lengths[2] + 1);
bzero(EEPROMCFG_RedisHost, lengths[2] + 1);
zwassert(EEPROM.readBytes(offset, EEPROMCFG_RedisHost, lengths[2]) == lengths[2]);
offset += lengths[2];
dprint("CFG_EEPROM_read: REDIS IP: %s\n", EEPROMCFG_RedisHost);

zwassert(EEPROM.readBytes(offset, &EEPROMCFG_RedisPort, lengths[3]) == lengths[3]);
offset += lengths[3];
dprint("CFG_EEPROM_read: REDIS PORT: %d\n", EEPROMCFG_RedisPort);

EEPROMCFG_RedisPass = (char *)malloc(lengths[4] + 1);
bzero(EEPROMCFG_RedisPass, lengths[4] + 1);
zwassert(EEPROM.readBytes(offset, EEPROMCFG_RedisPass, lengths[4]) == lengths[4]);
offset += lengths[4];
dprint("CFG_EEPROM_read: REDIS PASS: %s\n", EEPROMCFG_RedisPass);

EEPROMCFG_OTAHost = (char *)malloc(lengths[5] + 1);
bzero(EEPROMCFG_OTAHost, lengths[5] + 1);
zwassert(EEPROM.readBytes(offset, EEPROMCFG_OTAHost, lengths[5]) == lengths[5]);
offset += lengths[5];
dprint("CFG_EEPROM_read: OTA HOST: %s\n", EEPROMCFG_OTAHost);
}

#define PSTRING_LENGTH_LIMIT (CFG_EEPROM_SIZE / 2)

inline bool __provStrCheck(const char *str)
{
return str && strnlen(str, CFG_EEPROM_SIZE - CFG_HEADER_SIZE) <= PSTRING_LENGTH_LIMIT;
}

bool checkUnitProvisioning()
{
auto eeHnLength = EEPROM.readBytes(ZW_EEPROM_HOSTNAME_ADDR, __hnShadowBuf, ZW_EEPROM_SIZE);

if (!eeHnLength)
return false;

gHostname = String(__hnShadowBuf);
dprint("EEPROM HOSTNAME %s\n", gHostname.c_str());

CFG_EEPROM_read();

return __provStrCheck(EEPROMCFG_WiFiSSID) && __provStrCheck(EEPROMCFG_WiFiPass) &&
__provStrCheck(EEPROMCFG_RedisHost) && __provStrCheck(EEPROMCFG_RedisPass) &&
__provStrCheck(EEPROMCFG_OTAHost) && EEPROMCFG_RedisPort > 0;
}

#if ZEROWATCH_PROVISIONING_MODE
void provisionUnit()
{
dprint("***** ZeroWatch provisioning mode *****\n");
dprint("\tHostname: \t%s\n", ZWPROV_HOSTNAME);
dprint("\tWiFi SSID: \t%s\n", ZWPROV_WIFI_SSID);
dprint("\tWiFi password: \t%s\n", ZWPROV_WIFI_PASSWORD);
dprint("\tRedis host: \t%s\n", ZWPROV_REDIS_HOST);
dprint("\tRedis password: \t%s\n", ZWPROV_REDIS_PASSWORD);
dprint("\tRedis port: \t%u\n", ZWPROV_REDIS_PORT);
dprint("\tOTA host: \t%s\n", ZWPROV_OTA_HOST);
dprint("***** WILL COMMIT THESE VALUES TO EEPROM IN %d SECONDS.... *****\n", ZWPROV_MODE_WRITE_DELAY);
delay(ZWPROV_MODE_WRITE_DELAY * 1000);
dprint("ZeroWatch provisioning mode writing provisioning values...\n");

auto hostname = String(ZWPROV_HOSTNAME);
zwassert(EEPROM.writeString(ZW_EEPROM_HOSTNAME_ADDR, hostname) == hostname.length());

uint16_t lengths[CFG_ELEMENTS];

auto _local_port = ZWPROV_REDIS_PORT;
lengths[0] = strlen(ZWPROV_WIFI_SSID);
lengths[1] = strlen(ZWPROV_WIFI_PASSWORD);
lengths[2] = strlen(ZWPROV_REDIS_HOST);
lengths[3] = sizeof(_local_port); // redis port, always 16 bits
lengths[4] = strlen(ZWPROV_REDIS_PASSWORD);
lengths[5] = strlen(ZWPROV_OTA_HOST);

dprint("ZeroWatch provisioning LENGTHS: ");
for (int i = 0; i < CFG_ELEMENTS; i++)
{
dprint("%d ", lengths[i]);
}
dprint("\n");

auto writeLen = EEPROM.writeBytes(CFG_EEPROM_ADDR, lengths, CFG_HEADER_SIZE);
if (writeLen != CFG_HEADER_SIZE)
{
dprint("ZeroWatch provisioning ERROR: Config write (%d) failed\n", writeLen);
return;
}

auto offset = CFG_EEPROM_ADDR + CFG_HEADER_SIZE;

void **ptrptrs = (void **)malloc(sizeof(void *) * CFG_ELEMENTS);
ptrptrs[0] = (void *)ZWPROV_WIFI_SSID;
ptrptrs[1] = (void *)ZWPROV_WIFI_PASSWORD;
ptrptrs[2] = (void *)ZWPROV_REDIS_HOST;
ptrptrs[3] = (void *)&_local_port;
ptrptrs[4] = (void *)ZWPROV_REDIS_PASSWORD;
ptrptrs[5] = (void *)ZWPROV_OTA_HOST;

for (int i = 0; i < CFG_ELEMENTS; i++)
{
dprint("ZeroWatch provisioning: Writing %d bytes of element %d to address %d\n",
lengths[i], i, offset);
writeLen = EEPROM.writeBytes(offset, ptrptrs[i], lengths[i]);
if (writeLen != lengths[i])
{
dprint("ZeroWatch provisioning ERROR: element %d failed to write (%d != %d)\n", i, writeLen, lengths[i]);
return;
}
offset += lengths[i];
}

if (EEPROM.commit())
{
dprint("ZeroWatch provisioning: Write complete, verifying data...\n");

// TODO: hostname check
// TODO: FUNCTIONAL verification (?) (wifi, redis) - optional maybe

if (checkUnitProvisioning())
{
zwassert(!memcmp(ptrptrs[0], EEPROMCFG_WiFiSSID, lengths[0]));
zwassert(!memcmp(ptrptrs[1], EEPROMCFG_WiFiPass, lengths[1]));
zwassert(!memcmp(ptrptrs[2], EEPROMCFG_RedisHost, lengths[2]));
zwassert(!memcmp(ptrptrs[3], &EEPROMCFG_RedisPort, lengths[3]));
zwassert(!memcmp(ptrptrs[4], EEPROMCFG_RedisPass, lengths[4]));
zwassert(!memcmp(ptrptrs[5], EEPROMCFG_OTAHost, lengths[5]));
dprint("ZeroWatch provisioning SUCCESS!\n");
}
else
{
dprint("ZeroWatch provisioning FAILED! :shrug:\n");
}
}
else
{
dprint("ZeroWatch provisioning: commit failed\n");
}

__haltOrCatchFire();
}
#endif

void verifyProvisioning()
{
EEPROM_setup();

#if ZEROWATCH_DEL_PROVISIONS
dprint("***** ZEROWATCH_DEL_PROVISIONS is set! Waiting %d seconds... *****\n", ZWPROV_MODE_WRITE_DELAY);
delay(ZWPROV_MODE_WRITE_DELAY * 1000);
dprint("***** CLEARING PROVISIONING!! ***** \n");
__debugClearEEPROM();
#endif

#if ZEROWATCH_PROVISIONING_MODE
provisionUnit();
#endif

if (!checkUnitProvisioning())
{
zlog("ERROR: This device is not provisioned! Cannot continue, halting forever.\n");
__haltOrCatchFire();
}

// TODO: better place for this? I'm sure there is one...
if (!(gHostname.equals("ezero") || gHostname.equals("amini")))
{
zlog("ERROR: Unrecognized hostname '%s', halting forever!\n", __hnShadowBuf);
__haltOrCatchFire();
}
}

+ 40
- 0
zw_provision.h View File

@@ -0,0 +1,40 @@
#ifndef __ZW_PROVISION__H__
#define __ZW_PROVISION__H__

#include <EEPROM.h>

// provisioning definitions
#define ZEROWATCH_PROVISIONING_MODE 0
#if ZEROWATCH_PROVISIONING_MODE
// these will be written to EEPROM
#define ZWPROV_HOSTNAME ""
#define ZWPROV_WIFI_SSID ""
#define ZWPROV_WIFI_PASSWORD ""
#define ZWPROV_REDIS_HOST ""
#define ZWPROV_REDIS_PASSWORD ""
#define ZWPROV_REDIS_PORT 6379
#define ZWPROV_OTA_HOST ""
#endif

#define ZW_EEPROM_SIZE 32
#define ZW_EEPROM_HOSTNAME_ADDR 0
#define CFG_EEPROM_SIZE 3192
#define CFG_EEPROM_ADDR ZW_EEPROM_SIZE
#define EEPROM_SIZE ZW_EEPROM_SIZE + CFG_EEPROM_SIZE
#define ZWPROV_MODE_WRITE_DELAY 60

// dangerous!
#define ZEROWATCH_DEL_PROVISIONS 0

extern char *EEPROMCFG_WiFiSSID;
extern char *EEPROMCFG_WiFiPass;
extern char *EEPROMCFG_RedisHost;
extern char *EEPROMCFG_RedisPass;
extern char *EEPROMCFG_OTAHost;
extern uint16_t EEPROMCFG_RedisPort;

extern String gHostname;

void verifyProvisioning();

#endif

+ 182
- 0
zw_redis.cpp View File

@@ -0,0 +1,182 @@
#include "zw_redis.h"
#include "zw_logging.h"

#define REDIS_KEY(x) String(hostname + x).c_str()

#define REDIS_KEY_CREATE_LOCAL(x) \
auto redisKey_local__String_capture = String(hostname + x); \
auto redisKey_local = redisKey_local__String_capture.c_str();

bool ZWRedis::connect()
{
connection.wifi = new WiFiClient();

if (!connection.wifi->connect(configuration.host, configuration.port))
{
dprint("Redis connection failed");
delete connection.wifi, connection.wifi = nullptr;
return false;
}
else
{
connection.redis = new Redis(*connection.wifi);
if (connection.redis->authenticate(configuration.password) != RedisSuccess)
{
dprint("Redis auth failed");
delete connection.redis, connection.redis = nullptr;
return false;
}
}

return true;
}

void ZWRedis::checkin(
unsigned long ticks,
const char* localIp,
unsigned long immediateLatency,
unsigned long averageLatency,
int expireMessage)
{
auto rKey = String("rpjios.checkin." + hostname);
const char *key = rKey.c_str();

// TODO: error check!
connection.redis->hset(key, "host", hostname.c_str());
connection.redis->hset(key, "up", String(ticks).c_str());
connection.redis->hset(key, "ver", ZEROWATCH_VER);
char _ifbuf[1024];
bzero(_ifbuf, 1024);
snprintf(_ifbuf, 1024,
"{ \"wifi\": { \"address\": \"%s\", \"latency\": "
"{ \"immediate\": %ld, \"rollingAvg\": %ld }}}",
localIp, immediateLatency, averageLatency);
connection.redis->hset(key, "ifaces", _ifbuf);
connection.redis->expire(key, expireMessage);
}

bool ZWRedis::heartbeat(int expire)
{
REDIS_KEY_CREATE_LOCAL(":heartbeat");
if (!connection.redis->set(redisKey_local, String(micros()).c_str()))
return false;

if (expire)
connection.redis->expire(redisKey_local, expire);

return true;
}

int ZWRedis::incrementBootcount(bool reset)
{
REDIS_KEY_CREATE_LOCAL(":bootcount");
auto bc = connection.redis->get(redisKey_local);
auto bcNext = (reset ? 0 : bc.toInt()) + 1;

if (connection.redis->set(redisKey_local, String(bcNext).c_str())) {
return bcNext;
}
return -1;
}

ZWAppConfig ZWRedis::readConfig()
{
ZWAppConfig retCfg;

// TODO: error check!
auto bc = connection.redis->get(REDIS_KEY(":config:brightness"));
auto rc = connection.redis->get(REDIS_KEY(":config:refresh"));
auto dg = connection.redis->get(REDIS_KEY(":config:debug"));
auto pl = connection.redis->get(REDIS_KEY(":config:publishLogs"));
auto pu = connection.redis->get(REDIS_KEY(":config:pauseRefresh"));

retCfg.brightness = bc.toInt();
retCfg.refresh = rc.toInt();
retCfg.debug = (bool)dg.toInt();
retCfg.publishLogs = (bool)pl.toInt();
retCfg.pauseRefresh = (bool)pu.toInt();

return retCfg;
}

bool ZWRedis::handleUserKey(const char *keyPostfix, ZWRedisUserKeyHandler handler)
{
if (!keyPostfix || !handler)
{
zlog("ZWRedis::handleUserKey ERROR arguments\n");
return false;
}

auto getReturn = connection.redis->get(REDIS_KEY(keyPostfix));

if (getReturn && getReturn.length()) {
REDIS_KEY_CREATE_LOCAL(keyPostfix + ":" + getReturn);
///
// TODO: handle this wierd print on things like 'update'...
// and make sure they never write to keys like that!
// e.g.
// ZWRedis::handleUserKey(ezero) (key=:config:update) has return path 'ezero:config:update:{
// "url": "zero_watch_updates/zero_watch-v0.2.0.4.ino.bin",
// "md5": "eb8a0182161c88328c4888cc64e8a822",
// "size": 924368,
// "otp": 619
// }'"
///
dprint("ZWRedis::handleUserKey(%s) (key=%s) has return path '%s'\n",
hostname.c_str(), keyPostfix, redisKey_local);
ZWRedisResponder responder(*this, redisKey_local__String_capture);
if (handler(getReturn, responder))
{
return connection.redis->del(REDIS_KEY(keyPostfix));
}
}

return false;
}

void ZWRedis::responderHelper(const char *key, const char *msg, int expire)
{
connection.redis->publish(key, msg);
if (!connection.redis->set(key, msg)) {
zlog("ERROR: ZWRedis::responderHelper() set of %s failed\n", key);
return;
}

if (expire > 0) {
dprint("ZWRedis::responderHelper expiring %s at %d\n", key, expire);
connection.redis->expire(key, expire);
}
}

void ZWRedis::publishLog(const char* msg)
{
connection.redis->publish(REDIS_KEY(":info:publishLogs"), msg);
}

bool ZWRedis::postCompletedUpdate()
{
return connection.redis->del(REDIS_KEY(":config:update"));
}

std::vector<String> ZWRedis::getRange(const char* key, int start, int stop)
{
return connection.redis->lrange(key, start, stop);
}

bool ZWRedis::clearControlPoint()
{
return connection.redis->del(REDIS_KEY(":config:controlPoint"));
}

void ZWRedisResponder::setValue(const char *format, ...)
{
#define BUFLEN 1024
char _buf[1024];
bzero(_buf, 1024);
va_list args;
va_start(args, format);
vsnprintf(_buf, BUFLEN, format, args);
redis.responderHelper(key.c_str(), _buf, expire);
va_end(args);
}

+ 97
- 0
zw_redis.h View File

@@ -0,0 +1,97 @@
#ifndef __ZW_REDIS__H__
#define __ZW_REDIS__H__

#include <Redis.h>
#include <WiFiClient.h>
#include <vector>

#include "zw_common.h"

#define ZWREDIS_DEFAULT_EXPIRY 120

struct ZWRedisHostConfig
{
const char *host;
uint16_t port;
const char *password;
};

class ZWRedis;

class ZWRedisResponder {
protected:
ZWRedis& redis;
String key;
int expire = ZWREDIS_DEFAULT_EXPIRY;

public:
ZWRedisResponder(ZWRedis& parent, String currentKey) :
redis(parent), key(currentKey) {}

~ZWRedisResponder() {}

ZWRedisResponder(const ZWRedisResponder &) = delete;
ZWRedisResponder &operator=(const ZWRedisResponder &) = delete;

void setExpire(int newExpire) { expire = newExpire; }

void setValue(const char* format, ...);
};

typedef bool (*ZWRedisUserKeyHandler)(String& userKeyValue, ZWRedisResponder& responder);

class ZWRedis {
protected:
friend class ZWRedisResponder;

struct RedisClientConn
{
Redis *redis;
WiFiClient *wifi;
};

String &hostname;
ZWRedisHostConfig configuration;
RedisClientConn connection;

void responderHelper(const char* key, const char* msg, int expire = 0);

public:
ZWRedis(String &hostname, ZWRedisHostConfig config) :
hostname(hostname), configuration(config)
{}

~ZWRedis() {}

ZWRedis(const ZWRedis &) = delete;
ZWRedis &operator=(const ZWRedis &) = delete;

// TODO moves

bool connect();

void checkin(
unsigned long ticks,
const char* localIp,
unsigned long immediateLatency,
unsigned long averageLatency,
int expireMessage = 60);

bool heartbeat(int expire = 0);

int incrementBootcount(bool reset = false);

ZWAppConfig readConfig();

bool handleUserKey(const char *keyPostfix, ZWRedisUserKeyHandler handler);

void publishLog(const char* msg);

bool postCompletedUpdate();

std::vector<String> getRange(const char* key, int start, int stop);

bool clearControlPoint();
};

#endif

Loading…
Cancel
Save