Loading files
/**
* @file host_link/describe.cpp
* @brief ComponentWriter + RenderDescriptor (descriptor auto-derivation).
*/
#include "alchemy/host_link/describe.h"
#include <cstdio>
#include "alchemy/surface/button_bank.h"
#include "alchemy/surface/jack.h"
#include "alchemy/surface/manual.h"
#include "alchemy/surface/page.h"
#include "alchemy/surface/pager.h"
#include "alchemy/surface/presets.h"
#include "alchemy/surface/settings.h"
#include "alchemy/surface/virtual_knob.h"
namespace alchemy {
namespace hostlink {
/* ── ComponentWriter ────────────────────────────────────────────────── */
ComponentWriter& ComponentWriter::Ident(const char* id)
{
if (opened_) ok_ = false;
else id_ = id;
return *this;
}
ComponentWriter& ComponentWriter::Kind(const char* kind)
{
if (opened_) ok_ = false;
else kind_ = kind;
return *this;
}
ComponentWriter& ComponentWriter::Label(const char* display_name)
{
if (opened_) ok_ = false;
else label_ = display_name;
return *this;
}
ComponentWriter& ComponentWriter::Grid(uint8_t pages, uint8_t pots)
{
if (opened_) ok_ = false;
else { grid_pages_ = pages; grid_pots_ = pots; has_grid_ = true; }
return *this;
}
ComponentWriter& ComponentWriter::PageMeta(const char* const* names,
const char* const* colors,
uint8_t n)
{
if (opened_) ok_ = false;
else { names_ = names; colors_ = colors; n_meta_ = n; }
return *this;
}
bool ComponentWriter::Open()
{
if (opened_) return ok_;
opened_ = true;
ok_ = db_.BeginComponent(id_, kind_, comp_, label_) && ok_;
if (ok_ && has_grid_) ok_ = db_.ComponentGrid(grid_pages_, grid_pots_);
if (ok_ && n_meta_) ok_ = db_.ComponentPageMeta(names_, colors_, n_meta_);
return ok_;
}
bool ComponentWriter::Field(const char* id, const char* name, uint32_t off,
FieldType type, float def, const char* disp_json,
uint16_t zones, int16_t page, int16_t pot,
const char* help, const SeeRef* see,
uint8_t num_see)
{
if (!Open()) return false;
ok_ = db_.GenericField(id, name, off, type, def, zones, disp_json,
page, pot, help, see, num_see)
&& ok_;
return ok_;
}
bool ComponentWriter::Meta(const char* key, const char* raw_json)
{
if (!Open()) return false;
ok_ = db_.GenericMeta(key, raw_json) && ok_;
return ok_;
}
bool ComponentWriter::MetaUInt(const char* key, uint32_t value)
{
if (!Open()) return false;
ok_ = db_.GenericMetaUInt(key, value) && ok_;
return ok_;
}
bool ComponentWriter::MetaStr(const char* key, const char* value)
{
if (!Open()) return false;
ok_ = db_.GenericMetaStr(key, value) && ok_;
return ok_;
}
bool ComponentWriter::Finalize()
{
if (!Open()) return false;
ok_ = db_.EndComponent() && ok_;
return ok_;
}
/* ── Knob lookup + display-hint derivation ──────────────────────────── */
namespace {
const VirtualKnob* FindKnob(const PageSet* pages, uint8_t pg, uint8_t pot)
{
if (!pages) return nullptr;
for (uint8_t i = 0; i < pages->count; i++)
{
const Page* p = pages->pages[i];
if (!p || p->Index() != pg) continue;
for (uint8_t j = 0; j < p->Count(); j++)
{
const VirtualKnob* k = p->At(j);
if (k && k->Pot() == pot) return k;
}
}
return nullptr;
}
/* Visibility belongs to the page position, independent of whether a
* VirtualKnob was declared there (or shared with another page). */
bool IsPotHidden(const PageSet* pages, uint8_t pg, uint8_t pot)
{
if (!pages) return false;
for (uint8_t i = 0; i < pages->count; i++)
{
const Page* p = pages->pages[i];
if (p && p->Index() == pg && p->PotHidden(pot)) return true;
}
return false;
}
/* Derive a display hint from the knob's declared value transform:
* Exp(lo, hi) → {"kind":"exp","lo":..,"hi":..[,"unit":".."]}
* Linear(lo, hi) → {"kind":"linear",...} (plain 0..1 → percent)
* Selector + Labels → {"kind":"snap","labels":[...],"bins":N}
* Selector unlabeled → {"kind":"linear","lo":0,"hi":N-1,"bins":N}
* "bins" is additive: floor(norm*N), clamped to [0,N-1], with writes at
* (index+0.5)/N. Without it, legacy snap positions remain i/(N-1).
* A raw .Disp(json) wins outright. Returns nullptr for the plain
* percent readout (and on buffer overflow — a safe degrade). */
const char* DeriveKnobDisp(const VirtualKnob& k, char* buf, size_t cap)
{
if (k.DispJson()) return k.DispJson();
JsonWriter jw(buf, cap);
switch (k.Transform())
{
case VirtualKnob::Xform::Selector:
{
const uint8_t zones = static_cast<uint8_t>(k.XformMax());
if (k.Labels() && k.NumLabels() == zones && zones > 0u)
{
jw.BeginObj();
jw.Key("kind"); jw.Str("snap");
jw.Key("labels");
jw.BeginArr();
for (uint8_t i = 0; i < zones; i++) jw.Str(k.Labels()[i]);
jw.EndArr();
jw.Key("bins"); jw.UInt(zones);
jw.EndObj();
}
else
{
jw.BeginObj();
jw.Key("kind"); jw.Str("linear");
jw.Key("lo"); jw.Float(0.f);
jw.Key("hi"); jw.Float(zones > 0u ? zones - 1.f : 0.f);
jw.Key("bins"); jw.UInt(zones);
jw.EndObj();
}
break;
}
case VirtualKnob::Xform::Exp:
case VirtualKnob::Xform::Linear:
default:
{
const bool exp = (k.Transform() == VirtualKnob::Xform::Exp);
if (!exp && k.XformMin() == 0.f && k.XformMax() == 1.f
&& !k.Unit())
return nullptr; /* stock 0..1 knob → percent readout */
jw.BeginObj();
jw.Key("kind"); jw.Str(exp ? "exp" : "linear");
jw.Key("lo"); jw.Float(k.XformMin());
jw.Key("hi"); jw.Float(k.XformMax());
if (k.Unit()) { jw.Key("unit"); jw.Str(k.Unit()); }
jw.EndObj();
break;
}
}
if (!jw.Terminate()) return nullptr;
return buf;
}
/* ── Built-in describers for the standard surfaces ──────────────────── */
bool DescribePager(DescriptorBuilder& db, const Pager& pager,
const PageSet* pages, const char* id)
{
/* Tab labels/colors/help from the declared Pages (sparse ok). */
const char* names [Pager::kMaxPages] = {};
const char* colors[Pager::kMaxPages] = {};
const char* help [Pager::kMaxPages] = {};
bool any_name = false, any_color = false, any_help = false;
if (pages)
{
for (uint8_t i = 0; i < pages->count; i++)
{
const Page* p = pages->pages[i];
if (!p || p->Index() >= pager.NumPages()) continue;
if (p->TabName()) { names [p->Index()] = p->TabName(); any_name = true; }
if (p->TabColor()) { colors[p->Index()] = p->TabColor(); any_color = true; }
if (p->HelpText()) { help [p->Index()] = p->HelpText(); any_help = true; }
}
}
bool ok = db.BeginPager(id, pager,
any_name ? names : nullptr,
any_color ? colors : nullptr,
any_help ? help : nullptr);
for (uint8_t pg = 0; ok && pg < pager.NumPages(); pg++)
{
for (uint8_t pot = 0; ok && pot < pager.NumPots(); pot++)
{
const VirtualKnob* k = FindKnob(pages, pg, pot);
if (k && k->SeeOverflowed()) return false;
char idbuf[16], namebuf[16], dispbuf[256];
const char* fid = (k && k->Ident()) ? k->Ident() : idbuf;
if (fid == idbuf)
std::snprintf(idbuf, sizeof idbuf, "p%u.%u", pg, pot);
const char* nm = (k && k->Name() && k->Name()[0]) ? k->Name()
: namebuf;
if (nm == namebuf)
std::snprintf(namebuf, sizeof namebuf, "P%u K%u",
pg + 1u, pot + 1u);
const char* disp =
k ? DeriveKnobDisp(*k, dispbuf, sizeof dispbuf) : nullptr;
ok = db.PagerField(pg, pot, fid, nm, disp,
k ? k->ManualHelp() : nullptr,
k ? k->SeeRefs() : nullptr,
k ? k->NumSeeRefs() : 0u,
IsPotHidden(pages, pg, pot));
}
}
return ok && db.EndPager();
}
const char* SettingsKindName(SettingsKind k)
{
switch (k)
{
case SettingsKind::Brightness: return "Brightness";
case SettingsKind::Knob: return "Knob";
case SettingsKind::Bipolar: return "Amount";
case SettingsKind::Selector: return "Mode";
default: return "Setting";
}
}
bool DescribeButtons(DescriptorBuilder& db, const ButtonBank& bank,
const char* id)
{
bool ok = db.BeginButtons(id, bank);
ok = ok && bank.Ok(); /* declaration errors latched at freeze */
for (uint8_t i = 0; ok && i < bank.NumModal(); i++)
ok = db.ButtonsModal(*bank.ModalAt(i), bank.ModalPage(i));
for (uint8_t i = 0; ok && i < bank.NumCells(); i++)
ok = db.ButtonsField(*bank.CellAt(i), i, bank.CellPage(i));
return ok && db.EndButtons();
}
bool DescribeSettings(DescriptorBuilder& db, const Settings& st,
const char* id)
{
const char* names[kSettingsMaxPages] = {};
const char* help [kSettingsMaxPages] = {};
bool any_name = false, any_help = false;
for (uint8_t pg = 0; pg < st.NumPages() && pg < kSettingsMaxPages; pg++)
{
if (st.PageNameAt(pg)) { names[pg] = st.PageNameAt(pg); any_name = true; }
if (st.PageHelpAt(pg)) { help [pg] = st.PageHelpAt(pg); any_help = true; }
}
bool ok = db.BeginSettings(id, st, any_name ? names : nullptr, nullptr,
any_help ? help : nullptr);
/* Count persisted controls per kind so a unique control gets a plain
* name ("Brightness") and repeats get position suffixes. */
uint8_t kind_counts[16] = {};
for (uint8_t pg = 0; pg < st.NumPages(); pg++)
for (uint8_t pot = 0; pot < kNumPots; pot++)
{
const SettingsKind k = st.KindAt(pg, pot);
if (Settings::PersistedBytesFor(k) > 0u)
kind_counts[static_cast<uint8_t>(k) & 0x0Fu]++;
}
for (uint8_t pg = 0; ok && pg < st.NumPages(); pg++)
{
for (uint8_t pot = 0; ok && pot < kNumPots; pot++)
{
const SettingsKind k = st.KindAt(pg, pot);
if (Settings::PersistedBytesFor(k) == 0u) continue;
if (st.SeeOverflowedAt(pg, pot)) return false;
char fid[12], nm[24];
std::snprintf(fid, sizeof fid, "s%u.%u", pg, pot);
if (kind_counts[static_cast<uint8_t>(k) & 0x0Fu] > 1u)
std::snprintf(nm, sizeof nm, "%s %u.%u", SettingsKindName(k),
pg + 1u, pot + 1u);
else
std::snprintf(nm, sizeof nm, "%s", SettingsKindName(k));
/* Declared identity wins; brightness ships stock help. */
const char* ident = st.IdentAt(pg, pot);
const char* name = st.DisplayNameAt(pg, pot);
const char* h = st.HelpAt(pg, pot);
if (!h && k == SettingsKind::Brightness)
h = stock_help::kBrightness;
uint8_t num_see = 0u;
const SeeRef* see = st.SeeRefsAt(pg, pot, &num_see);
ok = db.SettingsField(pg, pot, ident ? ident : fid,
name ? name : nm,
nullptr /* derive from kind/labels */,
h, see, num_see);
}
}
return ok && db.EndSettings();
}
/* A failed build degrades to a minimal descriptor carrying the reason:
* identity intact, no state (empty components, zero size/hash), "error"
* root key — hosts show the cause instead of a silent "no descriptor". */
uint32_t RenderErrorDescriptor(char* buf, size_t cap,
const DescriptorBuilder::ModuleInfo& m,
const char* reason)
{
JsonWriter w(buf, cap);
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(0u);
w.Key("size"); w.UInt(0u);
w.Key("components"); w.BeginArr(); w.EndArr();
w.Key("error");
w.Str(reason && reason[0] ? reason : "descriptor build failed");
w.EndObj();
return w.Ok() ? static_cast<uint32_t>(w.Length()) : 0u;
}
} // namespace
/* ── RenderDescriptor ───────────────────────────────────────────────── */
uint32_t RenderDescriptor(char* buf, size_t cap,
const DescriptorBuilder::ModuleInfo& info,
const Presets& presets,
const PageSet* pages,
const DescribeOverride* overrides,
uint8_t num_overrides,
const VirtualButton* const* buttons,
uint8_t num_buttons,
const char* const* root_fragments,
uint8_t num_root_fragments,
const Jack* const* jacks,
uint8_t num_jacks,
const Manual* manual,
const uint8_t* factory,
size_t factory_len,
bool* succeeded)
{
if (succeeded) *succeeded = false;
/* A manual tagline rides the module{} block. */
DescriptorBuilder::ModuleInfo mi = info;
if (manual && manual->TaglineText() && !mi.tagline)
mi.tagline = manual->TaglineText();
DescriptorBuilder db(buf, cap, presets);
if (factory && factory_len) db.Defaults(factory, factory_len);
bool ok = db.Begin(mi);
if (ok) ok = db.Buttons(buttons, num_buttons);
if (ok) ok = db.Jacks(jacks, num_jacks);
if (ok) ok = db.ManualBlock(manual); /* stock preset help at minimum */
for (uint8_t i = 0u; ok && i < num_root_fragments; i++)
ok = db.RawRoot(root_fragments[i]);
uint8_t n_opaque = 0u;
for (uint8_t i = 0; ok && i < presets.NumManaged(); i++)
{
const Serializable* s = presets.ManagedAt(i);
if (!s) { ok = false; break; }
char auto_id[8];
std::snprintf(auto_id, sizeof auto_id, "c%u", i);
/* 1. Explicit override. */
const DescribeOverride* ov = nullptr;
for (uint8_t j = 0; j < num_overrides; j++)
if (overrides[j].component == s) { ov = &overrides[j]; break; }
if (ov)
{
ComponentWriter w(db, *s, auto_id);
ok = ov->fn(w, ov->ctx) && w.Finalize();
continue;
}
/* 2. Standard surfaces. */
switch (s->DescribeKind())
{
case Serializable::DescKind::Pager:
ok = DescribePager(db, static_cast<const Pager&>(*s), pages,
"pager");
continue;
case Serializable::DescKind::Settings:
ok = DescribeSettings(db, static_cast<const Settings&>(*s),
"settings");
continue;
case Serializable::DescKind::Name:
ok = db.Name("name", *s);
continue;
case Serializable::DescKind::Buttons:
ok = DescribeButtons(db, static_cast<const ButtonBank&>(*s),
"buttons");
continue;
default:
break;
}
/* 3. The component's own Describe(); 4. opaque fallback. */
ComponentWriter w(db, *s, auto_id);
if (s->Describe(w))
{
ok = w.Finalize();
}
else if (w.DidWrite())
{
ok = false; /* wrote, then bailed — half a component */
}
else
{
char nm[12];
std::snprintf(nm, sizeof nm, "Data %u", ++n_opaque);
ok = db.Opaque(auto_id, *s, nm);
}
}
const uint32_t len = ok ? db.Finish() : 0u;
if (len)
{
if (succeeded) *succeeded = true;
return len;
}
return RenderErrorDescriptor(buf, cap, mi, db.LastError());
}
} // namespace hostlink
} // namespace alchemy
b51161896This file is pinned to the selected release. Download the original to open it in your design tool.