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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org).

## [1.7.0] - 2026-xx-xx
## [1.7.0] - 2026-04-14

- Add button to open output folder in system viewer
- Fix decoding of ANNOTATION TDF
- Update TDF definitions

## [1.6.0] - 2026-01-16

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "infuse_decoder"
version = "1.6.0"
version = "1.7.0"
edition = "2024"

[[bin]]
Expand Down
31 changes: 26 additions & 5 deletions scripts/tdf.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
{
"name": "type",
"type": "uint8_t",
"description": "Address type (0 == Public, 1 == Random)"
"description": "Address type (0 = Public, 1 = Random)"
},
{
"name": "val",
Expand Down Expand Up @@ -211,7 +211,7 @@
"digits": 12
},
"conversion": {
"int": "little"
"int": "big"
},
"description": "Address bytes"
}
Expand Down Expand Up @@ -322,7 +322,7 @@
{
"name": "flags",
"type": "uint8_t",
"description": "Flags (BIT(0) == SD blocks)",
"description": "Flags (BIT(0) = SD blocks)",
"display": {
"fmt": "hex",
"digits": 2
Expand Down Expand Up @@ -559,7 +559,7 @@
{
"name": "flags",
"type": "uint8_t",
"description": "Flags (BIT(0) == SD blocks)",
"description": "Flags (BIT(0) = SD blocks, BIT(7) = Shipping)",
"display": {
"fmt": "hex",
"digits": 2
Expand Down Expand Up @@ -1244,7 +1244,7 @@
"display": {
"postfix": "B/sec"
},
"description": "Data throughput (-1 == disconnected)"
"description": "Data throughput (-1 = disconnected)"
}
]
},
Expand Down Expand Up @@ -1622,6 +1622,10 @@
{
"name": "infuse_id",
"type": "uint64_t",
"display": {
"fmt": "hex",
"digits": 16
},
"description": "Infuse-IoT ID of remote device"
},
{
Expand Down Expand Up @@ -1938,6 +1942,23 @@
"description": "Right channel sample"
}
]
},
"61": {
"name": "KVS_VALUE_CHANGED",
"description": "Record of key value store data updates",
"fields": [
{
"name": "key",
"type": "uint16_t",
"description": "KV Store key identifier"
},
{
"name": "value",
"type": "uint8_t",
"num": 0,
"description": "New data value, empty for delete, '*' for write-only"
}
]
}
}
}
38 changes: 25 additions & 13 deletions scripts/tdf_decoder.rs.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,29 @@ pub fn tdf_fields(tdf_id: &u16) -> Vec<&'static str>
}
}

fn tdf_field_read_string(cursor: &mut Cursor<&[u8]>, size: u8) -> Result<String>
fn vla_bytes_remaining(cursor: &mut Cursor<&[u8]>, cursor_start: u64, size: u8) -> Result<usize>
{
let mut buf = vec![0u8; size as usize];
let cursor_current = cursor.position();
let cursor_read = cursor_current - cursor_start;
if cursor_read >= size as u64 {
return Result::Err(Error::new(
ErrorKind::InvalidData,
"Insufficient data remaining",
));
}
let bytes_remaining = size as u64 - cursor_read;

Ok(bytes_remaining as usize)
}

fn tdf_field_read_string(cursor: &mut Cursor<&[u8]>, cursor_start: u64, num: u8, size: u8) -> Result<String>
{
let string_length = match num {
0 => vla_bytes_remaining(cursor, cursor_start, size)?,
_ => size as usize,
};

let mut buf = vec![0u8; string_length];
cursor.read_exact(&mut buf)?;

match String::from_utf8(buf) {
Expand All @@ -35,16 +55,8 @@ fn tdf_field_read_string(cursor: &mut Cursor<&[u8]>, size: u8) -> Result<String

fn tdf_field_read_vla(cursor: &mut Cursor<&[u8]>, cursor_start: u64, size: u8) -> Result<String>
{
let cursor_current = cursor.position();
let cursor_read = cursor_current - cursor_start;
if cursor_read >= size as u64 {
return Result::Err(Error::new(
ErrorKind::InvalidData,
"Insufficient data remaining",
));
}
let bytes_remaining = size as u64 - cursor_read;
let mut buf = vec![0u8; bytes_remaining as usize];
let bytes_remaining = vla_bytes_remaining(cursor, cursor_start, size)?;
let mut buf = vec![0u8; bytes_remaining];

cursor.read_exact(&mut buf)?;
Ok(format!("{}", hex::encode(buf)))
Expand Down Expand Up @@ -85,7 +97,7 @@ pub fn tdf_read_into_str(tdf_id: &u16, size: u8, cursor: &mut Cursor<&[u8]>) ->

// Handle read underflow (more data specified than expected)
if underflow > 0 {
tdf_field_read_string(cursor, underflow as u8)?;
tdf_field_read_string(cursor, cursor_start, 0, underflow as u8)?;
}
res
}
121 changes: 62 additions & 59 deletions scripts/tdf_decoder_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@

# C type: (rust_type, ::<LittleEndian>?)
rust_type = {
'char': ('u8', False),
'int8_t': ('i8', False),
'uint8_t': ('u8', False),
'int16_t': ('i16', True),
'uint16_t': ('u16', True),
'int32_t': ('i32', True),
'uint32_t': ('u32', True),
'int64_t': ('i64', True),
'uint64_t': ('u64', True),
'float': ('f32', True),
'float32_t': ('f32', True),
'float64_t': ('f64', True),
"char": ("u8", False),
"int8_t": ("i8", False),
"uint8_t": ("u8", False),
"int16_t": ("i16", True),
"uint16_t": ("u16", True),
"int32_t": ("i32", True),
"uint32_t": ("u32", True),
"int64_t": ("i64", True),
"uint64_t": ("u64", True),
"float": ("f32", True),
"float32_t": ("f32", True),
"float64_t": ("f64", True),
}


def decoders_gen(tdf_defs, output):
env = Environment(
loader=FileSystemLoader(pathlib.Path(__file__).parent),
Expand All @@ -35,30 +36,30 @@ def decoders_gen(tdf_defs, output):
tdf_template = env.get_template("tdf_decoder.rs.jinja")

def field_conv_func(field, name_prefix=None):
t = rust_type[field['type']]
t = rust_type[field["type"]]
func = f"cursor.read_{t[0]}"
if t[1]:
func += "::<LittleEndian>"
func += "()?"
if c := field.get('conversion'):
if endian := c.get('int', None):
assert 'num' in field
assert t[0] == 'u8'
e = 'LittleEndian' if endian == 'little' else 'BigEndian'
if field['num'] == 3:
t = 'u24'
elif field['num'] == 6:
t = 'u48'
if c := field.get("conversion"):
if endian := c.get("int", None):
assert "num" in field
assert t[0] == "u8"
e = "LittleEndian" if endian == "little" else "BigEndian"
if field["num"] == 3:
t = "u24"
elif field["num"] == 6:
t = "u48"
else:
raise RuntimeError("Unknown integer length")

func = f"cursor.read_{t}::<{e}>()?"
del field['num']
del field["num"]

if 'm' in c or 'c' in c:
func += ' as f64'
if 'm' in c and c['m'] != 0:
val = c['m']
if "m" in c or "c" in c:
func += " as f64"
if "m" in c and c["m"] != 0:
val = c["m"]
inverse_ratio = (1 / val).as_integer_ratio()
# If number can be represented as a whole number division, use that
# instead for numerical stability (/ 10) is better than (* 0.1) as
Expand All @@ -67,81 +68,83 @@ def field_conv_func(field, name_prefix=None):
func += f" / {inverse_ratio[0]}.0"
else:
func += f" * {float_format(c['m'])}"
if 'c' in c and c['c'] != 0:
if "c" in c and c["c"] != 0:
func += f" + {float_format(c['c'])}"

n = field['name']
n = field["name"]
if name_prefix is not None:
n = f"{name_prefix}." + n

if 'num' in field:
if field['type'] == 'char':
return [(n, f"tdf_field_read_string(cursor, {field['num']})?")]
if "num" in field:
if field["type"] == "char":
return [
(
n,
f"tdf_field_read_string(cursor, cursor_start, {field['num']}, size)?",
)
]
else:
if field['num'] == 0:
return [(n, f"tdf_field_read_vla(cursor, cursor_start, size)?")]
if field["num"] == 0:
return [(n, "tdf_field_read_vla(cursor, cursor_start, size)?")]
else:
return [(n + f'[{idx}]', func) for idx in range(field['num'])]
return [(n + f"[{idx}]", func) for idx in range(field["num"])]
else:
return [(n, func)]

def field_fmt(field):
if field['type'] == 'char':
if field["type"] == "char":
return ["{}"]
if 'display' in field and field['display'].get('fmt', '') == "hex":
if digits := field['display'].get('digits', None):
if "display" in field and field["display"].get("fmt", "") == "hex":
if digits := field["display"].get("digits", None):
single = [f"0x{{:0{digits}x}}"]
else:
single = ["0x{:x}"]
else:
single = ["{}"]
if field.get('num', None) == 0:
if field.get("num", None) == 0:
return ["{}"]
return single * field.get('num', 1)

return single * field.get("num", 1)

structs = {}
struct_fmts = {}
for name, struct in tdf_defs['structs'].items():
for name, struct in tdf_defs["structs"].items():
funcs = []
fmts = []
for f in struct['fields']:
for f in struct["fields"]:
funcs += field_conv_func(f)
fmts += field_fmt(f)
structs[f"struct {name}"] = funcs
struct_fmts[f"struct {name}"] = fmts

# Generate rust conversion functions
for tdf_id, info in tdf_defs['definitions'].items():
info['rust_convs'] = []
for tdf_id, info in tdf_defs["definitions"].items():
info["rust_convs"] = []
fmt = []
for f in info['fields']:
if f['type'] in structs:
info['rust_convs'] += structs[f['type']]
fmt += struct_fmts[f['type']]
elif f['type'] in rust_type:
info['rust_convs'] += field_conv_func(f)
for f in info["fields"]:
if f["type"] in structs:
info["rust_convs"] += structs[f["type"]]
fmt += struct_fmts[f["type"]]
elif f["type"] in rust_type:
info["rust_convs"] += field_conv_func(f)
fmt += field_fmt(f)
else:
raise RuntimeError(f"Bad type '{f['type']}'")

info['rust_head'] = ",".join([f"\"{c[0]}\"" for c in info['rust_convs']])
info['rust_fmt'] = ",".join(fmt)

info["rust_head"] = ",".join([f'"{c[0]}"' for c in info["rust_convs"]])
info["rust_fmt"] = ",".join(fmt)

tdf_output = pathlib.Path(output) / 'decoders.rs'
tdf_output = pathlib.Path(output) / "decoders.rs"
with tdf_output.open("w") as f:
f.write(
tdf_template.render(
structs=tdf_defs["structs"], definitions=tdf_defs["definitions"]
structs=tdf_defs["structs"], definitions=tdf_defs["definitions"]
)
)
f.write(os.linesep)


if __name__ == "__main__":
parser = argparse.ArgumentParser(
"Generate rust TDF decoders", allow_abbrev=False
)
parser = argparse.ArgumentParser("Generate rust TDF decoders", allow_abbrev=False)
parser.add_argument("--json", required=True, type=str, help="TDF json description")
parser.add_argument("--out", required=True, type=str, help="Output folder")
args = parser.parse_args()
Expand Down
Loading
Loading