Hi Alex,
Here it is.
I also have some questions:
- I am treating my system to have the mcu as the master controller not the notecard. Is this the right way or shall the notecard be treated as the master?
- the switch on the notecarrier F v1.3 FEATHER_EN, should be switched to the ON or the F_ATTN for low power applications?
- In general, the system behaves differently when on battery and when on usb. For example, the below firmware when on USB, any changes I make on the env vars on notehub, they are properly implemented during run time. Once it goes on battery, I can see from notehub that env vars are fetched but are never implemented.
#include <Notecard.h>
#include <STM32LowPower.h>
#include <ModbusMaster.h>
#include <Arduino.h>
// =================== CONFIGURATION FLAGS ======================
#define MAX_ENV_FETCH_RETRIES 3
#define SENSOR_ENABLE_PIN 5
#define RS485_BAUD 9600
#define SENSOR_MODBUS_ID 1
#define PRODUCT_UID “xxx”
#define FIRMWARE_VERSION “0.0.1”
#define DATA_FILE “data.qo”
// =================== GLOBAL VARIABLES ==========================
Notecard notecard;
ModbusMaster node;
unsigned long loopIntervalMs = 300000;
uint32_t sensorWarmupMs = 5000;
uint32_t readDelayMs = 5000;
uint8_t totalReadings = 3;
uint8_t syncFailCount = 0;
uint32_t loopStart = 0;
String deviceID = “xxx”;
bool DEBUG_MODE = false;
String wifiSSID = “xxx”;
String wifiPassword = “xxx”;
// =================== ENV VAR FETCH FUNCTIONS ====================
int getEnvInt(J* body, const char* key, int defaultVal) {
if (body && JGetObjectItem(body, key)) {
return atoi(JGetString(body, key));
}
return defaultVal;
}
const char* getEnvStr(J* body, const char* key, const char* defaultVal) {
if (body && JGetObjectItem(body, key)) {
return JGetString(body, key);
}
return defaultVal;
}
void fetchEnvVarsWithRetries() {
bool success = false;
for (int attempt = 0; attempt < MAX_ENV_FETCH_RETRIES; attempt++) {
if (DEBUG_MODE) {
Serial.print(“
Fetching env vars (Attempt “);
Serial.print(attempt + 1);
Serial.println(”)…”);
}
J* req = notecard.newRequest("env.get");
J* env = notecard.requestAndResponse(req);
if (env) {
J* body = JGetObjectItem(env, "body");
loopIntervalMs = getEnvInt(body, "interval_sec", loopIntervalMs / 1000) * 1000UL;
sensorWarmupMs = getEnvInt(body, "sensor_warmup_ms", sensorWarmupMs);
readDelayMs = getEnvInt(body, "read_delay_ms", readDelayMs);
totalReadings = getEnvInt(body, "num_readings", totalReadings);
wifiSSID = getEnvStr(body, "wifi_ssid", wifiSSID.c_str());
wifiPassword = getEnvStr(body, "wifi_pass", wifiPassword.c_str());
JDelete(env);
success = true;
break;
}
delay(2000);
}
if (DEBUG_MODE) {
if (success) {
Serial.println(“
Env vars fetched successfully:”);
Serial.print(" interval_sec: “); Serial.println(loopIntervalMs / 1000);
Serial.print(” sensorWarmupMs: “); Serial.println(sensorWarmupMs);
Serial.print(” readDelayMs: “); Serial.println(readDelayMs);
Serial.print(” totalReadings: “); Serial.println(totalReadings);
Serial.print(” WiFi SSID: "); Serial.println(wifiSSID);
} else {
Serial.println(“
Failed to fetch env vars after max retries. Using defaults.”);
}
}
}
// =================== TIME FORMAT HELPER ====================
String formatTimestamp(uint32_t ts) {
time_t rawTime = (time_t) ts;
struct tm* timeinfo = gmtime(&rawTime);
char buf[25];
sprintf(buf, “%02d/%02d/%02d %02d:%02d:%02d GMT”,
timeinfo->tm_mday,
timeinfo->tm_mon + 1,
timeinfo->tm_year % 100,
timeinfo->tm_hour,
timeinfo->tm_min,
timeinfo->tm_sec);
return String(buf);
}
uint32_t getCurrentTimeSec() {
J* timeReq = notecard.requestAndResponse(notecard.newRequest(“card.time”));
if (timeReq && JGetObjectItem(timeReq, “time”)) {
uint32_t ts = (uint32_t) JGetNumber(timeReq, “time”);
JDelete(timeReq);
if (DEBUG_MODE) {
Serial.print(“
getCurrentTimeSec(): “);
Serial.print(ts);
Serial.print(” (”);
Serial.print(formatTimestamp(ts));
Serial.println(“)”);
}
return ts;
}
JDelete(timeReq);
return 0;
}
// =================== PERIPHERALS ====================
void initializePeripherals() {
pinMode(SENSOR_ENABLE_PIN, OUTPUT);
digitalWrite(SENSOR_ENABLE_PIN, LOW);
Serial.begin(115200);
delay(10000);
if (Serial) {
DEBUG_MODE = true;
Serial.println(“
USB connected: Debug mode ON”);
} else {
DEBUG_MODE = false;
}
if (DEBUG_MODE) Serial.println(“
Initializing Notecard and peripherals…”);
notecard.begin();
if (DEBUG_MODE) notecard.setDebugOutputStream(Serial);
LowPower.begin();
if (DEBUG_MODE) Serial.println(“
Low power mode initialized”);
Serial1.begin(RS485_BAUD);
node.begin(SENSOR_MODBUS_ID, Serial1);
if (DEBUG_MODE) Serial.println(“
RS485 and Modbus initialized”);
}
// =================== NOTECARD CONFIG ====================
void configureNotecard() {
if (DEBUG_MODE) Serial.println(“
Configuring Notecard Wi-Fi and connection…”);
J *wifi = notecard.newRequest(“card.wifi”);
if (wifi) {
JAddStringToObject(wifi, “ssid”, wifiSSID.c_str());
JAddStringToObject(wifi, “password”, wifiPassword.c_str());
notecard.sendRequest(wifi);
}
J *wireless = notecard.newRequest(“card.wireless”);
if (wireless) {
JAddStringToObject(wireless, “mode”, “wifi”);
JAddBoolToObject(wireless, “wifi”, true);
JAddBoolToObject(wireless, “cell”, true);
notecard.sendRequest(wireless);
}
J *hub = notecard.newRequest(“hub.set”);
if (hub) {
JAddStringToObject(hub, “product”, PRODUCT_UID);
JAddStringToObject(hub, “mode”, “minimum”);
notecard.sendRequest(hub);
}
}
void syncWithNotehub() {
if (DEBUG_MODE) Serial.println(“
Syncing with Notehub…”);
notecard.sendRequest(notecard.newRequest(“hub.sync”));
bool completed = false;
for (int i = 0; i < 40; i++) {
delay(500);
J* statusReq = notecard.requestAndResponse(notecard.newRequest(“hub.status”));
if (statusReq) {
if (JGetBool(statusReq, “completed”)) {
completed = true;
JDelete(statusReq);
break;
}
JDelete(statusReq);
}
}
if (DEBUG_MODE) Serial.println(completed ? “
Notehub sync completed” : “
Notehub sync timeout”);
}
void putNotecardToSleep() {
if (DEBUG_MODE) Serial.println(“
Putting Notecard to sleep…”);
J* sleepReq = notecard.newRequest(“card.sleep”);
if (sleepReq) {
JAddStringToObject(sleepReq, “mode”, “off”);
J* resp = notecard.requestAndResponse(sleepReq);
if (resp) JDelete(resp);
}
if (DEBUG_MODE) Serial.println(“
Notecard sleep requested”);
}
// =================== SENSOR LOGIC ====================
bool readJXBSData(float &temp, float &humidity, float &ec, float &ph, float &n, float &p, float &k) {
bool success = true;
if (node.readHoldingRegisters(0x0012, 2) == node.ku8MBSuccess) {
humidity = node.getResponseBuffer(0) / 10.0;
temp = node.getResponseBuffer(1) / 10.0;
} else success = false;
if (node.readHoldingRegisters(0x0006, 1) == node.ku8MBSuccess) {
ph = node.getResponseBuffer(0) / 100.0;
} else success = false;
if (node.readHoldingRegisters(0x0015, 1) == node.ku8MBSuccess) {
ec = node.getResponseBuffer(0);
} else success = false;
if (node.readHoldingRegisters(0x001E, 3) == node.ku8MBSuccess) {
n = node.getResponseBuffer(0);
p = node.getResponseBuffer(1);
k = node.getResponseBuffer(2);
} else success = false;
if (success && DEBUG_MODE) {
Serial.println(“
Sensor Data:”);
Serial.print("
Temp: “); Serial.print(temp); Serial.print(” °C, ");
Serial.print("
Humidity: “); Serial.print(humidity); Serial.print(” %, ");
Serial.print("
EC: “); Serial.print(ec); Serial.print(” µS/cm, ");
Serial.print("
pH: “); Serial.print(ph); Serial.print(”, ");
Serial.print("
N: “); Serial.print(n); Serial.print(” mg/kg, ");
Serial.print("P: “); Serial.print(p); Serial.print(” mg/kg, ");
Serial.print(“K: “); Serial.print(k); Serial.println(” mg/kg”);
}
return success;
}
void powerOnSensor() {
if (DEBUG_MODE) Serial.println(“
Powering ON sensor…”);
digitalWrite(SENSOR_ENABLE_PIN, HIGH);
delay(sensorWarmupMs);
if (DEBUG_MODE) Serial.println(“
Sensor powered ON”);
}
void powerOffSensor() {
if (DEBUG_MODE) Serial.println(“
Powering OFF sensor…”);
digitalWrite(SENSOR_ENABLE_PIN, LOW);
if (DEBUG_MODE) Serial.println(“
Sensor powered OFF”);
}
float getBatteryVoltage() {
float battery = -1.0;
J* volt = notecard.requestAndResponse(notecard.newRequest(“card.voltage”));
if (volt && JGetObjectItem(volt, “value”))
battery = JGetNumber(volt, “value”);
JDelete(volt);
if (DEBUG_MODE) {
Serial.print("
Battery voltage: ");
Serial.println(battery);
}
return battery;
}
void getLocation(double &lat, double &lon, String &source) {
lat = lon = 0.0;
source = “unknown”;
J* start = notecard.newRequest(“card.location.track”);
if (start) {
JAddBoolToObject(start, “start”, true);
notecard.sendRequest(start);
delay(5000); // allow GPS time to get a fix
}
J* loc = notecard.requestAndResponse(notecard.newRequest(“card.location”));
if (loc) {
if (JGetObjectItem(loc, “lat”)) lat = JGetNumber(loc, “lat”);
if (JGetObjectItem(loc, “lon”)) lon = JGetNumber(loc, “lon”);
const char* mode = JGetString(loc, "mode"); // "gps", "off", etc.
const char* status = JGetString(loc, "status"); // "locked", "gps-inactive", etc.
if (mode && strcmp(mode, "gps") == 0 && status && strcmp(status, "locked") == 0) {
source = "gps";
} else if (status && strstr(status, "gps-inactive")) {
source = "cached";
}
JDelete(loc);
}
J* stop = notecard.newRequest(“card.location.track”);
if (stop) {
JAddBoolToObject(stop, “stop”, true);
notecard.sendRequest(stop);
}
if (DEBUG_MODE) {
Serial.print("
Location: “);
Serial.print(lat);
Serial.print(”, “);
Serial.print(lon);
Serial.print(” | Source: ");
Serial.println(source);
}
}
void collectAndSendSensorData() {
powerOnSensor();
J* note = notecard.newRequest(“note.add”);
if (note) {
JAddStringToObject(note, “file”, DATA_FILE);
JAddBoolToObject(note, “sync”, true);
J* body = JCreateObject();
JAddStringToObject(body, “device”, deviceID.c_str());
JAddStringToObject(body, “fw_ver”, FIRMWARE_VERSION);
float battery = getBatteryVoltage();
double lat, lon;
String locSource;
getLocation(lat, lon, locSource);
JAddNumberToObject(body, “battery (V)”, battery);
JAddNumberToObject(body, “lat”, lat);
JAddNumberToObject(body, “lon”, lon);
JAddStringToObject(body, “location_source”, locSource.c_str());
J* samples = JCreateArray();
for (uint8_t i = 0; i < totalReadings; i++) {
float temp, hum, ec, ph, n, p, k;
if (readJXBSData(temp, hum, ec, ph, n, p, k)) {
uint32_t ts = getCurrentTimeSec();
J* entry = JCreateObject();
JAddNumberToObject(entry, "ts", ts);
JAddStringToObject(entry, "ts_str", formatTimestamp(ts).c_str());
JAddNumberToObject(entry, "temp (°C)", temp);
JAddNumberToObject(entry, "hum (%)", hum);
JAddNumberToObject(entry, "ec (ÎĽS/cm)", ec);
JAddNumberToObject(entry, "ph", ph);
JAddNumberToObject(entry, "n (mg/kg)", n);
JAddNumberToObject(entry, "p (mg/kg)", p);
JAddNumberToObject(entry, "k (mg/kg)", k);
JAddItemToArray(samples, entry);
if (DEBUG_MODE) {
Serial.print("⏱️ Sample Time: ");
Serial.println(formatTimestamp(ts));
}
}
delay(readDelayMs);
}
if (JGetArraySize(samples) > 0) {
JAddItemToObject(body, "samples", samples);
JAddItemToObject(note, "body", body);
notecard.sendRequest(note);
if (DEBUG_MODE) Serial.println("âś… Sensor data sent");
} else {
JDelete(samples);
JDelete(body);
JDelete(note);
if (DEBUG_MODE) Serial.println("⚠️ No valid sensor data, nothing sent");
}
}
powerOffSensor();
}
// =================== SETUP & LOOP ====================
void setup() {
loopStart = millis();
initializePeripherals();
configureNotecard();
syncWithNotehub();
fetchEnvVarsWithRetries();
collectAndSendSensorData();
putNotecardToSleep();
uint32_t duration = millis() - loopStart;
uint32_t sleepMs = (loopIntervalMs > duration) ? loopIntervalMs - duration : 1;
if (DEBUG_MODE) {
Serial.print(“
Entering deep sleep for “);
Serial.print(sleepMs / 1000);
Serial.println(” seconds”);
Serial.flush();
delay(100);
}
LowPower.shutdown(sleepMs);
}
void loop() {
}