Repository: JHUAPL-DTNMA/dtnma-camp
Pull request #31: Add C code generation support for rules
Author: login: justinethier
Created: 2025-04-09T21:35:04Z | Merged: 2025-05-22T18:19:13Z

Add C code generation for SBR and TBR.

Depends on JHUAPL-DTNMA/dtnma-ace#53


Diff:
diff --git a/src/camp/generators/data/agent.c.jinja b/src/camp/generators/data/agent.c.jinja
index 4b9e7e6..f1cf97e 100644
--- a/src/camp/generators/data/agent.c.jinja
+++ b/src/camp/generators/data/agent.c.jinja
@@ -103,6 +103,16 @@ cace_ari_set_tstr({{ptrname}}, {{value.value | c_str}}, true);
     cace_data_init_view(&data, sizeof(raw), (cace_data_ptr_t)raw);
     cace_ari_set_bstr({{ptrname}}, &data, true);
 }
+{%- elif value is ari_builtin('tp') -%}
+{
+  struct timespec ts = {{ value.value | as_timepoint }};
+  cace_ari_set_tp({{ptrname}}, ts);
+}
+{%- elif value is ari_builtin('td') -%}
+{
+  struct timespec ts = {{ value.value | as_timedelta }};
+  cace_ari_set_td({{ptrname}}, ts);
+}
 {#- Non-primitive types #}
 {%- elif value is ari_builtin('aritype') -%}
 cace_ari_set_aritype({{ptrname}}, {{value.value | cpp_enum}});
@@ -405,7 +415,54 @@ int {{adm | c_func(suffix='init')}}(refda_agent_t *agent)
         }
 {%- endfor %}
 {%- endif %}
-{# FIXME add SBR and TBRs #}
+{%- if adm.sbr %}
+
+        /**
+         * Register SBR objects
+         */
+{%- for obj in adm.sbr %}
+        { // For {{obj | ref_text}}
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {{ ari_set_value('&(objdata->action)', obj.action_ari) | indent(12) }}
+            // condition
+            {{ ari_set_value('&(objdata->condition)', obj.condition_ari) | indent(12) }}
+            // min_interval
+            {{ ari_set_value('&(objdata->min_interval)', obj.min_interval_ari) | indent(12) }}
+            // init_enabled
+            objdata->init_enabled = {{obj.init_enabled | c_bool | indent(12)}};
+            // max_exec_count
+            objdata->max_exec_count = {{obj.max_count | c_int | indent(12)}};
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum({{obj.norm_name | c_str}}, {{obj | cpp_enum}}), objdata);
+        }
+{%- endfor %}
+{%- endif %}
+{%- if adm.tbr %}
+
+        /**
+         * Register TBR objects
+         */
+{%- for obj in adm.tbr %}
+        { // For {{obj | ref_text}}
+            refda_amm_tbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_tbr_desc_t));
+            refda_amm_tbr_desc_init(objdata);
+            // action
+            {{ ari_set_value('&(objdata->action)', obj.action_ari) | indent(12) }}
+            // period
+            {{ ari_set_value('&(objdata->period)', obj.period_ari) | indent(12) }}
+            // start_time
+            {{ ari_set_value('&(objdata->start_time)', obj.start_ari) | indent(12) }}
+            // init_enabled
+            objdata->init_enabled = {{obj.init_enabled | c_bool | indent(12)}};
+            // max_exec_count
+            objdata->max_exec_count = {{obj.max_count | c_int | indent(12)}};
+
+            obj = refda_register_tbr(adm, cace_amm_idseg_ref_withenum({{obj.norm_name | c_str}}, {{obj | cpp_enum}}), objdata);
+        }
+{%- endfor %}
+{%- endif %}
     }
     REFDA_AGENT_UNLOCK(agent, REFDA_AGENT_ERR_LOCK_FAILED);
     return 0;
diff --git a/src/camp/generators/data/agent.h.jinja b/src/camp/generators/data/agent.h.jinja
index 0806bb1..96fcb69 100644
--- a/src/camp/generators/data/agent.h.jinja
+++ b/src/camp/generators/data/agent.h.jinja
@@ -100,6 +100,26 @@ extern "C" {
 {%- endfor %}
 {%- endif %}
 
+{%- if adm.sbr %}
+
+/*
+ * Enumerations for SBR objects
+ */
+{%- for obj in adm.sbr %}
+{{obj_enum(obj)}}
+{%- endfor %}
+{%- endif %}
+
+{%- if adm.tbr %}
+
+/*
+ * Enumerations for TBR objects
+ */
+{%- for obj in adm.tbr %}
+{{obj_enum(obj)}}
+{%- endfor %}
+{%- endif %}
+
 /** Initializer for the ADM module {{adm.module_name}}.
  * @param[in,out] agent The agent to register this namespace and its
  * objects within.
diff --git a/src/camp/generators/lib/campch.py b/src/camp/generators/lib/campch.py
index 4e63d6d..1c3bea7 100644
--- a/src/camp/generators/lib/campch.py
+++ b/src/camp/generators/lib/campch.py
@@ -25,14 +25,14 @@
 import logging
 import ace.models
 import jinja2
+import math
 import textwrap
-import typing
 from typing import Union, Optional
 import ace
 from ace.typing import BUILTINS
 from ace import models, ari, ari_text
 from ace.lookup import dereference, ORM_TYPE
-import re
+from datetime import datetime, timedelta, timezone
 
 LOGGER = logging.getLogger(__name__)
 
@@ -53,6 +53,7 @@ def yang_to_sql(identifier):
         return identifier.replace('-', '_').replace('.', '_p_')
     return ""
 
+
 def update_jinja_env(env:jinja2.Environment, admset, sym_prefix:str):
     ''' Set state of a jinja environment for ADM implementation
     source generation.
@@ -150,6 +151,17 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timepoint(value:datetime) -> str:
+        diff = value - ari.DTN_EPOCH
+        tv_sec = diff.seconds + 24 * 60 * 60 * diff.days
+        tv_nsec = 1000 * diff.microseconds  # Convert to nanoseconds
+        return f"{{{tv_sec}, {tv_nsec}}}"
+
+    def as_timedelta(value:timedelta) -> str:
+        tv_sec = value.seconds + 24 * 60 * 60 * value.days
+        tv_nsec = 1000 * value.microseconds  # Convert to nanoseconds
+        return f"{{{tv_sec}, {tv_nsec}}}"
+
     def ref_text(obj:models.AdmObjMixin) -> str:
         ''' Create a text reference for an AMM object.
         '''
@@ -165,7 +177,7 @@ def ari_builtin(ari:ari.ARI, typename:str) -> bool:
         typeobj = BUILTINS[typename]
         got = typeobj.get(ari)
         return got is not None
-    
+
     def sql_name(value:str) -> str:
         ''' valid sql name
         '''
@@ -175,19 +187,17 @@ def sql_var_name(obj:ace.models.AdmObjMixin) -> str:
         ''' formatting a name to be a sql variable
         '''
         if obj:
-            return yang_to_sql(obj.__tablename__).lower()+"_"+yang_to_sql(obj.name).lower()
+            return yang_to_sql(obj.__tablename__).lower() + "_" + yang_to_sql(obj.name).lower()
         return "None"
 
-
     def sql_string(value:str) -> str:
-        ''' escape string and add quotes for sql 
+        ''' escape string and add quotes for sql
         '''
         if value:
-            return  '\'' + value.replace('\\', '\\\\').replace("'",' `').replace('\n', ' ') + '\''
+            return  '\'' + value.replace('\\', '\\\\').replace("'", ' `').replace('\n', ' ') + '\''
         else:
             return '\'\''
 
-    
     env.globals |= {
         'ari': ace.ari,
         'typing': ace.typing,
@@ -205,13 +215,13 @@ def sql_string(value:str) -> str:
         'c_bytes_init': c_bytes_init,
         'rewrap': rewrap,
         'as_text': as_text,
+        'as_timepoint': as_timepoint,
+        'as_timedelta': as_timedelta,
         'ref_text': ref_text,
         'deref': deref,
         'sql_name': sql_name,
         'sql_string': sql_string,
         'sql_var_name': sql_var_name,
-        
-
 
     }
     env.tests |= {
diff --git a/test/data/example-test.yang b/test/data/example-test.yang
index 45b6a3c..5b31d07 100644
--- a/test/data/example-test.yang
+++ b/test/data/example-test.yang
@@ -171,4 +171,30 @@ module example-test {
       amm:type "/ARITYPE/UVAST";
     }
   }
+  amm:sbr sbr1 {
+    amm:enum 8;
+    description
+      "";
+    amm:action "/AC/(./CTRL/first,./CTRL/second)";
+    amm:condition "/AC/(./EDD/sensor,./VAR/min_threshold,./OPER/compare_lt)";
+    amm:min-interval "/TD/PT30S";
+    amm:init-enabled false;
+    amm:max-count 10;
+  }
+
+  amm:sbr sbr2 {
+    amm:enum 9;
+    description
+      "";
+    amm:action "/AC/(./CTRL/first,./CTRL/second)";
+    amm:condition "/AC/(./EDD/sensor,./VAR/min_threshold,./OPER/compare_lt)";
+  }
+  amm:tbr tbr_rule {
+    amm:enum 30;
+    description
+      "Generate telemetry reports.";
+    amm:action "/AC/(./CTRL/first,./CTRL/other)";
+    amm:period "/TD/PT30S";
+    amm:start "/TP/20.5";
+  }
 }
diff --git a/test/data/gen_ch/example_test.c.jinja b/test/data/gen_ch/example_test.c.jinja
index 3926584..02c6797 100644
--- a/test/data/gen_ch/example_test.c.jinja
+++ b/test/data/gen_ch/example_test.c.jinja
@@ -489,6 +489,153 @@ int refda_adm_example_test_init(refda_agent_t *agent)
             // no parameters
         }
 
+        /**
+         * Register SBR objects
+         */
+        { // For ./SBR/sbr1
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/second
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // condition
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/EDD/sensor
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/VAR/min_threshold
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/OPER/compare_lt
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->condition), &acinit);
+            }
+            // min_interval
+            {
+              struct timespec ts = {30, 0};
+              cace_ari_set_td(&(objdata->min_interval), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = false;
+            // max_exec_count
+            objdata->max_exec_count = 10;
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum("sbr1", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR1), objdata);
+        }
+        { // For ./SBR/sbr2
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/second
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // condition
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/EDD/sensor
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/VAR/min_threshold
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/OPER/compare_lt
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->condition), &acinit);
+            }
+            // min_interval
+            {
+              struct timespec ts = {0, 0};
+              cace_ari_set_td(&(objdata->min_interval), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = true;
+            // max_exec_count
+            objdata->max_exec_count = 0;
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum("sbr2", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR2), objdata);
+        }
+
+        /**
+         * Register TBR objects
+         */
+        { // For ./TBR/tbr_rule
+            refda_amm_tbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_tbr_desc_t));
+            refda_amm_tbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/other
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // period
+            {
+              struct timespec ts = {30, 0};
+              cace_ari_set_td(&(objdata->period), ts);
+            }
+            // start_time
+            {
+              struct timespec ts = {20, 500000000};
+              cace_ari_set_tp(&(objdata->start_time), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = true;
+            // max_exec_count
+            objdata->max_exec_count = 0;
+
+            obj = refda_register_tbr(adm, cace_amm_idseg_ref_withenum("tbr_rule", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_TBR_TBR__RULE), objdata);
+        }
     }
     REFDA_AGENT_UNLOCK(agent, REFDA_AGENT_ERR_LOCK_FAILED);
     return 0;
diff --git a/test/data/gen_ch/example_test.h.jinja b/test/data/gen_ch/example_test.h.jinja
index 708c593..9f40557 100644
--- a/test/data/gen_ch/example_test.h.jinja
+++ b/test/data/gen_ch/example_test.h.jinja
@@ -74,6 +74,20 @@ extern "C" {
 /// For ./OPER/add
 #define REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_OPER_ADD 0
 
+/*
+ * Enumerations for SBR objects
+ */
+/// For ./SBR/sbr1
+#define REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR1 8
+/// For ./SBR/sbr2
+#define REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR2 9
+
+/*
+ * Enumerations for TBR objects
+ */
+/// For ./TBR/tbr_rule
+#define REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_TBR_TBR__RULE 30
+
 /** Initializer for the ADM module example-test.
  * @param[in,out] agent The agent to register this namespace and its
  * objects within.


Reviews:
user: login: BrianSipos
body: Minor comments that can be discussed separately
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T18:29:59Z
user: login: BrianSipos
body: One remaining comment that may be untested/dead code which can have a test case added to exercise it and a suggestion to fix it.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T13:52:48Z
user: login: justinethier
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T19:40:42Z
user: login: BrianSipos
body: Thank you for adding the test cases, there are a couple of fixes that need to be made. Can discuss detail or, if you want, I can commit an alternative.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-02T15:17:46Z
user: login: justinethier
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T21:01:00Z
user: login: justinethier
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T21:01:09Z
user: login: justinethier
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T21:04:18Z
user: login: BrianSipos
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T02:26:02Z

Review comments:
diff_hunk: @@ -150,6 +151,12 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timespec(value:datetime) -> str:
path: src/camp/generators/lib/campch.py
user: login: BrianSipos
body: Is this argument really a datetime or is it a timedelta?
They will need to be handled differently on the C side 
created_at: 2025-04-15T18:26:43Z
author_association: MEMBER
diff_hunk: @@ -150,6 +151,12 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timespec(value:datetime) -> str:
+      seconds = value.total_seconds()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"struct timespec ts; ts.tv_sec = {tv_sec}; ts.tv_nsec = {tv_nsec};"
path: src/camp/generators/lib/campch.py
user: login: BrianSipos
body: This can be made to use an inline struct field setter, which would avoid an implicit variable symbol name that could be a source of name collision depending on how it's used.
created_at: 2025-04-15T18:28:41Z
author_association: MEMBER
diff_hunk: @@ -150,6 +151,18 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timeperiod(value:datetime) -> str:
+      seconds = value.total_seconds()
path: src/camp/generators/lib/campch.py
user: login: BrianSipos
body: I don't think that a `datetime` object has this method. A timedelta can be computed as a difference from the DTN time epoch (which is a constant in one of the ACE modules) and then the `total_seconds()` can be used. Does this filter function actually get exercised by jinja?
created_at: 2025-04-17T13:51:52Z
author_association: MEMBER
diff_hunk: @@ -150,6 +151,18 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timeperiod(value:datetime) -> str:
+      seconds = value.total_seconds()
path: src/camp/generators/lib/campch.py
user: login: justinethier
body: Good point - `datetime` does not. I've updated the method and integrated `TP` into the unit testing.
created_at: 2025-05-01T19:40:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,6 +103,16 @@ cace_ari_set_tstr({{ptrname}}, {{value.value | c_str}}, true);
     cace_data_init_view(&data, sizeof(raw), (cace_data_ptr_t)raw);
     cace_ari_set_bstr({{ptrname}}, &data, true);
 }
+{%- elif value is ari_builtin('tp') -%}
+{
+  struct timespec ts = {{ value.value | as_timeperiod }};
path: src/camp/generators/data/agent.c.jinja
user: login: BrianSipos
body: The TP stands for time point, so the filter should probably use that term instead
created_at: 2025-05-02T01:24:02Z
author_association: MEMBER
diff_hunk: @@ -150,6 +151,18 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timeperiod(value:datetime) -> str:
+      seconds = value.replace(tzinfo=timezone.utc).timestamp()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"{{{tv_sec}, {tv_nsec}}}"
path: src/camp/generators/lib/campch.py
user: login: BrianSipos
body: This does not work properly; it is probably easiest to use the days, seconds, microseconds [timedelta](https://docs.python.org/3/library/datetime.html#datetime.timedelta) fields directly and convert with integer arithmetic.
created_at: 2025-05-02T15:13:08Z
author_association: MEMBER
diff_hunk: @@ -150,6 +151,18 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timeperiod(value:datetime) -> str:
+      seconds = value.replace(tzinfo=timezone.utc).timestamp()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"{{{tv_sec}, {tv_nsec}}}"
+
+    def as_timedelta(value:timedelta) -> str:
+      seconds = value.total_seconds()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"{{{tv_sec}, {tv_nsec}}}"
path: src/camp/generators/lib/campch.py
user: login: BrianSipos
body: Same comment as above.
created_at: 2025-05-02T15:13:19Z
author_association: MEMBER
diff_hunk: @@ -489,6 +489,153 @@ int refda_adm_example_test_init(refda_agent_t *agent)
             // no parameters
         }
 
+        /**
+         * Register SBR objects
+         */
+        { // For ./SBR/sbr1
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/second
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // condition
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/EDD/sensor
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/VAR/min_threshold
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/OPER/compare_lt
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->condition), &acinit);
+            }
+            // min_interval
+            {
+              struct timespec ts = {30, 0};
+              cace_ari_set_td(&(objdata->min_interval), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = false;
+            // max_exec_count
+            objdata->max_exec_count = 10;
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum("sbr1", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR1), objdata);
+        }
+        { // For ./SBR/sbr2
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/second
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // condition
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/EDD/sensor
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/VAR/min_threshold
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/OPER/compare_lt
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->condition), &acinit);
+            }
+            // min_interval
+            {
+              struct timespec ts = {0, 0};
+              cace_ari_set_td(&(objdata->min_interval), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = true;
+            // max_exec_count
+            objdata->max_exec_count = 0;
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum("sbr2", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR2), objdata);
+        }
+
+        /**
+         * Register TBR objects
+         */
+        { // For ./TBR/tbr_rule
+            refda_amm_tbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_tbr_desc_t));
+            refda_amm_tbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/other
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // period
+            {
+              struct timespec ts = {30, 0};
+              cace_ari_set_td(&(objdata->period), ts);
+            }
+            // start_time
+            {
+              struct timespec ts = {946684820, 5};
path: test/data/gen_ch/example_test.c.jinja
user: login: BrianSipos
body: This is where it doesn't correspond with the input `/TP/20.5`, which would be `{CACE_ARI_DTN_EPOCH + 20, 500000000}`. Using the CACE macro may actually be good to do in the generator, since the ARI includes are guaranteed anyway.
created_at: 2025-05-02T15:16:49Z
author_association: MEMBER
diff_hunk: @@ -150,6 +151,18 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timeperiod(value:datetime) -> str:
+      seconds = value.replace(tzinfo=timezone.utc).timestamp()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"{{{tv_sec}, {tv_nsec}}}"
path: src/camp/generators/lib/campch.py
user: login: justinethier
body: Updated the logic to properly convert fractional seconds to nanoseconds. 
created_at: 2025-05-14T21:01:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,6 +151,18 @@ def as_text(ari:ari.ARI) -> str:
         enc.encode(ari, buf)
         return buf.getvalue()
 
+    def as_timeperiod(value:datetime) -> str:
+      seconds = value.replace(tzinfo=timezone.utc).timestamp()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"{{{tv_sec}, {tv_nsec}}}"
+
+    def as_timedelta(value:timedelta) -> str:
+      seconds = value.total_seconds()
+      tv_sec = int(seconds)
+      tv_nsec = int(str(seconds).split(".")[1]) if "." in str(seconds) else 0 
+      return f"{{{tv_sec}, {tv_nsec}}}"
path: src/camp/generators/lib/campch.py
user: login: justinethier
body: Updated the logic to properly convert fractional seconds to nanoseconds. 
created_at: 2025-05-14T21:01:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,6 +489,153 @@ int refda_adm_example_test_init(refda_agent_t *agent)
             // no parameters
         }
 
+        /**
+         * Register SBR objects
+         */
+        { // For ./SBR/sbr1
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/second
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // condition
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/EDD/sensor
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/VAR/min_threshold
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/OPER/compare_lt
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->condition), &acinit);
+            }
+            // min_interval
+            {
+              struct timespec ts = {30, 0};
+              cace_ari_set_td(&(objdata->min_interval), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = false;
+            // max_exec_count
+            objdata->max_exec_count = 10;
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum("sbr1", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR1), objdata);
+        }
+        { // For ./SBR/sbr2
+            refda_amm_sbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_sbr_desc_t));
+            refda_amm_sbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/second
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // condition
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/EDD/sensor
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/VAR/min_threshold
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/OPER/compare_lt
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->condition), &acinit);
+            }
+            // min_interval
+            {
+              struct timespec ts = {0, 0};
+              cace_ari_set_td(&(objdata->min_interval), ts);
+            }
+            // init_enabled
+            objdata->init_enabled = true;
+            // max_exec_count
+            objdata->max_exec_count = 0;
+
+            obj = refda_register_sbr(adm, cace_amm_idseg_ref_withenum("sbr2", REFDA_ADM_EXAMPLE_TEST_ENUM_OBJID_SBR_SBR2), objdata);
+        }
+
+        /**
+         * Register TBR objects
+         */
+        { // For ./TBR/tbr_rule
+            refda_amm_tbr_desc_t *objdata = CACE_MALLOC(sizeof(refda_amm_tbr_desc_t));
+            refda_amm_tbr_desc_init(objdata);
+            // action
+            {
+                cace_ari_ac_t acinit;
+                cace_ari_ac_init(&acinit);
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/first
+                    // FIXME reference to unknown object
+                }
+                {
+                    cace_ari_t *item = cace_ari_list_push_back_new(acinit.items);
+                    // ari://example/test/CTRL/other
+                    // FIXME reference to unknown object
+                }
+                cace_ari_set_ac(&(objdata->action), &acinit);
+            }
+            // period
+            {
+              struct timespec ts = {30, 0};
+              cace_ari_set_td(&(objdata->period), ts);
+            }
+            // start_time
+            {
+              struct timespec ts = {946684820, 5};
path: test/data/gen_ch/example_test.c.jinja
user: login: justinethier
body: Using `CACE_ARI_DTN_EPOCH` is a good idea however ace is already incorporating the DTN epoch. With the above changes to emit proper nanosecond values we are now generating the proper timestamps.
created_at: 2025-05-14T21:04:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: justinethier
created_at: 2025-05-01T19:41:43Z
body: @BrianSipos OK, this is ready for your re-review.
author_association: CONTRIBUTOR
user: login: justinethier
created_at: 2025-05-14T21:05:57Z
body: > Thank you for adding the test cases, there are a couple of fixes that need to be made. Can discuss detail or, if you want, I can commit an alternative.

@BrianSipos Please review the latest changes. If you would like to make additional changes feel free to commit as needed or message me to discuss.
author_association: CONTRIBUTOR
user: login: BrianSipos
created_at: 2025-05-15T02:20:59Z
body: I updated the time processing to use only integer arithmetic, which avoids possible loss of precision when using python float. There was also a misuse of the `cace_ari_set_tp()` which uses DTN epoch as opposed to `cace_ari_set_tp_posix()`.

If the difference was unclear, a separate change can update the CACE API to call it `cace_ari_set_tp_dtn()` and similar for the getter.
author_association: MEMBER