Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pkg/catalog/classes.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ func (tpl *Template) resolveTypedefs() {
}
d.Tipo = td.Tipo
}
// CompoundOverrides may also carry typedef-aliased TIPOs.
// Resolve in place so the codec only ever sees primitives.
for _, ov := range d.CompoundOverrides {
if td, ok := tpl.Typedefs[ov.Tipo]; ok {
ov.Tipo = td.Tipo
}
}
}
for _, c := range g.Children {
visit(c)
Expand Down Expand Up @@ -194,7 +201,7 @@ func applyVarRanges(v *ClassVar, rows []rangeRow) {
n, _ := strconv.Atoi(rows[0].Value)
v.StringLen = n
}
case "ENUM", "ENUM_BYTE", "ENUM_LONG":
case "ENUM", "ENUM_BYTE", "ENUM_WORD", "ENUM_LONG":
e := &Enum{Name: v.Name + "Inline"}
for _, r := range rows {
ov, _ := strconv.Atoi(r.Override)
Expand Down
41 changes: 35 additions & 6 deletions pkg/catalog/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ type Data struct {
Range *DataRange // first <RANGE> child, kept for back-compat
Ranges []*DataRange // all <RANGE> children (multi-band DATA)

// CompoundOverrides carries per-instance sub-field overrides for
// compound DATA (TIPO referencing a <CLASS>). Catalog templates
// declare each compound's general layout in a <CLASS> with <VAR>
// children, but the per-instance <DATA TIPO="<class>"> can carry
// nested <DATA NAME="..."> children that re-declare a sub-field's
// effective TIPO (e.g. SOGLIA's Stato/Tipo widen from ENUM to
// ENUM_LONG). Without honouring these, the encoded register count
// can mismatch the on-wire DIM and corrupt writes. nil for
// non-compound DATA and for compounds whose CLASS layout already
// matches DIM. See issue #6.
CompoundOverrides map[string]*CompoundFieldOverride

// Linked from WSDL in Task 11.
Message *Message
}
Expand Down Expand Up @@ -324,10 +336,15 @@ func (g *Group) GobDecode(data []byte) error {
}

// decodeDataChildren reads the children of a <DATA> element and
// populates Data.Info, Data.InfoVis, Data.Range when present. Nested
// <DATA> children (compound sub-fields such as the inner DATA of
// CONTATORE / INFO_MISURA) are skipped — the codec recovers their
// layout from the Class definitions parsed in classes.go.
// populates Data.Info, Data.InfoVis, Data.Range, and (for compound
// DATA) Data.CompoundOverrides.
//
// A nested <DATA NAME=…> inside a compound <DATA TIPO="<class>">
// re-declares one of the CLASS's sub-fields with an effective TIPO,
// DEFAULT, RANGE, or VISIBILITY. Today we capture only the TIPO into
// CompoundOverrides — that's what fixes the layout mismatch reported
// in #6 (SOGLIA's Stato/Tipo widen from ENUM to ENUM_LONG, restoring
// DIM=14). Per-instance RANGE / DEFAULT folding is a follow-up.
func decodeDataChildren(dec *xml.Decoder, parentStart *xml.StartElement, d *Data) error {
for {
tok, err := dec.Token()
Expand Down Expand Up @@ -364,9 +381,21 @@ func decodeDataChildren(dec *xml.Decoder, parentStart *xml.StartElement, d *Data
if err := dec.Skip(); err != nil {
return err
}
case "DATA":
child := decodeData(t)
if err := decodeDataChildren(dec, &t, child); err != nil {
return err
}
if child.Name != "" && child.Tipo != "" {
if d.CompoundOverrides == nil {
d.CompoundOverrides = make(map[string]*CompoundFieldOverride)
}
d.CompoundOverrides[child.Name] = &CompoundFieldOverride{Tipo: child.Tipo}
}
default:
// Nested <DATA>, anything else — skip; compound layouts
// come from the Class registry.
// Anything else (notably <message> / <operation>
// remnants if the template ever interleaves them
// inside MENU): skip.
if err := dec.Skip(); err != nil {
return err
}
Expand Down
37 changes: 37 additions & 0 deletions pkg/catalog/menu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ func TestParseMenu(t *testing.T) {
}
}

func TestParseCompoundOverridesFromNestedDATA(t *testing.T) {
// Issue #6: <DATA TIPO="<class>"> can carry nested <DATA NAME="..."
// TIPO="..."> children that override the CLASS VAR's TIPO for that
// instance. Without parsing those, the wire layout falls back to the
// (under-filled) CLASS-level widths and writes corrupt the device.
tpl, err := ParseTemplate(filepath.Join("..", "..", "testdata", "us", "TEST-VB0-a"))
if err != nil {
t.Fatalf("ParseTemplate: %v", err)
}
soglie := tpl.Menu.FindGroup("Set/Soglie")
if soglie == nil {
t.Fatal("Set/Soglie group missing from fixture")
}
d := soglie.FindData("TEST_SOGLIA")
if d == nil {
t.Fatal("TEST_SOGLIA DATA missing")
}
if d.CompoundOverrides == nil {
t.Fatal("CompoundOverrides nil — nested DATA children were not captured")
}
state, ok := d.CompoundOverrides["State"]
if !ok || state.Tipo != "ENUM_LONG" {
t.Errorf("State override = %+v, want Tipo=ENUM_LONG", state)
}
pickup, ok := d.CompoundOverrides["Pickup"]
if !ok || pickup.Tipo != "UWORD" {
t.Errorf("Pickup override = %+v, want Tipo=UWORD", pickup)
}
// Other DATA in the same fixture must NOT have overrides (we want to
// confirm we didn't accidentally start collecting children for
// non-compound DATA).
mb := tpl.Menu.FindGroup("Set/Base").FindData("MB_address")
if mb.CompoundOverrides != nil {
t.Errorf("MB_address (non-compound) accidentally has CompoundOverrides: %+v", mb.CompoundOverrides)
}
}

func equalSlices(a, b []string) bool {
if len(a) != len(b) {
return false
Expand Down
9 changes: 9 additions & 0 deletions pkg/catalog/types_extra.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ type Typedef struct {
Tipo string
}

// CompoundFieldOverride captures the per-instance metadata that a
// nested <DATA> child of a compound <DATA TIPO="<class>"> can carry.
// Today only Tipo is consumed (codec layout uses it instead of the
// CLASS VAR TIPO for the matching sub-field); richer fields land here
// without ABI churn when callers start needing them.
type CompoundFieldOverride struct {
Tipo string // effective TIPO for this sub-field, e.g. "ENUM_LONG" overriding the CLASS "ENUM"
}

// Info is the <INFO UM=… DP=… KVIS=… …/> child of a DATA leaf. Carries
// display formatting for measurement renderers.
type Info struct {
Expand Down
1 change: 1 addition & 0 deletions pkg/catalog/walk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestWalk(t *testing.T) {
"NomeLinea",
"TEST_EnableBoard",
"TEST_IP_Address",
"TEST_SOGLIA",
"UL1",
}
if !equalSlices(got, want) {
Expand Down
33 changes: 26 additions & 7 deletions pkg/codec/compound.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@ import (
// to uint64 (unsigned) or int64 (signed). Strings decode to Go strings.
// Inline-enum sub-fields decode to their resolved label (string), or fall
// back to the numeric value if the label is missing.
func DecodeCompound(regs []uint16, cls *catalog.Class, _ map[string]*catalog.Enum) (map[string]any, error) {
//
// overrides, when non-nil, carries per-sub-field TIPO overrides from
// the parent <DATA TIPO="<class>">. The CLASS layout is the default;
// each VAR's effective TIPO is the override's TIPO when present. This
// is how compounds whose <CLASS> declares a sub-field as ENUM but the
// instance promotes it to ENUM_LONG (e.g. SOGLIA.Stato in NV10P
// templates) get the right wire width.
func DecodeCompound(regs []uint16, cls *catalog.Class, _ map[string]*catalog.Enum, overrides map[string]*catalog.CompoundFieldOverride) (map[string]any, error) {
if cls == nil {
return nil, fmt.Errorf("DecodeCompound: nil class")
}
out := make(map[string]any, len(cls.Vars))
cursor := 0
for _, v := range cls.Vars {
v = applyOverride(v, overrides)
w := varWidth(v)
if w == 0 {
return nil, fmt.Errorf("DecodeCompound: cannot determine width of VAR %s (TIPO=%s)", v.Name, v.Tipo)
Expand All @@ -38,12 +46,13 @@ func DecodeCompound(regs []uint16, cls *catalog.Class, _ map[string]*catalog.Enu
}

// EncodeCompound is the inverse of DecodeCompound. Used by Set.
func EncodeCompound(values map[string]any, cls *catalog.Class, _ map[string]*catalog.Enum) ([]uint16, error) {
func EncodeCompound(values map[string]any, cls *catalog.Class, _ map[string]*catalog.Enum, overrides map[string]*catalog.CompoundFieldOverride) ([]uint16, error) {
if cls == nil {
return nil, fmt.Errorf("EncodeCompound: nil class")
}
out := make([]uint16, 0, cls.Dim)
for _, v := range cls.Vars {
v = applyOverride(v, overrides)
w := varWidth(v)
if w == 0 {
return nil, fmt.Errorf("EncodeCompound: cannot determine width of VAR %s", v.Name)
Expand All @@ -61,10 +70,20 @@ func EncodeCompound(values map[string]any, cls *catalog.Class, _ map[string]*cat
return out, nil
}

// applyOverride returns v with its TIPO replaced by the override's TIPO
// when one is registered for v.Name. The CLASS VAR's InlineEnum and
// StringLen survive unchanged — overrides today carry only TIPO.
func applyOverride(v catalog.ClassVar, overrides map[string]*catalog.CompoundFieldOverride) catalog.ClassVar {
if ov, ok := overrides[v.Name]; ok && ov.Tipo != "" {
v.Tipo = ov.Tipo
}
return v
}

// varWidth returns the register count for a sub-field, in registers.
func varWidth(v catalog.ClassVar) int {
switch strings.ToUpper(v.Tipo) {
case "BYTE", "UBYTE", "WORD", "UWORD", "BIT16", "ENUM", "ENUM_BYTE":
case "BYTE", "UBYTE", "WORD", "UWORD", "BIT16", "INT", "ENUM", "ENUM_BYTE", "ENUM_WORD":
return 1
case "LONG", "ULONG", "BIT32", "ENUM_LONG":
return 2
Expand All @@ -83,7 +102,7 @@ func decodeVar(v catalog.ClassVar, regs []uint16) (any, error) {
return int64(int8(regs[0] & 0xFF)), nil
case "UBYTE":
return uint64(regs[0] & 0xFF), nil
case "WORD":
case "WORD", "INT":
i, _ := DecodeWORD(regs)
return int64(i), nil
case "UWORD", "BIT16":
Expand All @@ -97,7 +116,7 @@ func decodeVar(v catalog.ClassVar, regs []uint16) (any, error) {
return uint64(u), nil
case "STRING":
return DecodeSTRING(regs)
case "ENUM", "ENUM_BYTE":
case "ENUM", "ENUM_BYTE", "ENUM_WORD":
return resolveEnum(int(regs[0]), v), nil
case "ENUM_LONG":
u, _ := DecodeULONG(regs)
Expand All @@ -124,7 +143,7 @@ func encodeVar(v catalog.ClassVar, val any, width int) ([]uint16, error) {
return []uint16{uint16(toInt64(val) & 0xFF)}, nil
case "UBYTE":
return []uint16{uint16(toUint64(val) & 0xFF)}, nil
case "WORD":
case "WORD", "INT":
return []uint16{uint16(toInt64(val) & 0xFFFF)}, nil
case "UWORD", "BIT16":
return []uint16{uint16(toUint64(val) & 0xFFFF)}, nil
Expand All @@ -137,7 +156,7 @@ func encodeVar(v catalog.ClassVar, val any, width int) ([]uint16, error) {
case "STRING":
s, _ := val.(string)
return encodeStringChars(s, width), nil
case "ENUM", "ENUM_BYTE":
case "ENUM", "ENUM_BYTE", "ENUM_WORD":
return []uint16{uint16(enumNum(val, v) & 0xFFFF)}, nil
case "ENUM_LONG":
u := uint64(enumNum(val, v))
Expand Down
Loading