diff --git a/doc/LOGGING.md b/doc/LOGGING.md new file mode 100644 index 0000000..887d2e7 --- /dev/null +++ b/doc/LOGGING.md @@ -0,0 +1,214 @@ +# Application Note: Logging Data to FRAM + + + + + + +- [Introduction](#introduction) +- [Assumptions](#assumptions) + - [Message loss scenarios](#message-loss-scenarios) +- [Basic Design](#basic-design) +- [APIs](#apis) + - [Catena_FramRingBuf.h](#catena_framringbufh) + - [class McciCatena::FramRingBuffer_t](#class-mccicatenaframringbuffer_t) + - [class RecoverableUplinkQueue_t](#class-recoverableuplinkqueue_tuplinktype) + - [typedef RecoverableUplinkQueue_t::size_type](#typedef-recoverableuplinkqueue_tuplinktypesize_type) + - [typedef RecoverableUplinkQueue_t::sequence_type](#typedef-recoverableuplinkqueue_tuplinktypesequence_type) + - [RecoverableUplinkQueue_t::initializeFromFram](#recoverableuplinkqueue_tuplinktypeinitializefromfram) + - [RecoverableUplinkQueue_t::size](#recoverableuplinkqueue_tuplinktypesize) + - [RecoverableUplinkQueue_t::getBufferSize](#recoverableuplinkqueue_tuplinktypegetbuffersize) + - [RecoverableUplinkQueue_t::peek_head](#recoverableuplinkqueue_tuplinktypepeek_head) + - [RecoverableUplinkQueue_t::put_tail](#recoverableuplinkqueue_tuplinktypeput_tail) + - [RecoverableUplinkQueue_t::newSequenceNumber](#recoverableuplinkqueue_tuplinktypenewsequencenumber) +- [Discussion](#discussion) + - [What changes must I make to the message protocol between device and cloud?](#what-changes-must-i-make-to-the-message-protocol-between-device-and-cloud) + - [Should I add a downlink message to reset the buffer?](#should-i-add-a-downlink-message-to-reset-the-buffer) + - [What changes must I make to an existing device application?](#what-changes-must-i-make-to-an-existing-device-application) + - [How should my device application find messages with sequence numbers](#how-should-my-device-application-find-messages-with-sequence-numbers) + - [What changes must I make to my cloud application?](#what-changes-must-i-make-to-my-cloud-application) + - [What about multiple recoverable message sequences, e.g. on different LoRaWAN ports?](#what-about-multiple-recoverable-message-sequences-eg-on-different-lorawan-ports) + - [Why not use the uplink counter to detect missing messages?](#why-not-use-the-uplink-counter-to-detect-missing-messages) + + + + + +## Introduction + +In many situations, users may not be able to accept the inherent lossiness of uplink data transmits. Even if the device sends a confirmed uplink, the confirmation comes from the network server, not from the consuming application. The transmission still might be dropped before it gets to permanent storage in the application. + +MCCI Catena devices for LPWAN applications have an FRAM storage module that is able to store small quantities of data without worrying about wear leveling or wear out. In combination with a suitable application design, this can be used to implement a reasonable amount of data retention and playback. + +## Assumptions + +### Message loss scenarios + +We cater to two different scenarios for message loss. + +1. **Sporadic message loss due to interference or congestion**. In MCCI's testing, this rate is pretty low and predictable; we observed 3% loss in an application with 100 devices transmitting every 6 minutes and a single gateway. + +2. **Bulk message loss due to persistent network problems**. This could be due to a gateway being down, due to lack of gateway connection to the Internet, due to problems with the LoRaWAN network server, or due to problems with the application server. In this case, a large group of consecutive messages may be lost. + +In case #2, users will often try to reset the devices and gateways to restore service, and they won't always be able to observe when service has been restored. The application and protocol must be designed to be robust in the face of determined human intervention. + +## Basic Design + +Two entities cooperate in this design. They are: + +1. the firmware in the Catena device that transmits messages, or the "**device application**". +2. the software in the application that receives messages, or the "**cloud application**". + +We distinguish between "one-shot" and "recoverable" uplinks. One-shot uplinks are not recorded in FRAM and cannot be re-transmitted via this protocol. Recoverable uplinks are uniquely identified for recovery, and the application can later request that they be retransmitted (as long as they remain in memory). + +The device application maintains a sliding window of recoverable uplinks in the FRAM on the device. + +The device application also maintains a sequence counter for reliable uplink messages. This sequence counter is incremented for each message and enough number of bits of the counter are transmitted with each uplink, so that the cloud application can both determine when a message has been dropped, and know the identifier of the message that has been dropped. + +The sliding window always advances. When the device is initialized, the window is empty, but as recoverable uplinks are transmitted, they are appended to the window. Once the window becomes full, the oldest message is discarded prior to appending a new message. + +When the cloud application determines that a recoverable uplink must be retransmitted, the cloud application sends a downlink to the device application, including the desired sequence number of the recoverable uplink. In response, the device searches the recoverable uplink queue and retransmits the message with the requested sequence number. + +Because the sliding window always advances, it is possible that by the time the device receives the request for a given recoverable uplink, the message may have been overwritten. If so, the device responds in a suitable way (as determined by the use case) -- for example, it might transmit a "not available" message, or it might ignore the request. + +## APIs + +### `Catena_FramRingBuf.h` + +This header file defines the APIs described in this section. + + +### `class McciCatena::FramRingBuffer_t` + +This is the concrete class underlying the abstract class template. + +### `class RecoverableUplinkQueue_t` + +This class template is used to generate a global that is used for subsequence operations. This must be a singleton, because the corresponding object in the FRAM is also a singleton. + +### `typedef RecoverableUplinkQueue_t::size_type` + +This unsigned integral type is used for recording the sizes of objects (and things computed from the same) in the FRAM buffer. It's defined to be `uint16_t`. + +### `typedef RecoverableUplinkQueue_t::sequence_type` + +This unsigned integral type is used for sequence numbers. It's defined to be `uint16_t`. + +### `RecoverableUplinkQueue_t::initializeFromFram()` + +This method should be called once, during `setup()` processing. The argument `version` is user defined; if not equal to the version number in the FRAM image of the object, then the FRAM image is assumed not to be compatible with this version of the firmware, and is discarded. (Similarly, if the size of `uplinkType` in the running firmware is different than the size recorded in the FRAM, the previous FRAM image is assumed to be incompatible and is discarded). + +### `RecoverableUplinkQueue_t::size()` + +This method returns the current number of entries stored in the queue. It is useful for iterating over the queue. + +### `RecoverableUplinkQueue_t::getBufferSize()` + +This method returns the number of bytes allocated in total for storing data. + +### `RecoverableUplinkQueue_t::peek_head()` + +This method indexes from the head of the saved queue, and returns the sequence number and the data buffer. The explicit result is `true` if the index is less than `pQueue->size()`, and `false` otherwise. + +### `RecoverableUplinkQueue_t::put_tail()` + +This method appends `uplinkType` item to the the tail of the queue with an associated sequence number. It drops the head of the queue if necessary to create room. It returns `true` if all the FRAM operations were successful, `false` otherwise. + +### `RecoverableUplinkQueue_t::newSequenceNumber()` + +This method assigns a new sequence number, and updates the FRAM so that the next successful call will return a different sequence number, even if the system reboots in between. + +The method returns `true` if all FRAM operations success, `false` otherwise. If `false`, the sequence number out parameter is not changed. + +## Discussion + +### What changes must I make to the message protocol between device and cloud? + +1. You must draw a distinction between "one-shot" and "recoverable" uplinks. + +2. You must change the over-the-air data structure of a "recoverable uplink" as compared to one-shot uplinks: recoverable uplinks must include a new field for the sequence number, and one-shot uplinks must not contain that field. There are many ways to do this. If using the MCCI scheme of a bit map to indicate presence/absence of optional fields, you could define a new field (with its bit) for the sequence number. For simplicity, we recommend use of a 16-bit sequence number in the message over the air. + + As always, you must bear in mind the US 11-byte limitation for SF10 messages. + +3. You must add one or more downlinks to allow the cloud application to request retransmission of stored recoverable uplinks. The recommended change is a message that carries 4 bytes of payload, a starting sequence number and an ending sequence number. If the ending sequence number is less than the starting sequence number, then the device should retransmit all messages with sequence numbers in the ranges [start..`0xFFFF`] and [0..end]. + +4. You must define the retransmit format to be used when re-transmitting recoverable uplinks. This format must include enough information for the cloud application to reconstruct the original *time* of the recovered uplink, so it can put it in the database at the appropriate time. It's up to you how to do this; you could record a timestamp as part of the recoverable uplink, or you could simply use the known update period to approximate the original time of the uplink. This must not be distinguishable from a "first time" transmit. One way to do this is to use a different LoRaWAN port for re-transmissions. + +#### Should I add a downlink message to reset the buffer? + +You should not ordinarily reset the recoverable uplink buffer. It is therefore creating a dangerous temptation to add such a message to the protocol. However, because of bugs, it might be necessary to reset the buffer via a downlink; so you must use your judgment as to whether to implement this. + +### What changes must I make to an existing device application? + +Here's a list that's not necessarily exhaustive. + +1. Create a data structure for your uplink messages that's either fixed in size, or self-sizing with a fixed maximum size. Note that the design here requires that the recoverable uplinks stored in FRAM have a fixed size. So the usual message with optional fields used over the air *is not* used for FRAM storage. (You may still use variable length over the air; but if so, you must convert the fixed-length storage to variable at the appropriate time.) For the purposes of discussion, we'll use the name `UserData_t` for this data type, but you should use a more informative name in your application. + +2. Define a global variable of type `RecoverableUplinkQueue_t`. For the purposes of this discussion, we'll call it `gRecoverableUplinks`. + +3. During `setup()`, call `gRecoverableUplinks.initialize()` to set things up. + +4. When you want to generate a recoverable uplink sequence number, call `gRecoverableUplinks.issueSequenceNumber()` to generate it. + +5. When you send a recoverable uplink, include a unique sequence number in the message, and then create a `UserData_t` object from the uplinked data. Call `gRecoverableUplinks.putTail(sequenceNumber, userData)` to record the data. + +6. **Make sure your application doesn't send recoverable uplinks too often.** We often send a flurry of uplink messages after system boot, to assist with commissioning systems. *Do not make these messages recoverable.* Otherwise if the user gets into a commissioning loop because of other problems, the commissioning uplinks will overwrite valuable older data. You must rate limit the creation of recoverable uplinks at all times and in all circumstances. + + Best practice is not to record a recoverable uplink until after you've completed a service cycle. I.e., if the device sends messages every 8 hours, don't send a recoverable uplink until the first eight hours have passed. + +7. **Consider adding a polling cycle after receiving a downlink**. The MCCI Model 4811 automatically starts sending Class A poll messages every so often whenever it receives a command downlink from the cloud application. This makes the 4811 much more responsive to the cloud app. Otherwise on devices with 8 hour uplink cycles, you need to wait 8 hours for each control downlink to be transmitted. + +8. **Consider transmitting the size of the ring buffer (in messages) at start up**. This allows the cloud to auto-configure its recovery algorithm. + +9. **Consider transmitting the current sequence number at start up**. This allows the cloud to get resynchronized with the device, if needed. + +10. **You must define regression tests**. This is complicated, and so you must define suitable test sequences so you can confirm that the device on its own is working correctly. + +#### How should my device application find messages with sequence numbers + +If asked to retransmit one or more messages, your code should do something like this: + +```c++ +extern RecoverableUplinkQueue_t gQueue; + +bool retransmit_matching_uplink(sequence_type start, sequence_type end) + { + uplinkType item; + sequence_type seqno; + const bool no_wraparound = start <= end; + + for (size_type i = 0; gQueue.peek_head(seqno, item, i); ++i) + { + if (no_wraparound && start <= seqno && seqno <= end) + do_retransmit(item); + else if (!noWraparound && (start <= seqno || seqno <= end)) + do_retransmit(item); + else + /* skip */; + } + } +``` + +In other words, you iterate over all items in the queue, from oldest to newest, and consider the sequence number of each. + +- If the range of numbers given is normal (`start` <= `end`), then we treat this as a normal range, and we transmit any item that is in the range `start` <= item's sequence number <= `end`. + +- Otherwise, the range given is assumed to include the wrap-around due to integer overflow. For example if `start` is `0xFFFE` and `end` is `1`, this is interpreted as a request to transmit any message with sequence number `0xFFFE`, `0xFFFF`, `0`, or `1`. Note that in this case, `start` will be greater then `end`, and we can use `(start <= seqno || seqno <= end)` to match all the desired sequence numbers. + +### What changes must I make to my cloud application? + +1. You must define a mechanism to discover missing uplinks. + +2. You must define a policy for fetching missing uplinks. You don't want to do this too often, but you must detect and request both single messages and blocks of missing messages. If you request a missing message and the device tells you that the message is not available, don't request it again. + +3. You must define how to set the timestamp when storing retransmitted uplinks, so that data is stored properly in the database. + +4. You must define suitable regression tests, so you can inject various test data sequences into the could application and confirm that it requests the appropriate recoverable uplinks from the device, injects them with the appropriate timestamp into the database, and deals properly with lost uplinks in response to recoverable uplink requests. + +### What about multiple recoverable message sequences, e.g. on different LoRaWAN ports? + +This is a good idea, but is not supported now, because of the limitations in the FRAM object implementation. + +### Why not use the uplink counter to detect missing messages? + +The LoRaWAN protocol includes an uplink counter that is used for message deduplication. Unfortunately, this counts messages at the mac layer, not at the application layer; and the mac layer is owned by the network, not by the application. The network can send messages that require responses; the receiving application will not be aware of these messages and so can't easily determine whether an application message was dropped or not. diff --git a/src/Catena.h b/src/Catena.h index 6cb10ce..b695ea6 100644 --- a/src/Catena.h +++ b/src/Catena.h @@ -64,6 +64,9 @@ Copyright notice: #elif defined(ARDUINO_MCCI_CATENA_4802) # include "Catena4802.h" # define CATENA_H_SUPER_ McciCatena::Catena4802 +#elif defined(ARDUINO_MCCI_MODEL_4917) +# include "Catena4917.h" +# define CATENA_H_SUPER_ McciCatena::Catena4917 /* fallback in case it's SAMD but not what we expect */ #elif defined(ARDUINO_ARCH_SAMD) # include "CatenaSamd21.h" diff --git a/src/Catena4917.h b/src/Catena4917.h new file mode 100644 index 0000000..01a781f --- /dev/null +++ b/src/Catena4917.h @@ -0,0 +1,63 @@ +/* + +Module: Catena4917.h + +Function: + class Catena4917: CatenaBase Platform to represent a Catena 4917 + +Copyright notice: + See accompanying LICENSE file. + +Author: + Pranau R, MCCI Corporation November 2022 + +*/ + +#ifndef _Catena4917_H_ /* prevent multiple includes */ +#define _Catena4917_H_ + +#pragma once + +#ifndef _CATENA491x_H_ +# include "Catena491x.h" +#endif + +namespace McciCatena { + +class Catena4917 : public Catena491x + { +public: + using Super = Catena4917; + + // no specific constructor. + Catena4917() {}; + + // uses default destructor + + // neither copyable nor movable + Catena4917(const Catena4917&) = delete; + Catena4917& operator=(const Catena4917&) = delete; + Catena4917(const Catena4917&&) = delete; + Catena4917& operator=(const Catena4917&&) = delete; + + virtual const char *CatenaName() const override { return "Catena 4917"; }; + virtual float ReadVbat(void) const override; + virtual float ReadVbus(void) const override; + +protected: + // we are required to provide a table of platforms + virtual void getPlatformTable( + const CATENA_PLATFORM * const * &vPlatforms, + size_t &nvPlatforms + ) override; + +private: + // the known platforms + static const CATENA_PLATFORM(* const vPlatforms[]); + static const size_t nvPlatforms; + }; + +} // namespace McciCatena + +/**** end of Catena4917.h ****/ +#endif /* _Catena4917_H_ */ \ No newline at end of file diff --git a/src/Catena491x.h b/src/Catena491x.h new file mode 100644 index 0000000..995351b --- /dev/null +++ b/src/Catena491x.h @@ -0,0 +1,106 @@ +/* + +Module: Catena491x.h + +Function: + class Catena491x: CatenaBase Platform to represent a Catena 491x + (such as the 4917). + +Copyright notice: + See accompanying LICENSE file. + +Author: + Pranau R, MCCI Corporation November 2022 + +*/ + +#ifndef _CATENA491X_H_ /* prevent multiple includes */ +#define _CATENA491X_H_ + +#pragma once + +#ifndef _CATENASTM32L0_H_ +# include "CatenaStm32L0.h" +#endif + +namespace McciCatena { + +class Catena491x : public CatenaStm32L0 + { +public: + using Super = CatenaStm32L0; + + // no specific constructor. + Catena491x() {}; + + // uses default destructor + + // neither copyable nor movable + Catena491x(const Catena491x&) = delete; + Catena491x& operator=(const Catena491x&) = delete; + Catena491x(const Catena491x&&) = delete; + Catena491x& operator=(const Catena491x&&) = delete; + + // LoRaWAN binding + class LoRaWAN /* forward */; + + enum ANALOG_PINS + { + APIN_VBAT_SENSE = A3, + APIN_VBUS_SENSE = A4, + }; + + enum ANALOG_CHANNELS + { + ANALOG_CHANNEL_A0 = 0, + ANALOG_CHANNEL_A1 = 5, + ANALOG_CHANNEL_A2 = 4, + ANALOG_CHANNEL_A3 = 3, + ANALOG_CHANNEL_A4 = 2, + ANALOG_CHANNEL_VBAT = ANALOG_CHANNEL_A3, + ANALOG_CHANNEL_VBUS = ANALOG_CHANNEL_A4, + ANALOG_CHANNEL_VREF = 17, + }; + + enum DIGITAL_PINS + { + PIN_STATUS_LED = D13, + PIN_SPI2_FLASH_SS = D19, + PIN_SPI2_MOSI = D23, + PIN_SPI2_MISO = D22, + PIN_SPI2_SCK = D24, + }; + + // methods + virtual bool begin() override; + +protected: + +private: + }; + +/* +|| The LoRaWAN class for the Catena 455x. Assumes The Things Network +*/ +class Catena491x::LoRaWAN : public CatenaStm32L0::LoRaWAN + { +public: + using Super = CatenaStm32L0::LoRaWAN; + + /* + || the constructor. We don't do anything at this level, the + || Super constructor does most of the work. + */ + LoRaWAN() {}; + + bool begin(Catena491x *pParent); + +protected: + +private: + }; + +} // namespace McciCatena + +/**** end of Catena491x.h ****/ +#endif /* _CATENA491X_H_ */ \ No newline at end of file diff --git a/src/CatenaBase.h b/src/CatenaBase.h index a1d8a1a..e6eeb4f 100644 --- a/src/CatenaBase.h +++ b/src/CatenaBase.h @@ -212,6 +212,8 @@ class CatenaBase fHasI2cLevelShifter = 1 << 21, //platform has LTR329 Lux sensor fHasLuxLtr329 = 1 << 22, + //platform has LIS2HH12 Accelerometer + fHasLIS2HH12 = 1 << 23, // special wiring variants all are offsets from M100... // we support up to 127 variants, becuase we have 7 diff --git a/src/Catena_Fram.h b/src/Catena_Fram.h index 73b0c95..c729488 100644 --- a/src/Catena_Fram.h +++ b/src/Catena_Fram.h @@ -199,7 +199,7 @@ class cFram::Cursor // take a located cursor and create an object if needed bool create(void); - // test whether a cursro is bound to an object + // test whether a cursor is bound to an object bool isbound() const { return this->m_uKey != cFramStorage::StandardKeys::kMAX; @@ -211,6 +211,35 @@ class cFram::Cursor return this->m_offset != cFramStorage::kInvalidOffset; } + bool isinitialized() const + { + return this->m_pFram != nullptr; + } + + bool initialize(cFram *pFram) + { + if (pFram == nullptr) + { + // invalid paramter + return false; + } + + else if (this->m_pFram == nullptr) + { + // not set + this->m_pFram = pFram; + return true; + } + else if (this->m_pFram == pFram) + { + // not changing. + return true; + } + else + // already set and trying to chagne. + return false; + } + // set up a cursor to match a standard item bool locate(const cFramStorage::StandardItem); @@ -234,6 +263,16 @@ class cFram::Cursor return this->get((uint8_t *)&v, sizeof(v)); } + // get an object. + template + bool get(T &v) + { + return this->get((uint8_t *)&v, sizeof(v)); + } + + // get a part of a value + bool getPartialValue(uint8_t *pBuffer, size_t nBuffer, size_t offset); + // put a buffer bool put(const uint8_t *pBuffer, size_t nBuffer); @@ -243,6 +282,16 @@ class cFram::Cursor return this->put((const uint8_t *)&v, sizeof(v)); } + // put an object + template + bool put(const T &v) + { + return this->put((const uint8_t *)&v, sizeof(v)); + } + + // put a part of a value + bool putPartialValue(size_t offset, const uint8_t *pBuffer, size_t nBuffer); + // parse a value bool parsevalue( const char *pValue, diff --git a/src/Catena_FramRingBuf.h b/src/Catena_FramRingBuf.h new file mode 100644 index 0000000..5ecb987 --- /dev/null +++ b/src/Catena_FramRingBuf.h @@ -0,0 +1,117 @@ +/* + +Module: Catena_FramRingBuf.h + +Function: + FRAM-based non-volatile ring buffer templates + +Copyright and License: + This file copyright (C) 2024 by + + MCCI Corporation + 3520 Krums Corners Road + Ithaca, NY 14850 + + See accompanying LICENSE file for copyright and license information. + +Author: + Terry Moore, MCCI Corporation August 2024 + +*/ + +#ifndef _Catena_FramRingBuf_h_ +#define _Catena_FramRingBuf_h_ /* prevent multiple includes */ + +#pragma once + +#ifndef _Catena_FramStorage_h_ +# include "Catena_FramStorage.h" +#endif + +#ifndef _Catena_Fram_h_ +# include "Catena_Fram.h" +#endif + + +namespace McciCatena { + +class FramRingBuffer_t; + +class FramRingBuffer_t + { +public: + using OverflowPolicy_t = cFramStorage::DataLogHeader_t::OverflowPolicy_t; + using size_type = cFramStorage::DataLogHeader_t::size_type; + using sequence_type = cFramStorage::DataLogHeader_t::sequence_type; + + // neither copyable nor movable + FramRingBuffer_t(const FramRingBuffer_t&) = delete; + FramRingBuffer_t& operator=(const FramRingBuffer_t&) = delete; + FramRingBuffer_t(const FramRingBuffer_t&&) = delete; + FramRingBuffer_t& operator=(const FramRingBuffer_t&&) = delete; + + // constructor + FramRingBuffer_t(uint8_t version, size_type itemsize, OverflowPolicy_t policy) + : m_logheader(version, this->getBufferSize(), itemsize + sizeof(sequence_type), policy) + {} + +protected: + bool initializeFromFram(uint8_t version, size_type itemsize, OverflowPolicy_t policy); + bool put_tail(sequence_type seqnum, const uint8_t *pBuffer, size_type nBuffer); + bool peek_front(sequence_type &seqnum, uint8_t *pBuffer, size_type nBuffer, size_type iEntry = 0); + bool pop_front(); + +public: + size_type size() const + { + return this->m_logheader.size() - sizeof(sequence_type); + } + + size_type getBufferSize() const + { + return cFramStorage::kDataLogBufferSize; + } + + bool newSequenceNumber(sequence_type &sequenceNumber); + + bool clear(); + +private: + cFramStorage::DataLogHeader_t m_logheader; + cFram::Cursor m_logBufferCursor {nullptr}; + cFram::Cursor m_logHeaderCursor {nullptr}; + }; + +template +class RecoverableUplinkQueue_t : public FramRingBuffer_t + { +public: + // neither copyable nor movable + RecoverableUplinkQueue_t(const RecoverableUplinkQueue_t&) = delete; + RecoverableUplinkQueue_t& operator=(const RecoverableUplinkQueue_t&) = delete; + RecoverableUplinkQueue_t(const RecoverableUplinkQueue_t&&) = delete; + RecoverableUplinkQueue_t& operator=(const RecoverableUplinkQueue_t&&) = delete; + + RecoverableUplinkQueue_t(uint8_t version, OverflowPolicy_t policy) + : FramRingBuffer_t(version, sizeof(cDataItem), policy) + {} + + bool put_tail(sequence_type sequenceNumber, cDataItem &refItem) + { + return this->FramRingBuffer_t::put_tail(sequenceNumber, (const uint8_t *)&refItem, sizeof(refItem)); + } + + bool peek_front(sequence_type &sequenceNumber, cDataItem &refItem, size_type iEntry = 0) + { + return this->FramRingBuffer_t::peek_front(sequenceNumber, (uint8_t *)&refItem, sizeof(refItem), iEntry); + } + + bool initializeFromFram(uint8_t version) + { + return this->FramRingBuffer_t::initializeFromFram(version, sizeof(cDataItem), OverflowPolicy_t::kDropOldest); + }; + }; + +} // namespace McciCatena + +#endif /* _Catena_FramRingBuf_h_ */ diff --git a/src/Catena_FramStorage.h b/src/Catena_FramStorage.h index 08e745e..a5abf31 100644 --- a/src/Catena_FramStorage.h +++ b/src/Catena_FramStorage.h @@ -49,6 +49,9 @@ class cFramStorage kBme680Cal = 16, kAppConf = 17, kLmicSessionState = 18, + kUplinkInterval = 19, + kDataLogHeader = 20, + kDataLogBuffer = 21, // when you add something, also update McciCatena::cFramStorage::vItemDefs[]! kMAX }; @@ -101,7 +104,11 @@ class cFramStorage static const StandardItem vItemDefs[kMAX]; - class Object; + class Object; // forward reference. + class DataLogHeader_t; // forward reference + struct DataLogBuffer_t; // forward referencce + + static constexpr uint16_t kDataLogBufferSize = 2048; enum : uint32_t { @@ -388,6 +395,368 @@ struct cFramStorage::Object : public cFramStorage::ObjectRaw bool isValid(void) const; }; +/// +/// @brief the header for the FRAM-based data log +/// +/// @details +/// We support a limited data logging function, for storing a FIFO queue of fixed-size +/// records in a buffer in FRAM. This is implememnted with two FRAM objects. A single +/// DataLogBuffer_t is allocated storing the data elements; pointers are retained using +/// a separate cFramStorage::DataLogHeader_t object. +/// +/// Unlike the buffer, the header is maintained using normal cFramStorage techniques; +/// it is therefore updated reliably. It contains the classic ring-buffer insert and +/// remove pointers. It also needs to remember the size of the buffer, so it knows +/// how to handle wrap-around. So that we can handle software updates sanely, it keeps +/// an idea of the size of the objects recorded and of the data format; if care is taken +/// to update the data format when things change, software should be able to recognize +/// a log buffer that it doesn't understand and take appropriate action. +/// +/// The names of methods and type for the Data Logging Header are modeled on the names +/// used in the C++ standard header . We don't implement all the operations, +/// from , but since a ring buffer is fundamentally a dequeue, +/// it seems sane to follow that naming convention. +/// +/// Bear in mind, however, that the methods of the DataLogHeader_t do not move data +/// to the FRAM; there is a wrapper driver that takes care of this. The code here +/// manipulates the indices but another layer needs to carefully stage the writing +/// of the ring buffer headers, and stage the writing of the header vs the data +/// at the appropriate moments. +/// +struct cFramStorage::DataLogHeader_t + { +public: + using size_type = uint16_t; + using sequence_type = uint16_t; + + enum class OverflowPolicy_t : uint8_t + { + kDropOldest = 0, + kDropNew = 1, + }; + + constexpr DataLogHeader_t() + : m_version(0) + , m_buffersize(0) + , m_insert(0) + , m_remove(0) + , m_nDropped(0) + , m_overflowPolicy(OverflowPolicy_t::kDropOldest) + , m_itemsize(1) + , m_numSlots(0) + , m_sequenceNumber(0) + {} + + constexpr DataLogHeader_t(uint8_t version, size_type buffersize, uint16_t itemsize, OverflowPolicy_t policy) + : m_version(version) + , m_buffersize(buffersize < 1 ? 1 : buffersize) + , m_insert(0) + , m_remove(0) + , m_nDropped(0) + , m_overflowPolicy(policy) + , m_itemsize(itemsize < 1 ? 1 : itemsize) + , m_numSlots(this->m_buffersize / this->m_itemsize) + , m_sequenceNumber(0) + { + } + + /// + /// @brief check whether the structure read from FRAM is consistent with current software + /// @param version [in] the version tag for the data log buffer used by the current software. + /// @param buffersize [in] the size of the log buffer desired by the current software. + /// @param itemsize [in] the size of each item in the buffer + /// @return \c true if things look ok, \c false if things look out of sync. + /// + /// \c version only needs to change if the data format changes but the size of an item doesn't change. + /// \c buffersize is normally read from the buffer object; the software's idea of the size should + /// only be used when creating the buffer object. + /// + bool queryVersionMatch(uint8_t version, uint16_t buffersize, uint16_t itemsize) const + { + return version == this->m_version && + buffersize == this->m_buffersize && + itemsize == this->m_itemsize + ; + } + + constexpr bool initialize(uint8_t version, uint16_t buffersize, uint16_t itemsize, OverflowPolicy_t policy) + { + bool result = true; + + if (buffersize == 0) + { + result = false; + buffersize = 1; + } + if (itemsize == 0) + { + result = false; + itemsize = buffersize; + } + size_type const numSlots = buffersize / itemsize; + if (numSlots < 1) + { + result = false; + } + + this->m_version = version; + this->m_buffersize = buffersize; + this->m_insert = 0; + this->m_remove = 0; + this->m_nDropped = 0; + this->m_overflowPolicy = policy; + this->m_numSlots = numSlots; + return result; + } + + /// @brief calculate the maximum capacity of the buffer + /// @return number of elements that will fit. + /// + /// @details If itemsize is zero, the result will be zero. + /// + /// @note The name is based on the similar function in . + /// \c this->max_size() is also the largest permitted index value + /// in the buffer. It's one less than the allocation size of the + /// buffer (in units of itemsize) + constexpr size_type max_size() const + { + return this->m_numSlots - 1; + } + + /// @brief check whether the ring-buffer control structure is self-consistent. + /// @return true if things look reasonable. + bool queryConsistent() const + { + // basic checks + if (this->m_itemsize == 0) + return false; + + uint16_t const nItems = this->m_buffersize / this->m_itemsize; + if (nItems == 0) + return false; + + if (nItems != this->m_numSlots) + return false; + + if (this->m_insert >= nItems || this->m_remove >= nItems) + return false; + + return true; + } + + /// + /// @brief empty the ring buffer header + /// + /// @details + /// The reset pointer is advanced to the head. + /// + /// @note The name is based on the similar function in . + /// + void clear() + { + if (this->m_insert < this->max_size()) + this->m_remove = this->m_insert; + else + this->m_remove = this->m_insert = 0; + } + + /// @brief return the number of elements currently in the buffer + /// @return the count, in 0 .. max_size(). + constexpr size_type size() const + { + if (this->m_remove <= this->m_insert) + return this->m_insert - this->m_remove; + else + return this->m_insert + this->m_numSlots - this->m_remove; + } + + /// + /// @brief allocate room for entry at end of queue, return index of entry + /// @param insertIndex [out] is set to the index of the inserted entry + /// @return \c true if an entry was successfully added; \c false otherwise. + /// If successful, \c insertIndex is set to the index + /// of the new entry; otherwise its value is not defined. + /// + /// Assumses consistency. + /// + bool push_back(size_type &insertIndex) + { + return this->push_back(&insertIndex); + } + + /// + /// @brief make room (if possible) for an appended entry. + /// @param [in] pInsertIndex - optional pointer to variable to receive + /// insertion slot index. + /// @return \c true if an entry was made available, \c false if not. + /// + /// As a side effect, if data will be lost (either by advancing over + /// a slot that's already occupied, or by indicating that no entry + /// is available), the discarded-data counter is incremented. + /// + bool push_back(size_type *pInsertIndex = nullptr) + { + if (this->size() >= this->max_size()) + { + ++this->m_nDropped; + + return false; + } + + // if pointing at last entry, wrap aound + size_type const oldInsert = this->m_insert; + if (oldInsert == this->max_size()) + this->m_insert = 0; + else + this->m_insert = oldInsert + 1; + + if (pInsertIndex) + *pInsertIndex = oldInsert; + + return true; + } + + /// + /// @brief return the slot index of the entry at the head of (or desired relative + /// position in) the queue. + /// @param [out] entryIndex - if result is \c true, set to the slot index of the + /// requested entry. + /// @param [in] iDesiredEntry - optional index of desired entry. If omitted or zero, + /// the head entry is desired; if 1, the second entry is + /// desired, and so forth. + /// @return \c true if and only if the desired entry is present in the queue. + /// + constexpr bool peek_front(size_type &entryIndex, size_type iDesiredEntry = 0) const + { + if (this->size() < iDesiredEntry) + return false; + else + { + // no need to compute modulus for head. + if (iDesiredEntry == 0) + entryIndex = this->m_remove; + else + entryIndex = (this->m_remove + iDesiredEntry) % this->m_numSlots; + + return true; + } + } + + /// + /// @brief remove the head element from the queue. + /// @return \c true if an entry was removed, \c false if queue was + /// already empty. + /// + bool pop_front() + { + if (this->size() == 0) + return false; + else + { + if (this->m_remove == this->max_size()) + this->m_remove = 0; + else + ++this->m_remove; + + return true; + } + } + + /// + /// @brief convert a slot index to a byte offset in the buffer + /// @param entryIndex - + /// @return the byte index, given the item size. + /// + constexpr size_type indexToOffset(size_type entryIndex) const + { + return entryIndex * this->m_itemsize; + } + + constexpr size_type queryDropped(void) const + { + return this->m_nDropped; + } + + size_type adjustDropped(size_type adjustment) + { + auto nDropped = this->m_nDropped; + + if (nDropped < adjustment) + { + adjustment = nDropped; + } + nDropped -= adjustment; + this->m_nDropped = nDropped; + return nDropped; + } + + constexpr size_type queryItemSize() const + { + return this->m_itemsize; + } + + constexpr OverflowPolicy_t queryOverflowPolicy() const + { + return this->m_overflowPolicy; + } + + constexpr size_type absoluteIndex(size_type iEntry) const + { + return (this->m_remove + iEntry) % this->m_numSlots; + } + + constexpr sequence_type querySequenceNumber() const + { + return this->m_sequenceNumber; + } + + void setSequenceNumber(uint16_t sequenceNumber) + { + this->m_sequenceNumber = sequenceNumber; + } + + void advanceSequence() + { + ++this->m_sequenceNumber; + } + + constexpr size_type queryNumSlots() const + { + return this->m_numSlots; + } + +private: + uint16_t m_sequenceNumber; ///< monotonically increasing sequence number + size_type m_insert; ///< insert pointer, in slots + size_type m_remove; ///< removal pointer, in slots + size_type m_buffersize; ///< buffer size in bytes + size_type m_itemsize; ///< item size in bytes + size_type m_numSlots; ///< number of slots (capacity + 1) + uint16_t m_nDropped; ///< count of dropped entries since last succesful insert + uint8_t m_version; ///< version -- set by caller, used for consistency + /// checks on boot. Change this if interpretation of + /// data buffer changed -- the API doesn't support + /// that, and you need handle it at the outer layer, + /// or just discard older data. + OverflowPolicy_t m_overflowPolicy; ///< what do do when the buffer is full, Choices + /// are either to discard the new data, or + /// discard the oldest data. + }; + +/// +/// @brief The buffer data type for the FRAM-based data log +/// +/// @details +/// Becasue of implementation restrictions in cFramStorage, we need a data type with +/// a size for the buffer. The buffer is not meant to be read all at once into memory, +/// and we don't expect ever to instantiate an object of this type. But this is how +/// we set the size: by declaring an object with the appropriate size. +/// +struct cFramStorage::DataLogBuffer_t + { + uint8_t m_buffer[kDataLogBufferSize]; + }; + }; // namespace McciCatena /**** end of Catena_FramStorage.h ****/ diff --git a/src/Catena_Guids.h b/src/Catena_Guids.h index 1194277..4204aae 100644 --- a/src/Catena_Guids.h +++ b/src/Catena_Guids.h @@ -245,5 +245,9 @@ Copyright notice: #define GUID_HW_CATENA_4802_BASE(f) \ MCCIADK_GUID_GEN_INIT(f, 0xdaaf345e, 0xb5d5, 0x4a32, 0xa3, 0x03, 0x3a, 0xc7, 0x0b, 0x81, 0xd2, 0x60) +// {d9d35ffd-1859-4686-900c-dd7fd5941886} +#define GUID_HW_CATENA_4917_BASE(f) \ + MCCIADK_GUID_GEN_INIT(f, 0xd9d35ffd, 0x1859, 0x4686, 0x90, 0x0c, 0xdd, 0x7f, 0xd5, 0x94, 0x18, 0x86) + /**** end of catena_guids.h ****/ #endif /* _CATENA_GUIDS_H_ */ diff --git a/src/Catena_Platforms.h b/src/Catena_Platforms.h index 2dcf244..40b89af 100644 --- a/src/Catena_Platforms.h +++ b/src/Catena_Platforms.h @@ -82,6 +82,8 @@ extern const CATENA_PLATFORM gkPlatformCatena4630; extern const CATENA_PLATFORM gkPlatformCatena4801; extern const CATENA_PLATFORM gkPlatformCatena4802; +extern const CATENA_PLATFORM gkPlatformCatena4917; + } /* namespace McciCatena */ /**** end of Catena_Platforms.h ****/ diff --git a/src/lib/Catena_Fram.cpp b/src/lib/Catena_Fram.cpp index 8a529bb..7209c9d 100644 --- a/src/lib/Catena_Fram.cpp +++ b/src/lib/Catena_Fram.cpp @@ -877,6 +877,36 @@ McciCatena::cFram::Cursor::get( return false; } +bool +McciCatena::cFram::Cursor::getPartialValue( + uint8_t *pBuffer, + size_t nBuffer, + size_t offsetInValue + ) + { + if (! this->m_pFram->isReady()) + return false; + if (this->m_offset == cFramStorage::kInvalidOffset) + return false; + + auto const overallSize = this->m_uSize; + if (offsetInValue > overallSize) + offsetInValue = overallSize; + if (nBuffer > overallSize - offsetInValue) + nBuffer = overallSize - offsetInValue; + + if (nBuffer == 0) + return true; + + return this->m_pFram->read( + this->m_offset + cFramStorage::dataOffset(this->m_uSize, this->m_uVer) + offsetInValue, + pBuffer, + nBuffer + ); + + return false; + } + bool McciCatena::cFram::Cursor::put( const uint8_t *pBuffer, @@ -908,6 +938,106 @@ McciCatena::cFram::Cursor::put( nBuffer ); } + +/* + +Name: cFram::Cursor::putPartialValue() + +Function: + Modify contents of the underlying Fram value in place. + +Definition: + bool cFram::Cursor::putPartialValue( + size_t offsetInValue, + const uint8_t *pBuffer, + size_t nBuffer + ); + +Description: + This API allows a program to modify a portion of an FRAM + data value without having to do a read/modify/write on the + entire value. This is particularly convenient for FRAM entries + that are large, as it save lots of useless read/write cycles. + + pBuffer/nBuffer describe the range of bytes to be written. + offsetInValue gives the starting byte index within the value. + + The routine knows the szie of the overall object in FRAM, and + won't write beyond its limits. If offsetInValue is too large, + the routine does nothing; if offsetInValue in in range, but + nBuffer would extend beyond the defined length of the object, + nBuffer is trimmed to stay in limits. + +Returns: + No explicit result. + +Notes: + + +*/ + +bool +McciCatena::cFram::Cursor::putPartialValue( + size_t offsetInValue, + const uint8_t *pBuffer, + size_t nBuffer + ) + { + static const char FUNCTION[] = "cFram::Cursor::putPartialValue"; + + if (! this->islocated() || + ! this->isbound()) + { + gLog.printf( + gLog.kBug, + "%s: can't put to un%s cursor: " + "uSize(0x%x) uKey(0x%x) uVer(%u) offset(0x%x)\n", + FUNCTION, + (! this->islocated()) ? "located" : "bound", + this->m_uSize, + this->m_uKey, + this->m_uVer, + this->m_offset + ); + return false; + } + + auto const & item = cFramStorage::vItemDefs[this->m_uKey]; + + if (item.isReplicated()) + { + gLog.printf( + gLog.kBug, + "%s: object is replicated, partial update not supported: " + "uSize(0x%x) uKey(0x%x) uVer(%u) offset(0x%x)\n", + FUNCTION, + this->m_uSize, + this->m_uKey, + this->m_uVer, + this->m_offset + ); + return false; + } + + auto const overallSize = this->m_uSize; + if (offsetInValue > overallSize) + offsetInValue = overallSize; + if (nBuffer > overallSize - offsetInValue) + nBuffer = overallSize - offsetInValue; + + if (nBuffer == 0) + return true; + + return this->m_pFram->write( + this->m_offset + offsetInValue + + cFramStorage::dataOffset( + item.getSize(), + 0 + ), + pBuffer, + nBuffer + ); + } bool McciCatena::cFram::Cursor::getitem( diff --git a/src/lib/Catena_FramRingBuf.cpp b/src/lib/Catena_FramRingBuf.cpp new file mode 100644 index 0000000..bf29908 --- /dev/null +++ b/src/lib/Catena_FramRingBuf.cpp @@ -0,0 +1,395 @@ +/* + +Module: Catena_Fram_commands.cpp + +Function: + McciCatena::cFram::addCommands() and command processors. + +Copyright notice: + See accompanying LICENSE file. + +Author: + Terry Moore, MCCI Corporation March 2017 + +*/ + +#include "Catena_FramRingBuf.h" + +#include "CatenaBase.h" + +using namespace McciCatena; + +/* + +Name: McciCatena::FramRingBuffer_t::initializeFromFram() + +Function: + Initialize the in-memory copy of the log buffer control + structure from FRAM. + +Definition: + bool McciCatena::FramRingBuffer_t::initializeFromFram( + uint8_t version, // the item version number + size_type itemsize, // the item size + OverflowPolicy_t policy // the overflow policy to use + ); + +Description: + If not yet inititialized, read the log header from FRAM. If + not found, create one; otherwise either use what we read or + just initailize (due to code changes). Make sure there's a log + buffer in the FRAM. + +Returns: + true if successful, false otherwise. + +Notes: + + +*/ + +#define FUNCTION "FramRingBuffer_t::initializeFromFram" + +bool +FramRingBuffer_t::initializeFromFram( + uint8_t version, size_type base_itemsize, OverflowPolicy_t policy + ) + { + auto const pCatena = CatenaBase::pCatenaBase; + const size_t itemsize = base_itemsize + sizeof(sequence_type); + auto pFram = pCatena->getFram(); + bool fMustInitializeLogHeader = false; + + if (! this->m_logBufferCursor.isinitialized()) + { + auto const r1 = this->m_logBufferCursor.initialize(pFram); + auto const r2 = this->m_logHeaderCursor.initialize(pFram); + + if (! (r1 && r2)) + return false; + + // point it at the log buffer if it exists + this->m_logBufferCursor.locate(cFramStorage::StandardKeys::kDataLogBuffer); + this->m_logHeaderCursor.locate(cFramStorage::StandardKeys::kDataLogHeader); + } + + // start by establishing that there is a log-buffer object + if (! this->m_logBufferCursor.islocated()) + { + // buffer object doesn't exist! Try to create it. + if (! (this->m_logBufferCursor.create() && this->m_logHeaderCursor.create())) + return false; + + // created log buffer sucessfully: we therfore need to initialize the + // on-FRAM header from an empty header + fMustInitializeLogHeader = true; + } + // buffer object exists, what about the header? + else if (! this->m_logHeaderCursor.islocated()) + { + if (! this->m_logHeaderCursor.create()) + return false; + + // created log header successfully -- must initailize + fMustInitializeLogHeader = true; + } + // header exists: read it and validate it. + else + fMustInitializeLogHeader = false; + + + cFramStorage::DataLogHeader_t framHeader; + + if (! fMustInitializeLogHeader) + { + // read it. + do { + if (! this->m_logHeaderCursor.get((uint8_t *)&framHeader, sizeof(framHeader))) + { + // failed. must initialize. + fMustInitializeLogHeader = true; + break; + } + + // verify + if (! framHeader.queryConsistent()) + { + // failed. must initialize. + fMustInitializeLogHeader = true; + break; + } + + // compare to our parameters. + if (framHeader.queryVersionMatch( + version, + cFramStorage::kDataLogBufferSize, + itemsize)) + { + // failed: must initialize. + } + } while (false); + } + + if (fMustInitializeLogHeader) + { + if (! this->m_logheader.initialize(version, this->getBufferSize(), itemsize, policy)) + return false; + else if (! this->m_logHeaderCursor.put(this->m_logheader)) + return false; + else + return true; + } + else + { + // just set the header + this->m_logheader = framHeader; + return true; + } + } + +#undef FUNCTION + +/* + +Name: FramRingBuffer_t::put_tail() + +Function: + Append an object to the end of the ring buffer, if possible. + +Definition: + bool FramRingBuffer_t::put_tail( + sequence_type sequenceNumber, + const uint8_t *pBuffer, + size_type nBuffer + ); + +Description: + A slot is allocated for an item, and the item is appended to the buffer. The + data is written before we update the ring buffer pointers, so that a failure + while updating just results in the data being discarded. + +Returns: + true if data was written, false otherwise. + +Notes: + This is a protected method; typically only the abstract template + can call this. Regardless of the value of nBuffer, at most itemsize + bytes will be written. If nBuffer is really variable length, then + it must be self-sizing. + + If overwriting old entries, we must first delete the old head and + confirm the deletion. Then we can append. + + This call advances the sequence number. + +*/ + +#define FUNCTION "FramRingBuffer_t::put_tail" + +bool +FramRingBuffer_t::put_tail( + sequence_type sequenceNumber, + const uint8_t *pBuffer, + size_type nBuffer + ) + { + const size_type max_buffer = this->m_logheader.queryItemSize() - sizeof(sequence_type); + + if (nBuffer > max_buffer) + nBuffer = max_buffer; + + size_type itemIndex; + if (! this->m_logheader.push_back(&itemIndex)) + { + // no room, discarding this one. + if (this->m_logheader.queryOverflowPolicy() == cFramStorage::DataLogHeader_t::OverflowPolicy_t::kDropNew) + return false; + + // otherwise: make an empty slot, and commit so that a crash while writing + // new entry won't result in a partially corrupt but "valid" entry. + if (! (this->m_logheader.pop_front() /* delete head */ && + this->m_logHeaderCursor.put(this->m_logheader) /* commit */ && + this->m_logheader.push_back(&itemIndex)) /* reserve space */) + return false; + } + + // convert itemIndex to byte index + auto const byteIndex = this->m_logheader.indexToOffset(itemIndex); + + // + // write the sequence number + // force little-endian. Throw a compile error if + // we decide to change size of sequence_type. + // + bool fResult; + static_assert(sizeof(sequence_type) == 2); + uint8_t sequence_buffer[2]; + + sequence_buffer[0] = uint8_t(sequenceNumber & 0xFF); + sequence_buffer[1] = uint8_t(sequenceNumber >> 8); + + fResult = this->m_logBufferCursor.putPartialValue(byteIndex, sequence_buffer, sizeof(sequence_buffer)); + + // write the buffer + if (fResult) + fResult = this->m_logBufferCursor.putPartialValue( + byteIndex + sizeof(sequenceNumber), + pBuffer, nBuffer + ); + + if (fResult) + { + // write the pointers, committing the new item. + fResult = this->m_logHeaderCursor.put(this->m_logheader); + } + + return fResult; + } + +#undef FUNCTION + +/* + +Name: FramRingBuffer_t::peek_front() + +Function: + Read an entry from the queue, indexing from head == 0. + +Definition: + bool FramRingBuffer_t::peek_front( + sequence_type &sequenceNumber + uint8_t *pBuffer, + size_type nBuffer, + size_type iEntry // defaults to zero + ); + +Description: + If the specified entry exists, its contents are read from the FRAM + into the user's buffer. The ring buffer pointers are not chagned. + +Returns: + true for success, false for failure. + +Notes: + peek_front() is named followign terminology. + +*/ + +#define FUNCTION "FramRingBuffer_t::peek_front" + +bool +FramRingBuffer_t::peek_front( + sequence_type &sequenceNumber, + uint8_t *pBuffer, + size_type nBuffer, + size_type iEntry // defaults to zero + ) + { + bool fResult; + static_assert(sizeof(sequence_type) == 2); + uint8_t sequence_buffer[2]; + + if (iEntry >= this->m_logheader.size()) + return false; + + size_type const iItem = this->m_logheader.absoluteIndex(iEntry); + size_type const byteIndex = this->m_logheader.indexToOffset(iItem); + + fResult = this->m_logBufferCursor.getPartialValue(sequence_buffer, sizeof(sequence_buffer), byteIndex); + if (fResult) + fResult = this->m_logBufferCursor.getPartialValue(pBuffer, nBuffer, byteIndex + sizeof(sequence_buffer)); + + if (fResult) + sequenceNumber = sequence_buffer[0] + (sequence_buffer[1] << 8); + + return fResult; + } + +#undef FUNCTION + +/* + +Name: FramRingBuffer_t::newSequenceNumber() + +Function: + Assign a new sequence number and record the decision. + +Definition: + bool FramRingBuffer_t::newSequenceNumber( + FramRingBuffer_t::sequence_type &sequenceNumber + ); + +Description: + A sequence number is issued, updating FRAM so that the sequence number + can't be immediately reused. + +Returns: + This function returns true if the sequence number was successfully + assigned, false otherwise. If false, then sequenceNumber is not changed. + +Notes: + + +*/ + +#define FUNCTION "FramRingBuffer_t::newSequenceNumber" + +bool +FramRingBuffer_t::newSequenceNumber( + FramRingBuffer_t::sequence_type &sequenceNumber + ) + { + auto const newSequenceNumber = this->m_logheader.querySequenceNumber(); + + this->m_logheader.advanceSequence(); + if (! this->m_logHeaderCursor.put(this->m_logheader)) + { + this->m_logheader.setSequenceNumber(newSequenceNumber); + return false; + } + else + { + sequenceNumber = newSequenceNumber; + return true; + } + } + +#undef FUNCTION + +/* + +Name: FramRingBuffer_t::clear() + +Function: + Clear and save the ring buffer header. + +Definition: + bool FramRingBuffer_t::clear( + void + ); + +Description: + The ring buffer pointers are re-initialized. Sequence number is not + changed + +Returns: + This function returns true if and only if the header was successfully + saved to FRAM. + +Notes: + + +*/ + +#define FUNCTION "FramRingBuffer_t::clear" + +bool +FramRingBuffer_t::clear( + void + ) + { + this->m_logheader.clear(); + return this->m_logHeaderCursor.put(this->m_logheader); + } + +#undef FUNCTION diff --git a/src/lib/Catena_FramStorage.cpp b/src/lib/Catena_FramStorage.cpp index ca4fbbc..00f9262 100644 --- a/src/lib/Catena_FramStorage.cpp +++ b/src/lib/Catena_FramStorage.cpp @@ -72,6 +72,10 @@ McciCatena::cFramStorage::vItemDefs[cFramStorage::kMAX] = cFramStorage::StandardItem(kAppConf, cFramStorage::MaxAppConfSize, false), cFramStorage::StandardItem(kLmicSessionState, sizeof(Arduino_LoRaWAN::SessionState), /* number */ false), + cFramStorage::StandardItem(kUplinkInterval, sizeof(uint32_t), /* number */ true), + + cFramStorage::StandardItem(kDataLogHeader, sizeof(cFramStorage::DataLogHeader_t), /* number */ false), + cFramStorage::StandardItem(kDataLogBuffer, sizeof(cFramStorage::DataLogBuffer_t), /* number */ false, /* replicated */ false), }; /****************************************************************************\ diff --git a/src/lib/stm32/catena491x/Catena4917_ReadVoltage.cpp b/src/lib/stm32/catena491x/Catena4917_ReadVoltage.cpp new file mode 100644 index 0000000..6b2eb75 --- /dev/null +++ b/src/lib/stm32/catena491x/Catena4917_ReadVoltage.cpp @@ -0,0 +1,81 @@ +/* + +Module: Catena4917_ReadVoltage.cpp + +Function: + Catena4917::ReadVbat() and Catena4917::ReadVbus() + +Copyright notice: + See accompanying LICENSE file. + +Author: + Pranau R, MCCI Corporation November 2022 + +*/ + +#ifdef ARDUINO_ARCH_STM32 + +#include "Catena4917.h" +#include "Catena_Log.h" + +#include +using namespace McciCatena; + +/****************************************************************************\ +| +| Manifest constants & typedefs. +| +\****************************************************************************/ + + + +/****************************************************************************\ +| +| Read-only data. +| +\****************************************************************************/ + + + +/****************************************************************************\ +| +| Variables. +| +\****************************************************************************/ + +float +Catena4917::ReadVbat(void) const + { + float volt = this->ReadAnalog(Catena491x::ANALOG_CHANNEL_VBAT, 1, 1); + return volt / 1000; + } + +float +Catena4917::ReadVbus(void) const + { + float volt = this->ReadAnalog(Catena491x::ANALOG_CHANNEL_VBUS, 6, 3); + return volt / 1000; + } + +#if defined(ARDUINO_MCCI_MODEL_4917) && defined(USBD_LL_ConnectionState_WEAK) + +extern "C" { + +uint32_t USBD_LL_ConnectionState(void) + { + uint32_t vBus; + bool fStatus; + + fStatus = CatenaStm32L0_ReadAnalog( + Catena491x::ANALOG_CHANNEL_VBUS, 6, 3, &vBus + ); + return (fStatus && vBus < 3000) ? 0 : 1; + } + +} + +#endif // ARDUINO_MCCI_MODEL_4917 + +#endif // ARDUINO_ARCH_STM32 + +/**** end of Catena4917_ReadVoltage.cpp ****/ \ No newline at end of file diff --git a/src/lib/stm32/catena491x/Catena4917_getPlatformTable.cpp b/src/lib/stm32/catena491x/Catena4917_getPlatformTable.cpp new file mode 100644 index 0000000..9538bcc --- /dev/null +++ b/src/lib/stm32/catena491x/Catena4917_getPlatformTable.cpp @@ -0,0 +1,90 @@ +/* + +Module: Catena4917_getPlatformTable.cpp + +Function: + Catena4917::getPlatformTable() + +Copyright notice: + See accompanying LICENSE file. + +Author: + Pranau R, MCCI Corporation November 2022 + +*/ + +#ifdef ARDUINO_ARCH_STM32 + +#include "Catena4917.h" + +#include "Catena_Log.h" +#include "Catena_Platforms.h" +#include "Catena_Guids.h" + +/****************************************************************************\ +| +| Read-only data. +| +\****************************************************************************/ + +namespace McciCatena { + +const CATENA_PLATFORM gkPlatformCatena4917 = + { + Guid: GUID_HW_CATENA_4917_BASE(WIRE), + pParent: &gkPlatformCatena4917, + PlatformFlags: + CatenaBase::fHasLoRa | + CatenaBase::fHasTtnNycLoRa | + CatenaBase::fHasLIS2HH12 | + CatenaBase::fHasFRAM | + CatenaBase::fHasFlash + }; + +const CATENA_PLATFORM (* const Catena4917::vPlatforms[]) = + { + // entry 0 is the default + &gkPlatformCatena4917, + }; + +const size_t Catena4917::nvPlatforms = sizeof(Catena4917::vPlatforms) / sizeof(Catena4917::vPlatforms[0]); + +/* + +Name: Catena4917::getPlatformTable() + +Function: + Get the known platform table. + +Definition: + public: virtual + void Catena4917::getPlatformTable( + const CATENA_PLATFORM * const * &vPlatforms, + size_t &nvPlatforms + ) override; + +Description: + This override for getPlatformTable() returns the vector of platform + GUIDs for this Catena. + +Returns: + vPlatforms is set to the base of the array of pointers to platform + stuctures; and nvPlatforms is set to the number of entries in + the table. + +*/ + +/* public virtual override */ +void +Catena4917::getPlatformTable( + const CATENA_PLATFORM * const * &result_vPlatforms, + size_t &result_nvPlatforms + ) + { + result_vPlatforms = vPlatforms; + result_nvPlatforms = nvPlatforms; + } + +} /* namespace McciCatena */ + +#endif // ARDUINO_ARCH_STM32 diff --git a/src/lib/stm32/catena491x/Catena491x_LoRaWAN_begin.cpp b/src/lib/stm32/catena491x/Catena491x_LoRaWAN_begin.cpp new file mode 100644 index 0000000..891815e --- /dev/null +++ b/src/lib/stm32/catena491x/Catena491x_LoRaWAN_begin.cpp @@ -0,0 +1,68 @@ +/* + +Module: Catena491x_LoRaWAN_begin.cpp + +Function: + Catena491x::LoRaWAN::begin() + +Copyright notice: + See accompanying LICENSE file. + +Author: + Pranau R, MCCI Corporation November 2022 + +*/ + +#ifdef ARDUINO_ARCH_STM32 + +#include "Catena491x.h" + +#include "Catena_Log.h" +#include "mcciadk_baselib.h" + +using namespace McciCatena; + +/* + +Name: Catena491x::LoRaWAN::begin() + +Function: + Record linkage to main Catena object and set up LoRaWAN. + +Definition: + bool Catena491x::LoRaWAN::begin( + Catena491x *pParent + ); + +Description: + We record parent pointers, and other useful things for later. + +Returns: + true for success, false for failure. + +*/ + +bool +Catena491x::LoRaWAN::begin( + Catena491x *pParent + ) + { + gLog.printf(gLog.kTrace, "+Catena491x::LoRaWAN::begin()\n"); + + /* call the base begin */ + if (! this->Super::begin(pParent)) + { + gLog.printf( + gLog.kBug, + "?Catena491x::LoRaWAN::begin: Super::begin() failed\n" + ); + return false; + } + + /* indicate success to the client */ + return true; + } + +#endif // ARDUINO_ARCH_STM32 + +/**** end of Catena491x_LoRaWAN_begin.cpp ****/ \ No newline at end of file diff --git a/src/lib/stm32/catena491x/Catena491x_begin.cpp b/src/lib/stm32/catena491x/Catena491x_begin.cpp new file mode 100644 index 0000000..c21dedb --- /dev/null +++ b/src/lib/stm32/catena491x/Catena491x_begin.cpp @@ -0,0 +1,63 @@ +/* + +Module: Catena491x_begin.cpp + +Function: + Catena491x::begin(). + +Copyright notice: + See accompanying LICENSE file. + +Author: + Pranau R, MCCI Corporation November 2022 + +*/ + +#ifdef ARDUINO_ARCH_STM32 + +#include "Catena491x.h" + +#include "Catena_Log.h" + +using namespace McciCatena; + +/* + +Name: Catena491x::begin() + +Function: + Set up all the well-known board peripherals. + +Definition: + bool Catena491x::begin(); + +Description: + Issues begin() for all the Catena491x things. + +Returns: + true for success, false for failure. + +*/ + +bool Catena491x::begin() + { + Serial.begin(115200); + Wire.begin(); + delay(1000); + gLog.begin(cLog::DebugFlags(gLog.kError | gLog.kBug)); + gLog.printf( + gLog.kTrace, + "\n+Catena491x::begin() for %s\n", + CatenaName() + ); + + // do the platform selection. + if (! this->Super::begin()) + return false; + + return true; + } + +#endif // ARDUINO_ARCH_STM32 + +/**** end of Catena491x_begin.cpp ****/ \ No newline at end of file