mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
The read routes leave out a room, shade, group or linked shade that doesn't fit the 3 KB unit, without telling the client (KNOWN_ISSUES.md). test_snapshot_limits fills a snapshot with the largest possible items and checks every read document written with the real unit is identical to the same document written with an unlimited one. The largest item, a shade, is 1,097 bytes. The unit is now SNAPSHOT_UNIT_SIZE in ModelSnapshot.h, used by Web.cpp and the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
7.5 KiB
C++
205 lines
7.5 KiB
C++
#ifndef MODEL_SNAPSHOT_H
|
|
#define MODEL_SNAPSHOT_H
|
|
#include <atomic>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include "ShadeTypes.h"
|
|
#include "JsonFormatter.h"
|
|
|
|
// A copy of the shade model taken on loop() for one read, so the reply
|
|
// describes a single moment (docs/adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md),
|
|
// and the JSON the read routes write from it. Kept free of the shade model and
|
|
// the web server so it can be unit tested on the PC (pio test -e native).
|
|
|
|
// Positions are as clients see them: whole percentages, flipped for a shade
|
|
// with flipPosition, -1 when unset.
|
|
// No default member values: with them the SnapshotSlots would be stored in
|
|
// flash as initialized data (11 KB). The snapshot functions set every field;
|
|
// anything else starts from {}.
|
|
struct room_snapshot_t {
|
|
uint8_t roomId;
|
|
char name[21];
|
|
int8_t sortOrder;
|
|
};
|
|
struct linked_remote_snapshot_t {
|
|
uint32_t remoteAddress;
|
|
uint16_t lastRollingCode;
|
|
};
|
|
struct shade_snapshot_t {
|
|
uint8_t shadeId;
|
|
uint8_t roomId;
|
|
char name[21];
|
|
uint32_t remoteAddress;
|
|
uint32_t upTime;
|
|
uint32_t downTime;
|
|
uint32_t tiltTime;
|
|
uint16_t stepSize;
|
|
bool paired;
|
|
uint16_t lastRollingCode;
|
|
int8_t position;
|
|
int8_t target;
|
|
int8_t myPos;
|
|
int8_t direction;
|
|
uint8_t tiltType;
|
|
int8_t tiltPosition;
|
|
int8_t tiltTarget;
|
|
int8_t myTiltPos;
|
|
int8_t tiltDirection;
|
|
uint8_t shadeType;
|
|
uint8_t bitLength;
|
|
uint8_t proto;
|
|
uint8_t flags;
|
|
bool flipCommands;
|
|
bool flipPosition;
|
|
bool inGroup;
|
|
bool sunSensor;
|
|
bool light;
|
|
bool simMy;
|
|
uint8_t repeats;
|
|
int8_t sortOrder;
|
|
uint8_t gpioUp;
|
|
uint8_t gpioDown;
|
|
uint8_t gpioMy;
|
|
bool gpioLLTrigger;
|
|
uint8_t linkedRemoteCount;
|
|
linked_remote_snapshot_t linkedRemotes[SOMFY_MAX_LINKED_REMOTES];
|
|
};
|
|
struct group_snapshot_t {
|
|
uint8_t groupId;
|
|
uint8_t roomId;
|
|
char name[21];
|
|
uint32_t remoteAddress;
|
|
uint16_t lastRollingCode;
|
|
uint8_t bitLength;
|
|
uint8_t proto;
|
|
bool sunSensor;
|
|
bool flipCommands;
|
|
uint8_t flags; // its linked shades' flags together
|
|
uint8_t repeats;
|
|
int8_t sortOrder;
|
|
uint8_t linkedShadeCount;
|
|
uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES];
|
|
};
|
|
// Only the rooms, shades, groups and repeaters that exist, in model order.
|
|
struct model_snapshot_t {
|
|
uint32_t startingAddress;
|
|
uint8_t radioBitLength; // what a new shade or group gets
|
|
uint8_t radioProto;
|
|
uint8_t roomCount;
|
|
uint8_t shadeCount;
|
|
uint8_t groupCount;
|
|
uint8_t repeaterCount;
|
|
room_snapshot_t rooms[SOMFY_MAX_ROOMS];
|
|
shade_snapshot_t shades[SOMFY_MAX_SHADES];
|
|
group_snapshot_t groups[SOMFY_MAX_GROUPS];
|
|
uint32_t repeaters[SOMFY_MAX_REPEATERS];
|
|
const room_snapshot_t *room(uint8_t roomId) const;
|
|
const shade_snapshot_t *shade(uint8_t shadeId) const;
|
|
const group_snapshot_t *group(uint8_t groupId) const;
|
|
};
|
|
|
|
// The fields of one item, as every route has always written them.
|
|
void writeRoom(JsonFormatter &json, const room_snapshot_t &room);
|
|
void writeShadeRef(JsonFormatter &json, const shade_snapshot_t &shade);
|
|
void writeShade(JsonFormatter &json, const shade_snapshot_t &shade);
|
|
void writeGroupRef(JsonFormatter &json, const group_snapshot_t &group);
|
|
// The shade a group links to, or nullptr when it has gone.
|
|
typedef const shade_snapshot_t *(*shade_lookup_t)(uint8_t shadeId, void *ctx);
|
|
// writeGroupRef plus linkedShades, the refs of the shades it links to.
|
|
void writeGroup(JsonFormatter &json, const group_snapshot_t &group, shade_lookup_t lookup, void *ctx);
|
|
|
|
// The documents the read routes send.
|
|
enum class snapshot_doc_t : uint8_t {
|
|
controller, // /controller
|
|
discovery, // /discovery
|
|
rooms, // /rooms
|
|
shades, // /shades
|
|
groups, // /groups
|
|
room, // /room?roomId=
|
|
shade, // /shade?shadeId=
|
|
group, // /group?groupId=
|
|
repeaters, // /repeaters
|
|
nextRoom, // /getNextRoom: the id and defaults a new room would get
|
|
nextShade, // /getNextShade
|
|
nextGroup, // /getNextGroup
|
|
groupOptions // /groupOptions?groupId=: the group and the shades it could link
|
|
};
|
|
// Writes the fields of /controller or /discovery that aren't in the snapshot
|
|
// (device, radio and firmware details) into the open top-level object.
|
|
typedef void (*snapshot_header_t)(JsonFormatter &json, const model_snapshot_t &snap);
|
|
|
|
// The buffer each room, shade, group and linked shade is formatted into on its
|
|
// own. test_snapshot_limits checks the largest possible item of each fits.
|
|
#define SNAPSHOT_UNIT_SIZE 3072
|
|
|
|
// Writes a document from a snapshot a piece at a time for a chunked response.
|
|
// Each room, shade, group and linked shade is formatted into unit on its own;
|
|
// one that doesn't fit is logged and left out so the JSON stays valid.
|
|
class SnapshotWriter {
|
|
public:
|
|
// id picks the item for the single-item documents. header is only called
|
|
// for controller and discovery. unit must outlive the writer.
|
|
SnapshotWriter(const model_snapshot_t &snap, snapshot_doc_t doc, uint8_t id, snapshot_header_t header, char *unit, size_t unitSize);
|
|
// Copies up to maxLen more bytes of the document into out. 0 once it is all
|
|
// out.
|
|
size_t generate(uint8_t *out, size_t maxLen);
|
|
private:
|
|
const model_snapshot_t &snap;
|
|
const uint8_t *steps;
|
|
uint8_t step = 0;
|
|
uint8_t id;
|
|
snapshot_header_t header;
|
|
BufferedJsonFormatter fmt;
|
|
char *unit;
|
|
size_t unitSize;
|
|
size_t unitLen = 0;
|
|
size_t consumed = 0;
|
|
uint8_t idx = 0;
|
|
uint8_t lsIdx = 0; // next linked shade of the group being written
|
|
bool groupOpen = false; // the group's ref is out, its linked shades are next
|
|
bool firstItem = true;
|
|
bool firstLinked = true;
|
|
void produceUnit();
|
|
void nextStep();
|
|
void literal(const char *text);
|
|
void resetFmt(size_t offset);
|
|
bool overflowed(const char *what, uint8_t itemId);
|
|
size_t itemStart();
|
|
void groupUnit(const group_snapshot_t &group, bool inList, const char *close = "]}");
|
|
void finish(const char *text);
|
|
};
|
|
// What the next room, shade or group added would get: its id (0 for a room and
|
|
// 255 for a shade or group when there is no room for one) and remote address.
|
|
uint8_t nextRoomId(const model_snapshot_t &snap);
|
|
uint8_t nextShadeId(const model_snapshot_t &snap);
|
|
uint8_t nextGroupId(const model_snapshot_t &snap);
|
|
uint32_t nextRemoteAddress(const model_snapshot_t &snap, uint8_t id);
|
|
|
|
// The snapshots reads are served from. A read takes a slot on the web server
|
|
// task and queues a fill; loop() fills it; the read streams it and lets it go.
|
|
// The read may go away before loop() gets to it, so the slot's state decides
|
|
// who frees it: whichever side is last. A slot's snapshot (about 5.6 KB) is
|
|
// only allocated while a read holds it.
|
|
#define SNAPSHOT_SLOTS 2
|
|
class SnapshotSlots {
|
|
public:
|
|
// A free slot with its snapshot allocated, or -1 when every slot is in use
|
|
// or there is no memory. Any task.
|
|
int8_t acquire();
|
|
// Fills the slot with fill, unless its read has gone. loop() only.
|
|
void fill(uint8_t slot, void (*fill)(model_snapshot_t &snap));
|
|
// Whether loop() has filled the slot. The read's task.
|
|
bool filled(uint8_t slot) const;
|
|
// Valid from acquire() until the read releases the slot.
|
|
model_snapshot_t &snapshot(uint8_t slot) { return *this->snaps[slot]; }
|
|
// The read is done with the slot, or gone. The read's task.
|
|
void release(uint8_t slot);
|
|
// Frees a slot whose fill was never queued. The read's task.
|
|
void cancel(uint8_t slot);
|
|
private:
|
|
enum : uint32_t { FREE, WAITING, FILLING, FILLED, ABANDONED };
|
|
model_snapshot_t *snaps[SNAPSHOT_SLOTS] = {};
|
|
std::atomic<uint32_t> state[SNAPSHOT_SLOTS] = {};
|
|
void freeSlot(uint8_t slot);
|
|
};
|
|
#endif
|