Loading files
3f40b6632This file is pinned to the selected release. Download the original to open it in your design tool.
/**
* @file host_link/descriptor.cpp
* @brief DescriptorBuilder implementation.
*/
#include "alchemy/host_link/descriptor.h"
#include <cstdio>
#include <cstring>
#include "alchemy/host_link/json_check.h"
#include "alchemy/surface/jack.h"
#include "alchemy/surface/manual.h"
#include "alchemy/surface/pager.h"
#include "alchemy/surface/presets.h"
#include "alchemy/surface/settings.h"
#include "alchemy/surface/serializable.h"
#include "alchemy/surface/virtual_button.h"
#include "alchemy/surface/virtual_knob.h"
namespace alchemy {
namespace hostlink {
DescriptorBuilder::DescriptorBuilder(char* buf, size_t cap,
const Presets& presets)
: w_(buf, cap), presets_(presets)
{
for (uint8_t pg = 0; pg < kMaxSettingsPages; pg++)
for (uint8_t p = 0; p < kMaxPots; p++)
offsets_[pg][p] = -1;
}
void DescriptorBuilder::Defaults(const uint8_t* blob, size_t len)
{
defs_ = blob;
defs_len_ = len;
}
bool DescriptorBuilder::DefF32(uint32_t abs_off, float* v)
{
if (abs_off + 4u > defs_len_) return Fail("defaults image short");
std::memcpy(v, defs_ + abs_off, 4u);
return true;
}
bool DescriptorBuilder::DefByte(uint32_t abs_off, uint8_t* v)
{
if (abs_off >= defs_len_) return Fail("defaults image short");
*v = defs_[abs_off];
return true;
}
bool DescriptorBuilder::Begin(const ModuleInfo& m)
{
w_.BeginObj();
w_.Key("dv"); w_.UInt(1u);
w_.Key("module");
w_.BeginObj();
w_.Key("id"); w_.Str(m.id);
w_.Key("name"); w_.Str(m.name);
w_.Key("fw"); w_.Str(m.fw);
w_.Key("git"); w_.Str(m.git);
w_.Key("sdk"); w_.Str(m.sdk);
w_.Key("board"); w_.Str(m.board);
if (m.tagline) { w_.Key("tagline"); w_.Str(m.tagline); }
w_.EndObj();
w_.Key("schemaHash"); w_.UInt(presets_.LiveSchemaHash());
w_.Key("size"); w_.UInt(static_cast<uint32_t>(presets_.LiveSize()));
w_.Key("components");
w_.BeginArr();
return !error_;
}
/* The component now being described must be the next managed component —
* this pins descriptor order (and offsets) to the real Manage() order. */
bool DescriptorBuilder::CheckManaged(const Serializable& s)
{
if (presets_.ManagedAt(comp_index_) != &s)
return Fail("component out of Manage() order");
return true;
}
/* ── Failure reasons + cross-field ref validation ───────────────────── */
bool DescriptorBuilder::Fail(const char* msg)
{
if (!err_) err_ = msg;
error_ = true;
return false;
}
bool DescriptorBuilder::FailRef(const char* kind, const char* target,
const char* what)
{
if (!err_)
{
std::snprintf(errbuf_, sizeof errbuf_, "%s '%s' %s",
kind, target, what);
err_ = errbuf_;
}
error_ = true;
return false;
}
/* FNV-1a: a hash collision would let one dangling ref through, never
* reject a good one — the safe direction for a placement hint. */
uint32_t DescriptorBuilder::IdHash(const char* s)
{
uint32_t h = 2166136261u;
for (; *s; ++s)
{
h ^= static_cast<uint8_t>(*s);
h *= 16777619u;
}
return h;
}
bool DescriptorBuilder::RecordFieldId(const char* id)
{
if (!id) return Fail("field id null");
if (num_field_ids_ >= kMaxFieldIds)
return Fail("too many fields for ref validation");
field_ids_[num_field_ids_++] = IdHash(id);
return true;
}
bool DescriptorBuilder::RecordExtraId(const char* id)
{
if (!id) return Fail("entity id null");
if (num_extra_ids_ >= kMaxExtraIds)
return Fail("too many entity ids for ref validation");
extra_ids_[num_extra_ids_++] = IdHash(id);
return true;
}
bool DescriptorBuilder::RecordRef(const char* kind, const char* target,
bool fields_only)
{
if (num_refs_ >= kMaxRefs) return Fail("too many field refs");
refs_[num_refs_].kind = kind;
refs_[num_refs_].target = target;
refs_[num_refs_].hash = IdHash(target);
refs_[num_refs_].fields_only = fields_only;
num_refs_++;
return true;
}
/* ── SeeAlso resolution + prose emission ────────────────────────────── */
const char* DescriptorBuilder::ResolveSee(const SeeRef& r, char* buf,
size_t cap)
{
switch (r.K())
{
case SeeRef::Kind::Raw:
return r.Raw();
case SeeRef::Kind::Jack:
return static_cast<const Jack*>(r.Ptr())->Id();
case SeeRef::Kind::Button:
{
const VirtualButton* b = static_cast<const VirtualButton*>(r.Ptr());
if (b->Ident()) return b->Ident();
std::snprintf(buf, cap, "b%u", b->HwIndex());
return buf;
}
case SeeRef::Kind::Knob:
{
const VirtualKnob* k = static_cast<const VirtualKnob*>(r.Ptr());
if (k->Ident()) return k->Ident();
FailRef("see", k->Name(), "targets a knob without Ident()");
return nullptr;
}
case SeeRef::Kind::Slot:
{
const SettingsSlot* s = static_cast<const SettingsSlot*>(r.Ptr());
if (s->ident) return s->ident;
FailRef("see", s->display_name ? s->display_name : "(settings)",
"targets a control without Ident()");
return nullptr;
}
case SeeRef::Kind::None:
default:
Fail("see: empty reference");
return nullptr;
}
}
bool DescriptorBuilder::EmitHelpSee(const char* help, const SeeRef* see,
uint8_t num_see)
{
if (help && help[0]) { w_.Key("help"); w_.Str(help); }
if (see && num_see > 0u)
{
w_.Key("see");
w_.BeginArr();
for (uint8_t i = 0; i < num_see; i++)
{
char buf[8];
const char* id = ResolveSee(see[i], buf, sizeof buf);
if (!id) return false;
if (num_refs_ >= kMaxRefs) return Fail("too many field refs");
refs_[num_refs_].kind = "see";
refs_[num_refs_].target = (id == buf) ? nullptr : id;
refs_[num_refs_].hash = IdHash(id);
refs_[num_refs_].fields_only = false;
num_refs_++;
w_.Str(id);
}
w_.EndArr();
}
return !error_;
}
void DescriptorBuilder::OpenComponent(const char* id, const char* kind,
const Serializable& s)
{
comp_size_ = static_cast<uint32_t>(s.SerializedSize());
w_.BeginObj();
w_.Key("id"); w_.Str(id);
w_.Key("kind"); w_.Str(kind);
w_.Key("hash"); w_.UInt(s.SchemaHash());
w_.Key("size"); w_.UInt(comp_size_);
w_.Key("off"); w_.UInt(cum_off_);
}
/* ── Pager ──────────────────────────────────────────────────────────── */
bool DescriptorBuilder::BeginPager(const char* id, const Pager& pager,
const char* const* page_names,
const char* const* page_colors,
const char* const* page_help)
{
if (!CheckManaged(pager)) return false;
pager_ = &pager;
OpenComponent(id, "pager", pager);
/* Drift guard: the field-offset arithmetic below assumes exactly
* pages × pots × f32. */
const uint32_t expect = static_cast<uint32_t>(pager.NumPages())
* pager.NumPots() * 4u;
if (expect != comp_size_) return Fail("pager: size != pages*pots*f32");
w_.Key("pages"); w_.UInt(pager.NumPages());
w_.Key("pots"); w_.UInt(pager.NumPots());
EmitPageMeta(page_names, page_colors, page_help, pager.NumPages());
w_.Key("fields");
w_.BeginArr();
fields_open_ = true;
return !error_;
}
/* Optional per-page tab labels/colors/help ("pageNames" / "pageColors" /
* "pageHelp" — parallel arrays, protocol §5). Null array pointers emit
* nothing; null entries emit "" so hosts fall back per page. */
void DescriptorBuilder::EmitPageMeta(const char* const* names,
const char* const* colors,
const char* const* help,
uint8_t num_pages)
{
if (names)
{
w_.Key("pageNames");
w_.BeginArr();
for (uint8_t i = 0; i < num_pages; i++) w_.Str(names[i] ? names[i] : "");
w_.EndArr();
}
if (colors)
{
w_.Key("pageColors");
w_.BeginArr();
for (uint8_t i = 0; i < num_pages; i++) w_.Str(colors[i] ? colors[i] : "");
w_.EndArr();
}
if (help)
{
w_.Key("pageHelp");
w_.BeginArr();
for (uint8_t i = 0; i < num_pages; i++) w_.Str(help[i] ? help[i] : "");
w_.EndArr();
}
}
bool DescriptorBuilder::PagerField(uint8_t page, uint8_t pot,
const char* field_id, const char* name,
const char* disp_json,
const char* help,
const SeeRef* see, uint8_t num_see,
bool hidden)
{
if (!pager_ || !fields_open_) return Fail("pager: field before BeginPager");
if (page >= pager_->NumPages() || pot >= pager_->NumPots())
return Fail("pager: field page/pot out of range");
if (!RecordFieldId(field_id)) return false;
const uint32_t off =
(static_cast<uint32_t>(page) * pager_->NumPots() + pot) * 4u;
float def = pager_->Stored(page, pot);
if (defs_ && !DefF32(cum_off_ + off, &def)) return false;
w_.BeginObj();
w_.Key("id"); w_.Str(field_id);
w_.Key("name"); w_.Str(name);
w_.Key("off"); w_.UInt(off);
w_.Key("type"); w_.Str("f32");
w_.Key("page"); w_.UInt(page);
w_.Key("pot"); w_.UInt(pot);
w_.Key("def"); w_.Float(def);
w_.Key("disp");
if (disp_json) w_.RawValue(disp_json);
else w_.RawValue("{\"kind\":\"norm\"}");
if (!EmitHelpSee(help, see, num_see)) return false;
if (hidden) { w_.Key("hidden"); w_.Bool(true); }
w_.EndObj();
return !error_;
}
bool DescriptorBuilder::PagerAltMap(const char* layout_from,
const char* const* field_ids,
size_t count)
{
if (!pager_) return Fail("pager: alt map before BeginPager");
/* Bound check before any read: the id array must hold exactly one
* entry per (page, pot). A mismatch means the alt table drifted from
* the pager geometry — fail the build instead of over-reading. */
const size_t expect =
static_cast<size_t>(pager_->NumPages()) * pager_->NumPots();
if (count != expect) return Fail("pager: alt map count != pages*pots");
if (fields_open_) { w_.EndArr(); fields_open_ = false; }
w_.Key("alt");
w_.BeginObj();
w_.Key("layoutFrom"); w_.Str(layout_from);
w_.Key("pages");
w_.BeginArr();
size_t k = 0u;
for (uint8_t pg = 0; pg < pager_->NumPages(); pg++)
{
w_.BeginArr();
for (uint8_t p = 0; p < pager_->NumPots(); p++)
w_.Str(field_ids[k++]);
w_.EndArr();
}
w_.EndArr();
w_.EndObj();
return !error_;
}
bool DescriptorBuilder::EndPager()
{
if (!pager_) return Fail("pager: End without Begin");
if (fields_open_) { w_.EndArr(); fields_open_ = false; }
w_.EndObj();
cum_off_ += comp_size_;
comp_index_++;
pager_ = nullptr;
return !error_;
}
/* ── Opaque ─────────────────────────────────────────────────────────── */
bool DescriptorBuilder::Opaque(const char* id, const Serializable& s,
const char* name)
{
if (!CheckManaged(s)) return false;
OpenComponent(id, "opaque", s);
w_.Key("name"); w_.Str(name);
w_.EndObj();
cum_off_ += comp_size_;
comp_index_++;
return !error_;
}
/* ── Preset name ────────────────────────────────────────────────────── */
bool DescriptorBuilder::Name(const char* id, const Serializable& s)
{
if (!CheckManaged(s)) return false;
OpenComponent(id, "name", s);
w_.EndObj();
cum_off_ += comp_size_;
comp_index_++;
return !error_;
}
/* ── Generic component ──────────────────────────────────────────────── */
bool DescriptorBuilder::BeginComponent(const char* id, const char* kind,
const Serializable& s,
const char* display_name)
{
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("component: begin while another open");
if (!CheckManaged(s)) return false;
OpenComponent(id, kind, s);
if (display_name) { w_.Key("name"); w_.Str(display_name); }
generic_open_ = true;
return !error_;
}
bool DescriptorBuilder::ComponentGrid(uint8_t pages, uint8_t pots)
{
if (!generic_open_ || fields_open_) return Fail("component: grid after fields");
w_.Key("pages"); w_.UInt(pages);
w_.Key("pots"); w_.UInt(pots);
return !error_;
}
bool DescriptorBuilder::ComponentPageMeta(const char* const* names,
const char* const* colors,
uint8_t num_pages)
{
if (!generic_open_ || fields_open_) return Fail("component: page meta after fields");
EmitPageMeta(names, colors, nullptr, num_pages);
return !error_;
}
bool DescriptorBuilder::GenericField(const char* id, const char* name,
uint32_t off, FieldType type, float def,
uint16_t zones, const char* disp_json,
int16_t page, int16_t pot,
const char* help,
const SeeRef* see, uint8_t num_see)
{
if (!generic_open_) return Fail("component: field before Begin");
if (!fields_open_)
{
w_.Key("fields");
w_.BeginArr();
fields_open_ = true;
}
/* Same bound the shaped builders enforce: a field must lie inside
* the component's serialized bytes. */
const uint32_t width = (type == FieldType::Enum) ? 1u : 4u;
if (off + width > comp_size_)
return Fail("component: field outside serialized bytes");
if (!RecordFieldId(id)) return false;
w_.BeginObj();
w_.Key("id"); w_.Str(id);
w_.Key("name"); w_.Str(name);
w_.Key("off"); w_.UInt(off);
if (page >= 0) { w_.Key("page"); w_.UInt(static_cast<uint32_t>(page)); }
if (pot >= 0) { w_.Key("pot"); w_.UInt(static_cast<uint32_t>(pot)); }
if (type == FieldType::Enum)
{
if (zones == 0u) return Fail("component: enum field needs zones");
w_.Key("type"); w_.Str("enum");
w_.Key("zones"); w_.UInt(zones);
w_.Key("def"); w_.UInt(static_cast<uint32_t>(def));
w_.Key("disp");
w_.RawValue(disp_json ? disp_json : "{\"kind\":\"enum\"}");
}
else
{
w_.Key("type"); w_.Str("f32");
w_.Key("def"); w_.Float(def);
w_.Key("disp");
w_.RawValue(disp_json ? disp_json : "{\"kind\":\"norm\"}");
}
if (!EmitHelpSee(help, see, num_see)) return false;
w_.EndObj();
return !error_;
}
bool DescriptorBuilder::EndComponent()
{
if (!generic_open_) return Fail("component: End without Begin");
if (fields_open_) { w_.EndArr(); fields_open_ = false; }
w_.EndObj();
cum_off_ += comp_size_;
comp_index_++;
generic_open_ = false;
return !error_;
}
bool DescriptorBuilder::GenericMeta(const char* key, const char* raw_json)
{
/* Meta lives between the header and the fields array — after the
* first Field the writer has already opened "fields":[…] and we
* cannot inject a sibling key without producing malformed JSON. */
if (!generic_open_ || fields_open_) return Fail("component: meta after fields");
/* Reject empty strings too — an empty raw_json emits `"<key>":`
* with no value, which corrupts the object (subsequent siblings
* come out with a leading comma). Callers must pass a full JSON
* value; there is no valid empty representation. */
if (!key || !key[0] || !raw_json || !raw_json[0])
return Fail("component: meta key/value empty");
if (!ValidJsonValue(raw_json))
return Fail("component: meta not valid JSON");
w_.Key(key);
w_.RawValue(raw_json);
return !error_;
}
bool DescriptorBuilder::GenericMetaUInt(const char* key, uint32_t value)
{
if (!generic_open_ || fields_open_) return Fail("component: meta after fields");
if (!key || !key[0]) return Fail("component: meta key empty");
w_.Key(key);
w_.UInt(value);
return !error_;
}
bool DescriptorBuilder::GenericMetaStr(const char* key, const char* value)
{
if (!generic_open_ || fields_open_) return Fail("component: meta after fields");
if (!key || !key[0] || !value) return Fail("component: meta key/value empty");
w_.Key(key);
w_.Str(value);
return !error_;
}
/* ── Buttons component (kind "buttons") ─────────────────────────────── */
bool DescriptorBuilder::BeginButtons(const char* id, const Serializable& s)
{
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("buttons: begin while another open");
if (!CheckManaged(s)) return false;
OpenComponent(id, "buttons", s);
buttons_open_ = true;
buttons_next_off_ = 0u;
return !error_;
}
/* Effective field id: the declared Ident(), or the positional "b<hw>"
* fallback (must match ButtonBank's derivation). */
static const char* ButtonEffectiveId(const VirtualButton& b, char* buf,
size_t cap)
{
if (b.Ident()) return b.Ident();
std::snprintf(buf, cap, "b%u", b.HwIndex());
return buf;
}
/* Derive a host label for one gesture from the button's declaration:
* explicit label wins; else "Cycle LP / BP / HP" / "Toggle On / Off" /
* "Set <zone label>" from the zone labels; else just the verb. */
static const char* DeriveGestureLabel(const VirtualButton& b,
const VirtualButton::Gesture& g,
char* buf, size_t cap)
{
if (g.label) return g.label;
if (g.fn) return "";
const bool toggle = (g.action == VirtualButton::Action::Toggle)
|| (g.action == VirtualButton::Action::Cycle
&& b.Zones() == 2u);
size_t n = 0;
auto append = [&](const char* s)
{
while (*s && n + 1u < cap) buf[n++] = *s++;
};
if (g.action == VirtualButton::Action::Set)
{
append("Set");
if (b.ZoneLabels() && g.set_zone < b.NumZoneLabels())
{
append(" ");
append(b.ZoneLabels()[g.set_zone]);
}
buf[n] = '\0';
return buf;
}
append(toggle ? "Toggle" : "Cycle");
if (b.ZoneLabels())
{
for (uint8_t i = 0; i < b.NumZoneLabels(); i++)
{
append(i == 0u ? " " : " / ");
append(b.ZoneLabels()[i]);
}
}
buf[n] = '\0';
return buf;
}
static void EmitGestureEntry(JsonWriter& w, const char* gesture,
const char* label, const char* help = nullptr)
{
w.BeginObj();
w.Key("gesture"); w.Str(gesture);
w.Key("label"); w.Str(label ? label : "");
if (help && help[0]) { w.Key("help"); w.Str(help); }
w.EndObj();
}
/* The declared gestures as a JSON array: the structured tap/hold slots
* (labels derived when not given) plus any legacy Action() metadata. */
static void EmitGestures(JsonWriter& w, const VirtualButton& b,
bool implicit_tap)
{
char buf[96];
w.BeginArr();
if (b.TapGesture().used)
EmitGestureEntry(w, "tap",
DeriveGestureLabel(b, b.TapGesture(), buf, sizeof buf),
b.TapGesture().help);
else if (implicit_tap)
{
VirtualButton::Gesture g;
g.used = true;
EmitGestureEntry(w, "tap", DeriveGestureLabel(b, g, buf, sizeof buf));
}
if (b.HoldGesture().used)
EmitGestureEntry(w, "hold",
DeriveGestureLabel(b, b.HoldGesture(), buf, sizeof buf),
b.HoldGesture().help);
for (uint8_t i = 0; i < b.NumActions(); i++)
EmitGestureEntry(w, b.ActionGesture(i) ? b.ActionGesture(i) : "",
b.ActionLabel(i), b.ActionHelp(i));
w.EndArr();
}
/* Latched declaration errors fail the build; text never silently drops. */
bool DescriptorBuilder::EmitButtonManual(const VirtualButton& b)
{
if (b.BadGestureHelpKey())
return FailRef("gestureHelp", b.BadGestureHelpKey(),
"matches no declared gesture");
if (b.SeeOverflowed())
return FailRef("see", b.Name() ? b.Name() : "(button)",
"has too many SeeAlso refs");
return EmitHelpSee(b.ManualHelp(), b.SeeRefs(), b.NumSeeRefs());
}
bool DescriptorBuilder::ButtonsModal(const VirtualButton& b, int16_t page)
{
/* Modal entries precede the fields array — same JSON-shape rule as
* GenericMeta. */
if (!buttons_open_ || fields_open_) return Fail("buttons: modal after fields");
if (!modal_open_)
{
w_.Key("modal");
w_.BeginArr();
modal_open_ = true;
}
char idbuf[8];
const char* eff = ButtonEffectiveId(b, idbuf, sizeof idbuf);
if (!RecordExtraId(eff)) return false;
w_.BeginObj();
w_.Key("id"); w_.Str(eff);
w_.Key("name"); w_.Str(b.Name() ? b.Name() : "");
if (b.HasHw()) { w_.Key("index"); w_.UInt(b.HwIndex()); }
if (page >= 0) { w_.Key("page"); w_.UInt(static_cast<uint32_t>(page)); }
w_.Key("gestures");
EmitGestures(w_, b, false);
if (!EmitButtonManual(b)) return false;
w_.EndObj();
return !error_;
}
bool DescriptorBuilder::ButtonsField(const VirtualButton& b, uint32_t off,
int16_t page)
{
if (!buttons_open_) return Fail("buttons: field before BeginButtons");
if (modal_open_)
{
w_.EndArr();
modal_open_ = false;
}
if (!fields_open_)
{
w_.Key("fields");
w_.BeginArr();
fields_open_ = true;
}
/* One byte per stateful button, dense, in roster order. */
if (b.Zones() < 2u || off != buttons_next_off_
|| off + 1u > comp_size_)
return Fail("buttons: field not a dense 1-byte enum");
buttons_next_off_++;
char idbuf[8];
const char* eff = ButtonEffectiveId(b, idbuf, sizeof idbuf);
if (!RecordFieldId(eff)) return false;
if (b.AnchorField())
{
if (std::strcmp(b.AnchorField(), eff) == 0)
return FailRef("anchor", b.AnchorField(), "is the field itself");
if (!RecordRef("anchor", b.AnchorField(), /*fields_only=*/true)) return false;
}
w_.BeginObj();
w_.Key("id"); w_.Str(eff);
w_.Key("name"); w_.Str(b.Name() ? b.Name() : "");
w_.Key("off"); w_.UInt(off);
if (page >= 0) { w_.Key("page"); w_.UInt(static_cast<uint32_t>(page)); }
w_.Key("type"); w_.Str("enum");
w_.Key("zones"); w_.UInt(b.Zones());
w_.Key("def"); w_.UInt(b.DefaultZone());
w_.Key("disp");
if (b.DispJson())
{
w_.RawValue(b.DispJson());
}
else if (b.ZoneLabels())
{
w_.BeginObj();
w_.Key("kind"); w_.Str("enum");
w_.Key("labels");
w_.BeginArr();
for (uint8_t i = 0; i < b.NumZoneLabels(); i++)
w_.Str(b.ZoneLabels()[i] ? b.ZoneLabels()[i] : "");
w_.EndArr();
w_.EndObj();
}
else
{
w_.RawValue("{\"kind\":\"enum\"}");
}
if (b.AnchorField()) { w_.Key("anchor"); w_.Str(b.AnchorField()); }
if (b.HasHw())
{
const bool implicit_tap = !b.TapGesture().used
&& !b.HoldGesture().used;
w_.Key("btn");
w_.BeginObj();
w_.Key("index"); w_.UInt(b.HwIndex());
/* Primary action wire-name: the tap mutation (implicit Cycle
* reads as "toggle" on two-zone buttons). A callback tap
* mutates nothing — the key is omitted. */
const VirtualButton::Gesture& tap = b.TapGesture();
if (implicit_tap || (tap.used && !tap.fn))
{
enum VirtualButton::Action act =
tap.used ? tap.action : VirtualButton::Action::Cycle;
if (act == VirtualButton::Action::Cycle && b.Zones() == 2u)
act = VirtualButton::Action::Toggle;
w_.Key("action");
w_.Str(VirtualButton::ActionName(act));
}
w_.Key("gestures");
EmitGestures(w_, b, implicit_tap);
w_.EndObj();
}
if (!EmitButtonManual(b)) return false;
w_.EndObj();
return !error_;
}
bool DescriptorBuilder::EndButtons()
{
if (!buttons_open_) return Fail("buttons: End without Begin");
if (modal_open_) { w_.EndArr(); modal_open_ = false; }
if (fields_open_) { w_.EndArr(); fields_open_ = false; }
if (buttons_next_off_ != comp_size_)
Fail("buttons: size != stateful count");
w_.EndObj();
cum_off_ += comp_size_;
comp_index_++;
buttons_open_ = false;
return !error_;
}
/* ── Settings ───────────────────────────────────────────────────────── */
bool DescriptorBuilder::BeginSettings(const char* id, const Settings& settings,
const char* const* page_names,
const char* const* page_colors,
const char* const* page_help)
{
if (!CheckManaged(settings)) return false;
settings_ = &settings;
OpenComponent(id, "settings", settings);
EmitPageMeta(page_names, page_colors, page_help, settings.NumPages());
/* Gesture-owned pots (the UsePresets save/load surface) persist no
* bytes, so they can never be SettingsFields — but they ARE part of
* the panel the user sees. Emit them as display-only metadata so a
* host can mirror the module's settings pages exactly instead of
* rendering the preset pots as unexplained gaps. Hosts that predate
* the key ignore it (unknown keys are tolerated by protocol rule). */
bool gestures_open = false;
for (uint8_t pg = 0; pg < settings.NumPages() && pg < kMaxSettingsPages; pg++)
{
for (uint8_t p = 0; p < kMaxPots; p++)
{
const SettingsKind k = settings.KindAt(pg, p);
const char* gname =
(k == SettingsKind::PresetSlotPot) ? "Preset Slot"
: (k == SettingsKind::PresetActionPot) ? "Save / Load"
: nullptr;
if (!gname) continue;
if (!gestures_open)
{
w_.Key("gestures");
w_.BeginArr();
gestures_open = true;
}
w_.BeginObj();
w_.Key("page"); w_.UInt(pg);
w_.Key("pot"); w_.UInt(p);
w_.Key("name"); w_.Str(gname);
w_.EndObj();
}
}
if (gestures_open) w_.EndArr();
/* Walk (page, pot) in the exact Serialize() order, accumulating the
* per-slot byte offsets. The final total must equal the component's
* own SerializedSize() — the strongest drift guard available. */
uint32_t off = 0u;
for (uint8_t pg = 0; pg < settings.NumPages() && pg < kMaxSettingsPages; pg++)
{
for (uint8_t p = 0; p < kMaxPots; p++)
{
const size_t n = Settings::PersistedBytesFor(settings.KindAt(pg, p));
offsets_[pg][p] = (n > 0u) ? static_cast<int16_t>(off) : -1;
off += static_cast<uint32_t>(n);
}
}
if (off != comp_size_) return Fail("settings: size != slot walk");
w_.Key("fields");
w_.BeginArr();
fields_open_ = true;
return !error_;
}
bool DescriptorBuilder::SettingsField(uint8_t page, uint8_t pot,
const char* field_id, const char* name,
const char* disp_json,
const char* help,
const SeeRef* see, uint8_t num_see)
{
if (!settings_ || !fields_open_) return Fail("settings: field before BeginSettings");
if (page >= kMaxSettingsPages || pot >= kMaxPots)
return Fail("settings: page/pot out of range");
if (offsets_[page][pot] < 0) return Fail("settings: slot not persisted");
if (!RecordFieldId(field_id)) return false;
const SettingsKind kind = settings_->KindAt(page, pot);
w_.BeginObj();
w_.Key("id"); w_.Str(field_id);
w_.Key("name"); w_.Str(name);
w_.Key("off"); w_.UInt(static_cast<uint32_t>(offsets_[page][pot]));
w_.Key("page"); w_.UInt(page);
w_.Key("pot"); w_.UInt(pot);
const uint32_t abs_off =
cum_off_ + static_cast<uint32_t>(offsets_[page][pot]);
if (kind == SettingsKind::Selector)
{
const uint8_t zones = settings_->ZonesAt(page, pot);
const char* const* labels = settings_->LabelsAt(page, pot);
const uint8_t num_labels = settings_->NumLabelsAt(page, pot);
if (labels && num_labels != zones)
return Fail("settings: selector labels != zones");
uint8_t def = settings_->SelectorIdxAt(page, pot);
if (defs_ && !DefByte(abs_off, &def)) return false;
w_.Key("type"); w_.Str("enum");
w_.Key("zones"); w_.UInt(zones);
w_.Key("def"); w_.UInt(def);
w_.Key("disp");
if (disp_json)
{
w_.RawValue(disp_json);
}
else if (labels)
{
w_.BeginObj();
w_.Key("kind"); w_.Str("enum");
w_.Key("labels");
w_.BeginArr();
for (uint8_t i = 0; i < num_labels; i++)
w_.Str(labels[i] ? labels[i] : "");
w_.EndArr();
w_.EndObj();
}
else
{
w_.RawValue("{\"kind\":\"enum\"}");
}
}
else
{
float def = settings_->StoredNormAt(page, pot);
if (defs_ && !DefF32(abs_off, &def)) return false;
w_.Key("type"); w_.Str("f32");
w_.Key("def"); w_.Float(def);
w_.Key("disp");
if (disp_json)
{
w_.RawValue(disp_json);
}
else if (kind == SettingsKind::Brightness)
{
w_.BeginObj();
w_.Key("kind"); w_.Str("brightness");
w_.Key("lo"); w_.Float(settings_->RangeLoAt(page, pot));
w_.Key("hi"); w_.Float(settings_->RangeHiAt(page, pot));
w_.EndObj();
}
else if (kind == SettingsKind::Bipolar)
{
w_.RawValue("{\"kind\":\"bipolar\"}");
}
else
{
w_.RawValue("{\"kind\":\"norm\"}");
}
}
if (!EmitHelpSee(help, see, num_see)) return false;
w_.EndObj();
return !error_;
}
bool DescriptorBuilder::EndSettings()
{
if (!settings_) return Fail("settings: End without Begin");
if (fields_open_) { w_.EndArr(); fields_open_ = false; }
w_.EndObj();
cum_off_ += comp_size_;
comp_index_++;
settings_ = nullptr;
return !error_;
}
/* ── Raw root fragments (top-level) ────────────────────────────────── */
bool DescriptorBuilder::RawRoot(const char* fragment)
{
if (pager_ || settings_ || generic_open_ || buttons_open_
|| fragment == nullptr || fragment[0] == '\0'
|| num_root_fragments_ >= kMaxRootFragments)
return Fail("root fragment invalid or too many");
root_fragments_[num_root_fragments_++] = fragment;
return true;
}
/* ── Buttons (top-level) ───────────────────────────────────────────── */
bool DescriptorBuilder::Buttons(const VirtualButton* buttons, uint8_t count)
{
if (count > 0u && !buttons)
return Fail("buttons table null with nonzero count");
if (count > kMaxRootButtons) return Fail("too many buttons");
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("buttons table while component open");
for (uint8_t i = 0; i < count; i++) btn_ptrs_[i] = &buttons[i];
num_buttons_ = count;
return !error_;
}
bool DescriptorBuilder::Buttons(const VirtualButton* const* buttons,
uint8_t count)
{
if (count > 0u && !buttons)
return Fail("buttons table null with nonzero count");
if (count > kMaxRootButtons) return Fail("too many buttons");
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("buttons table while component open");
for (uint8_t i = 0; i < count; i++)
{
if (!buttons[i]) return Fail("buttons table has null entry");
btn_ptrs_[i] = buttons[i];
}
num_buttons_ = count;
return !error_;
}
bool DescriptorBuilder::Jacks(const Jack* jacks, uint8_t count)
{
if (count > 0u && !jacks)
return Fail("jacks table null with nonzero count");
if (count > kMaxJacks) return Fail("too many jacks");
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("jacks table while component open");
for (uint8_t i = 0; i < count; i++) jack_ptrs_[i] = &jacks[i];
num_jacks_ = count;
return !error_;
}
bool DescriptorBuilder::Jacks(const Jack* const* jacks, uint8_t count)
{
if (count > 0u && !jacks)
return Fail("jacks table null with nonzero count");
if (count > kMaxJacks) return Fail("too many jacks");
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("jacks table while component open");
for (uint8_t i = 0; i < count; i++)
{
if (!jacks[i]) return Fail("jacks table has null entry");
jack_ptrs_[i] = jacks[i];
}
num_jacks_ = count;
return !error_;
}
bool DescriptorBuilder::ManualBlock(const Manual* manual)
{
if (pager_ || settings_ || generic_open_ || buttons_open_)
return Fail("manual block while component open");
manual_ = manual;
emit_manual_ = true;
return !error_;
}
/* Emit the stashed button table as a top-level "buttons":[...] array.
* Called from Finish() between the components array close and the root
* object close. No-op when the caller stashed nothing. Controls
* entries are cross-field refs — collected for the Finish() match. */
void DescriptorBuilder::EmitRootButtons()
{
if (num_buttons_ == 0u) return;
w_.Key("buttons");
w_.BeginArr();
for (uint8_t i = 0; i < num_buttons_; i++)
{
const VirtualButton& b = *btn_ptrs_[i];
if (b.Ident()) RecordExtraId(b.Ident());
w_.BeginObj();
w_.Key("id"); w_.Str(b.Ident() ? b.Ident() : "");
w_.Key("name"); w_.Str(b.Name() ? b.Name() : "");
w_.Key("role"); w_.Str(VirtualButton::RoleName(b.RoleValue()));
if (b.TapGesture().used || b.HoldGesture().used || b.NumActions() > 0u)
{
w_.Key("actions");
EmitGestures(w_, b, /*implicit_tap=*/false);
}
if (b.NumControls() > 0u)
{
w_.Key("controls");
w_.BeginArr();
for (uint8_t j = 0; j < b.NumControls(); j++)
{
const char* f = b.ControlField(j);
w_.BeginObj();
w_.Key("field"); w_.Str(f ? f : "");
if (f && f[0]) RecordRef("controls", f, /*fields_only=*/true);
w_.Key("action"); w_.Str(VirtualButton::ActionName(b.ControlAction(j)));
w_.EndObj();
}
w_.EndArr();
}
EmitButtonManual(b);
w_.EndObj();
}
w_.EndArr();
}
void DescriptorBuilder::EmitRootJacks()
{
if (num_jacks_ == 0u) return;
w_.Key("jacks");
w_.BeginArr();
for (uint8_t i = 0; i < num_jacks_; i++)
{
const Jack& j = *jack_ptrs_[i];
if (!j.Id() || !j.Id()[0]) { Fail("jack: empty id"); return; }
RecordExtraId(j.Id());
if (j.SeeOverflowed())
{
FailRef("see", j.Id(), "has too many SeeAlso refs");
return;
}
w_.BeginObj();
w_.Key("id"); w_.Str(j.Id());
w_.Key("name"); w_.Str(j.Name() ? j.Name() : "");
if (j.Silk()) { w_.Key("short"); w_.Str(j.Silk()); }
w_.Key("sig"); w_.Str(Jack::SigName(j.Sig()));
if (j.Norm())
{
w_.Key("norm");
w_.Str(j.Norm()->Id());
RecordRef("norm", j.Norm()->Id());
}
EmitHelpSee(j.ManualHelp(), j.SeeRefs(), j.NumSeeRefs());
w_.EndObj();
}
w_.EndArr();
}
void DescriptorBuilder::EmitManualBlock()
{
if (!emit_manual_) return;
if (manual_ && manual_->Overflowed()) { Fail("manual: too many sections"); return; }
RecordExtraId("presets"); /* SeeAlso("presets") is always valid */
w_.Key("manual");
w_.BeginObj();
if (manual_ && manual_->PreambleText())
{
w_.Key("preamble");
w_.Str(manual_->PreambleText());
}
if (manual_ && manual_->NumSections() > 0u)
{
w_.Key("sections");
w_.BeginArr();
for (uint8_t i = 0; i < manual_->NumSections(); i++)
{
const Manual::SectionDecl& s = manual_->SectionAt(i);
if (!s.id || !s.title || !s.body)
{
Fail("manual: section missing id/title/body");
return;
}
w_.BeginObj();
w_.Key("id"); w_.Str(s.id);
w_.Key("title"); w_.Str(s.title);
w_.Key("body"); w_.Str(s.body);
w_.EndObj();
}
w_.EndArr();
}
w_.Key("presets");
w_.BeginObj();
w_.Key("help");
w_.Str((manual_ && manual_->PresetsHelpText()) ? manual_->PresetsHelpText()
: stock_help::kPresets);
w_.EndObj();
w_.EndObj();
}
/* ── Finish ─────────────────────────────────────────────────────────── */
uint32_t DescriptorBuilder::Finish()
{
w_.EndArr(); /* components */
EmitRootButtons();
EmitRootJacks();
for (uint8_t i = 0u; i < num_root_fragments_; i++)
w_.RawValue(root_fragments_[i]);
EmitManualBlock();
w_.EndObj(); /* root */
/* Cross-field refs match against the full id table only now — a
* ref may precede its target in Manage() order or follow it. */
for (uint8_t i = 0u; i < num_refs_; i++)
{
bool found = false;
for (uint16_t k = 0u; !found && k < num_field_ids_; k++)
found = field_ids_[k] == refs_[i].hash;
for (uint8_t k = 0u; !found && !refs_[i].fields_only
&& k < num_extra_ids_; k++)
found = extra_ids_[k] == refs_[i].hash;
if (!found)
FailRef(refs_[i].kind,
refs_[i].target ? refs_[i].target : "(positional)",
"names no field");
}
if (error_) return 0u;
if (comp_index_ != presets_.NumManaged())
{
Fail("components != Manage() count");
return 0u;
}
if (cum_off_ != presets_.LiveSize())
{
Fail("total size drift vs Manage() layout");
return 0u;
}
if (!w_.Ok())
{
Fail("descriptor buffer overflow");
return 0u;
}
if (!ValidJsonValue(w_.Data(), w_.Length()))
{
Fail("descriptor JSON invalid");
return 0u;
}
return static_cast<uint32_t>(w_.Length());
}
} // namespace hostlink
} // namespace alchemy