/* beescale_lora_mcci.ino BeieliScale, see https://mini-beieli.ch Joerg Lehmann, nbit Informatik GmbH */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace McciCatena; /****************************************************************************\ | | MANIFEST CONSTANTS & TYPEDEFS | \****************************************************************************/ /* how long do we wait between transmissions? (in seconds) */ enum { // set this to interval between transmissions, in seconds // Actual time will be a little longer because have to // add measurement and broadcast time, but we attempt // to compensate for the gross effects below. CATCFG_T_CYCLE = 6 * 60, // every 6 minutes CATCFG_T_CYCLE_TEST = 30, // every 10 seconds }; /* additional timing parameters; ususually you don't change these. */ enum { CATCFG_T_WARMUP = 1, CATCFG_T_SETTLE = 5, CATCFG_T_OVERHEAD = (CATCFG_T_WARMUP + CATCFG_T_SETTLE), }; constexpr uint32_t CATCFG_GetInterval(uint32_t tCycle) { return (tCycle < CATCFG_T_OVERHEAD) ? CATCFG_T_OVERHEAD : tCycle - CATCFG_T_OVERHEAD ; } enum { CATCFG_T_INTERVAL = CATCFG_GetInterval(CATCFG_T_CYCLE), }; enum { PIN_ONE_WIRE = A2, // XSDA1 == A2 PIN_SHT10_CLK = 8, // XSCL0 == D8 PIN_SHT10_DATA = 12, // XSDA0 == D12 }; // forwards static void settleDoneCb(osjob_t *pSendJob); static void warmupDoneCb(osjob_t *pSendJob); static void txFailedDoneCb(osjob_t *pSendJob); static void sleepDoneCb(osjob_t *pSendJob); static Arduino_LoRaWAN::SendBufferCbFn sendBufferDoneCb; /****************************************************************************\ | | READ-ONLY DATA | \****************************************************************************/ static const char sVersion[] = "0.1"; static const byte MAX_VALUES_TO_SEND = 4; static const uint8_t LORA_DATA_VERSION = 1; /****************************************************************************\ | | VARIABLES | \****************************************************************************/ typedef struct { uint8_t version; // Versionierung des Paketformats uint8_t vbat; // Batteriespannung (1 Einheit => 20 mV) uint8_t humidity; // Luftfeuchtigkeit in Zehntels-Prozent uint8_t pressure; // Luftdruck in XXXX uint8_t reading_offset[MAX_VALUES_TO_SEND]; // Zeit der Messung in Sekunden, erster Wert ist 0 int16_t weight_raw1[MAX_VALUES_TO_SEND]; // Reading (raw) der ersten Waegzelle int16_t weight_raw2[MAX_VALUES_TO_SEND]; // Reading (raw) der zweiten Waegzelle int16_t temperature[MAX_VALUES_TO_SEND]; // Temperatur in 1/10 Grad Celsius } LORA_data; // Global Variables LORA_data lora_data; // generic timer long t_cur; // the primary object Catena gCatena; // // the LoRaWAN backhaul. Note that we use the // Catena version so it can provide hardware-specific // information to the base class. // Catena::LoRaWAN gLoRaWAN; // // the LED // StatusLed gLed (Catena::PIN_STATUS_LED); // The temperature/humidity sensor Adafruit_BME280 gBME280; // The default initalizer creates an I2C connection bool fBme; SPIClass gSPI2( Catena::PIN_SPI2_MOSI, Catena::PIN_SPI2_MISO, Catena::PIN_SPI2_SCK ); // The flash Catena_Mx25v8035f gFlash; bool fFlash; // Scales HX711 LoadCell_1; HX711 LoadCell_2; // USB power bool fUsbPower; // have we printed the sleep info? bool g_fPrintedSleeping = false; // the job that's used to synchronize us with the LMIC code static osjob_t sensorJob; void sensorJob_cb(osjob_t *pJob); void setup(void) { gCatena.begin(); lora_data.version = LORA_DATA_VERSION; setup_platform(); setup_bme280(); setup_scales(); setup_flash(); setup_uplink(); } void setup_platform(void) { #ifdef USBCON // if running unattended, don't wait for USB connect. if (! (gCatena.GetOperatingFlags() & static_cast(gCatena.OPERATING_FLAGS::fUnattended))) { while (!Serial) /* wait for USB attach */ yield(); } #endif gCatena.SafePrintf("\n"); gCatena.SafePrintf("-------------------------------------------------------------------------------\n"); gCatena.SafePrintf("BeieliScale Version %s.\n", sVersion); { char sRegion[16]; gCatena.SafePrintf("Target network: %s / %s\n", gLoRaWAN.GetNetworkName(), gLoRaWAN.GetRegionString(sRegion, sizeof(sRegion)) ); } gCatena.SafePrintf("Enter 'help' for a list of commands.\n"); #ifdef CATENA_CFG_SYSCLK gCatena.SafePrintf("SYSCLK: %d MHz\n", CATENA_CFG_SYSCLK); #endif #ifdef USBCON gCatena.SafePrintf("USB enabled\n"); #else gCatena.SafePrintf("USB disabled\n"); #endif Catena::UniqueID_string_t CpuIDstring; gCatena.SafePrintf( "CPU Unique ID: %s\n", gCatena.GetUniqueIDstring(&CpuIDstring) ); gCatena.SafePrintf("--------------------------------------------------------------------------------\n"); gCatena.SafePrintf("\n"); // set up the LED gLed.begin(); gCatena.registerObject(&gLed); gLed.Set(LedPattern::FastFlash); // set up LoRaWAN gCatena.SafePrintf("LoRaWAN init: "); if (!gLoRaWAN.begin(&gCatena)) { gCatena.SafePrintf("failed\n"); } else { gCatena.SafePrintf("succeeded\n"); } gCatena.registerObject(&gLoRaWAN); /* find the platform */ const Catena::EUI64_buffer_t *pSysEUI = gCatena.GetSysEUI(); uint32_t flags; const CATENA_PLATFORM * const pPlatform = gCatena.GetPlatform(); if (pPlatform) { gCatena.SafePrintf("EUI64: "); for (unsigned i = 0; i < sizeof(pSysEUI->b); ++i) { gCatena.SafePrintf("%s%02x", i == 0 ? "" : "-", pSysEUI->b[i]); } gCatena.SafePrintf("\n"); flags = gCatena.GetPlatformFlags(); gCatena.SafePrintf( "Platform Flags: %#010x\n", flags ); gCatena.SafePrintf( "Operating Flags: %#010x\n", gCatena.GetOperatingFlags() ); } else { gCatena.SafePrintf("**** no platform, check provisioning ****\n"); flags = 0; } } void setup_bme280(void) { if (gBME280.begin(BME280_ADDRESS, Adafruit_BME280::OPERATING_MODE::Sleep)) { fBme = true; } else { fBme = false; gCatena.SafePrintf("No BME280 found: check wiring\n"); } } void setup_scales(void) { gCatena.SafePrintf("Setup Scales...\n"); // Initialize library with data output pin, clock input pin and gain factor. // Channel selection is made by passing the appropriate gain: // - With a gain factor of 64 or 128, channel A is selected // - With a gain factor of 32, channel B is selected // By omitting the gain factor parameter, the library // default "128" (Channel A) is used here. LoadCell_1.begin(A3, A2); LoadCell_2.begin(A1, A0); gCatena.SafePrintf("Setup Scales is complete\n"); } void setup_flash(void) { if (gFlash.begin(&gSPI2, Catena::PIN_SPI2_FLASH_SS)) { fFlash = true; gFlash.powerDown(); gCatena.SafePrintf("FLASH found, put power down\n"); } else { fFlash = false; gFlash.end(); gSPI2.end(); gCatena.SafePrintf("No FLASH found: check hardware\n"); } } void setup_uplink(void) { /* trigger a join by sending the first packet */ if (!(gCatena.GetOperatingFlags() & static_cast(gCatena.OPERATING_FLAGS::fManufacturingTest))) { if (! gLoRaWAN.IsProvisioned()) gCatena.SafePrintf("LoRaWAN not provisioned yet. Use the commands to set it up.\n"); else { gLed.Set(LedPattern::Joining); /* warm up the BME280 by discarding a measurement */ if (fBme) (void)gBME280.readTemperature(); /* trigger a join by sending the first packet */ startSendingUplink(); } } } // The Arduino loop routine -- in our case, we just drive the other loops. // If we try to do too much, we can break the LMIC radio. So the work is // done by outcalls scheduled from the LMIC os loop. void loop() { gCatena.poll(); /* for mfg test, don't tx, just fill -- this causes output to Serial */ if (gCatena.GetOperatingFlags() & static_cast(gCatena.OPERATING_FLAGS::fManufacturingTest)) { TxBuffer_t b; fillBuffer(b); delay(1000); } } void ReadSensors() { // vBat float vBat = gCatena.ReadVbat(); gCatena.SafePrintf("vBat: %d mV\n", (int) (vBat * 1000.0f)); lora_data.vbat = (vBat * 1000 / 20); // vBus float vBus = gCatena.ReadVbus(); gCatena.SafePrintf("vBus: %d mV\n", (int) (vBus * 1000.0f)); fUsbPower = (vBus > 3.0) ? true : false; if (fBme) { Adafruit_BME280::Measurements m = gBME280.readTemperaturePressureHumidity(); // temperature is 2 bytes from -0x80.00 to +0x7F.FF degrees C // pressure is 2 bytes, hPa * 10. // humidity is one byte, where 0 == 0/256 and 0xFF == 255/256. gCatena.SafePrintf( "BME280: T: %d P: %d RH: %d\n", (int) m.Temperature, (int) m.Pressure, (int) m.Humidity ); lora_data.temperature[0] = m.Temperature; lora_data.humidity = m.Temperature; lora_data.pressure = m.Pressure; } gCatena.SafePrintf("Before Read Scales\n"); if (LoadCell_1.wait_ready_timeout(1000)) { long w1 = LoadCell_1.read_average(5); gCatena.SafePrintf("Load_cell 1 output val: %ld\n", w1); } else { Serial.println("HX711 LoadCell_1 not found."); } if (LoadCell_2.wait_ready_timeout(1000)) { long w2 = LoadCell_2.read_average(5); gCatena.SafePrintf("Load_cell 2 output val: %ld\n", w2); } else { Serial.println("HX711 LoadCell_2 not found."); } gCatena.SafePrintf("After Read Scales\n"); } void fillBuffer(TxBuffer_t &b) { b.begin(); FlagsSensor2 flag; flag = FlagsSensor2(0); b.put(FormatSensor2); /* the flag for this record format */ uint8_t * const pFlag = b.getp(); b.put(0x00); /* will be set to the flags */ // vBat is sent as 5000 * v float vBat = gCatena.ReadVbat(); gCatena.SafePrintf("vBat: %d mV\n", (int) (vBat * 1000.0f)); b.putV(vBat); flag |= FlagsSensor2::FlagVbat; // vBus is sent as 5000 * v float vBus = gCatena.ReadVbus(); gCatena.SafePrintf("vBus: %d mV\n", (int) (vBus * 1000.0f)); fUsbPower = (vBus > 3.0) ? true : false; uint32_t bootCount; if (gCatena.getBootCount(bootCount)) { b.putBootCountLsb(bootCount); flag |= FlagsSensor2::FlagBoot; } if (fBme) { Adafruit_BME280::Measurements m = gBME280.readTemperaturePressureHumidity(); // temperature is 2 bytes from -0x80.00 to +0x7F.FF degrees C // pressure is 2 bytes, hPa * 10. // humidity is one byte, where 0 == 0/256 and 0xFF == 255/256. gCatena.SafePrintf( "BME280: T: %d P: %d RH: %d\n", (int) m.Temperature, (int) m.Pressure, (int) m.Humidity ); b.putT(m.Temperature); b.putP(m.Pressure); b.putRH(m.Humidity); flag |= FlagsSensor2::FlagTPH; } gCatena.SafePrintf("Before Read Scales\n"); if (LoadCell_1.wait_ready_timeout(1000)) { long w1 = LoadCell_1.read_average(5); gCatena.SafePrintf("Load_cell 1 output val: %ld\n", w1); } else { Serial.println("HX711 LoadCell_1 not found."); } if (LoadCell_2.wait_ready_timeout(1000)) { long w2 = LoadCell_2.read_average(5); gCatena.SafePrintf("Load_cell 2 output val: %ld\n", w2); } else { Serial.println("HX711 LoadCell_2 not found."); } gCatena.SafePrintf("After Read Scales\n"); *pFlag = uint8_t(flag); } void startSendingUplink(void) { TxBuffer_t b; LedPattern savedLed = gLed.Set(LedPattern::Measuring); fillBuffer(b); if (savedLed != LedPattern::Joining) gLed.Set(LedPattern::Sending); else gLed.Set(LedPattern::Joining); bool fConfirmed = false; if (gCatena.GetOperatingFlags() & (1 << 16)) { gCatena.SafePrintf("requesting confirmed tx\n"); fConfirmed = true; } //gLoRaWAN.SendBuffer(b.getbase(), b.getn(), sendBufferDoneCb, NULL, fConfirmed); gLoRaWAN.SendBuffer((uint8_t*) &lora_data, sizeof(LORA_data), sendBufferDoneCb, NULL, fConfirmed); } static void sendBufferDoneCb( void *pContext, bool fStatus ) { osjobcb_t pFn; gLed.Set(LedPattern::Settling); if (! fStatus) { gCatena.SafePrintf("send buffer failed\n"); pFn = txFailedDoneCb; } else { pFn = settleDoneCb; } os_setTimedCallback( &sensorJob, os_getTime()+sec2osticks(CATCFG_T_SETTLE), pFn ); } static void txFailedDoneCb( osjob_t *pSendJob ) { gCatena.SafePrintf("not provisioned, idling\n"); gLoRaWAN.Shutdown(); gLed.Set(LedPattern::NotProvisioned); } static void settleDoneCb( osjob_t *pSendJob ) { const bool fDeepSleep = checkDeepSleep(); if (! g_fPrintedSleeping) doSleepAlert(fDeepSleep); if (fDeepSleep) doDeepSleep(pSendJob); else doLightSleep(pSendJob); } bool checkDeepSleep(void) { bool const fDeepSleepTest = gCatena.GetOperatingFlags() & (1 << 19); bool fDeepSleep; if (fDeepSleepTest) { fDeepSleep = true; } #ifdef USBCON else if (Serial.dtr()) { fDeepSleep = false; } #endif else if (gCatena.GetOperatingFlags() & (1 << 17)) { fDeepSleep = false; } else if ((gCatena.GetOperatingFlags() & static_cast(gCatena.OPERATING_FLAGS::fUnattended)) != 0) { fDeepSleep = true; } else { fDeepSleep = false; } return fDeepSleep; } void doSleepAlert(const bool fDeepSleep) { g_fPrintedSleeping = true; if (fDeepSleep) { bool const fDeepSleepTest = gCatena.GetOperatingFlags() & (1 << 19); const uint32_t deepSleepDelay = fDeepSleepTest ? 10 : 30; gCatena.SafePrintf("using deep sleep in %u secs" #ifdef USBCON " (USB will disconnect while asleep)" #endif ": ", deepSleepDelay ); // sleep and print gLed.Set(LedPattern::TwoShort); for (auto n = deepSleepDelay; n > 0; --n) { uint32_t tNow = millis(); while (uint32_t(millis() - tNow) < 1000) { gCatena.poll(); yield(); } gCatena.SafePrintf("."); } gCatena.SafePrintf("\nStarting deep sleep.\n"); uint32_t tNow = millis(); while (uint32_t(millis() - tNow) < 100) { gCatena.poll(); yield(); } } else gCatena.SafePrintf("using light sleep\n"); } void doDeepSleep(osjob_t *pJob) { /* ok... now it's time for a deep sleep */ gLed.Set(LedPattern::Off); Serial.end(); Wire.end(); SPI.end(); if (fFlash) gSPI2.end(); gCatena.Sleep(CATCFG_T_INTERVAL); /* and now... we're awake again. trigger another measurement */ Serial.begin(); Wire.begin(); SPI.begin(); if (fFlash) gSPI2.begin(); sleepDoneCb(pJob); } void doLightSleep(osjob_t *pJob) { gLed.Set(LedPattern::Sleeping); os_setTimedCallback( pJob, os_getTime() + sec2osticks(CATCFG_T_INTERVAL), sleepDoneCb ); } static void sleepDoneCb( osjob_t *pJob ) { gLed.Set(LedPattern::WarmingUp); os_setTimedCallback( &sensorJob, os_getTime() + sec2osticks(CATCFG_T_WARMUP), warmupDoneCb ); } static void warmupDoneCb( osjob_t *pJob ) { startSendingUplink(); }