Determine location of item inside a union

Question for all the C/C++ gurus out there.

This is a simplified snippet of a union structure currently in use. (the actual code is 169 elements long, hence snippet)

typedef union
 {
  uint16_t au16data[15];
  struct
   {
    uint16_t ON_Setpoint;
    uint16_t OFF_Setpoint;
    uint16_t Control_Type; // absolute/differential
    uint16_t BrightLight_Setpoint;
    uint16_t Dark_Setpoint;

    uint16_t Panel_1_Supply_Temp;
    uint16_t Panel_1_Return_Temp;
    uint16_t Panel_1_Supply_Pres;
    uint16_t Panel_1_Return_Pres;
    uint16_t Panel_1_Flow_Rate;

    uint16_t Panel_2_Supply_Temp;
    uint16_t Panel_2_Return_Temp;
    uint16_t Panel_2_Supply_Pres;
    uint16_t Panel_2_Return_Pres;
    uint16_t Panel_2_Flow_Rate;
   };
 } modbus_registers_t;

The data type is used for communication via Modbus. The Modbus programming requires specifying the starting location in the array and the number of elements for the read/write commands.

Currently I have a couple of sets of positions and lengths. I would like to predetermine where in the array the start element is, without typing the actual number. For example, to read from panel 1 I must specify P=5 and L=5. But if I insert an item in the first group, then I have to find everywhere in the code where P=5 and change it to P=6 and change P=10 to P=11. I would like to predetermine the location of Panel_1_Supply_Temp relative to the start of the array. In the above example it would 5. Changing to

typedef union
 {
  uint16t au16data[18];
  struct
   {
    uint16_t ON_Setpoint;
    uint16_t OFF_Setpoint;
    uint16_t Control_Type; // absolute/differential
    uint16_t BrightLight_Setpoint;
    uint16_t Dark_Setpoint;
    uint16_t Hour;      // <- Added
    uint16_t Minute;   // <- Added
    uint16_t Second;  // <- Added

    uint16_t Panel_1_Supply_Temp;
    uint16_t Panel_1_Return_Temp;
    uint16_t Panel_1_Supply_Pres;
    uint16_t Panel_1_Return_Pres;
    uint16_t Panel_1_Flow_Rate;

    uint16_t Panel_2_Supply_Temp;
    uint16_t Panel_2_Return_Temp;
    uint16_t Panel_2_Supply_Pres;
    uint16_t Panel_2_Return_Pres;
    uint16_t Panel_2_Flow_Rate;
   {
 } modbus_registers_t;

The result would be 8.

The best solution would for it to be determined at compile time (preprocessor directive?). Obviously the array structure will not change once downloaded so there is no need to repetitively compute the location. It could be stored in a global variable and computed once in setup(); again since the location will not change once downloaded that would be a waste of valuable RAM.

Is this possible, especially at compile time?

I had to do something similar back in the eighties. I was using plain old C then and couldn't figure out how to do it. One of my colleagues suggested casting a null pointer to a pointer to the type of struct I was trying to get offsets into and making dirty use of the fact that NULL is 0.

I did and it worked. I would hope that C++ offers you a better alternative. If not, here is your example in all it's ugliness:

typedef union
 {
  uint16_t au16data[15];
  struct
   {
    uint16_t ON_Setpoint;
    uint16_t OFF_Setpoint;
    uint16_t Control_Type; // absolute/differential
    uint16_t BrightLight_Setpoint;
    uint16_t Dark_Setpoint;

    uint16_t Panel_1_Supply_Temp;
    uint16_t Panel_1_Return_Temp;
    uint16_t Panel_1_Supply_Pres;
    uint16_t Panel_1_Return_Pres;
    uint16_t Panel_1_Flow_Rate;

    uint16_t Panel_2_Supply_Temp;
    uint16_t Panel_2_Return_Temp;
    uint16_t Panel_2_Supply_Pres;
    uint16_t Panel_2_Return_Pres;
    uint16_t Panel_2_Flow_Rate;
   }x;
 } modbus_registers_t;
 
void setup()
{
Serial.begin(115200);           // start serial for output
void *ptr=NULL;
long a=&(((modbus_registers_t*)ptr)->x.Panel_1_Supply_Temp);
Serial.println(a);
}

void loop()
{
}

Shows 10 on my Uno meaning bytes into the struct.

wildbill:
I had to do something similar back in the eighties. I was using plain old C then and couldn't figure out how to do it. One of my colleagues suggested casting a null pointer to a pointer to the type of struct I was trying to get offsets into and making dirty use of the fact that NULL is 0.

I did and it worked. I would hope that C++ offers you a better alternative. If not, here is your example in all it's ugliness:

typedef union

{
  uint16_t au16data[15];
  struct
  {
    uint16_t ON_Setpoint;
    uint16_t OFF_Setpoint;
    uint16_t Control_Type; // absolute/differential
    uint16_t BrightLight_Setpoint;
    uint16_t Dark_Setpoint;

uint16_t Panel_1_Supply_Temp;
    uint16_t Panel_1_Return_Temp;
    uint16_t Panel_1_Supply_Pres;
    uint16_t Panel_1_Return_Pres;
    uint16_t Panel_1_Flow_Rate;

uint16_t Panel_2_Supply_Temp;
    uint16_t Panel_2_Return_Temp;
    uint16_t Panel_2_Supply_Pres;
    uint16_t Panel_2_Return_Pres;
    uint16_t Panel_2_Flow_Rate;
  }x;
} modbus_registers_t;

void setup()
{
Serial.begin(115200);          // start serial for output
void ptr=NULL;
long a=&(((modbus_registers_t
)ptr)->x.Panel_1_Supply_Temp);
Serial.println(a);
}

void loop()
{
}




Shows 10 on my Uno meaning bytes into the struct.

How does that come up with 10? I understand the 10=10b ytes, but not sure I follow the dereferencing.

I was thinking something similar would be the runtime solution. Actually I was thinking it would take more math. Find the address of the start of the array, find the address of the element, subtract and then divide. Looks like the last step is true enough.

ptr is null and is of void* type. Then I cast it to be a pointer to modbus_registers_t*. Then I use that to get the address of the Panel_1_Supply_Temp inside the struct, which I called x because the compiler insisted that it have a name.

Because null is zero, the address of the (actually non existent) Panel_1_Supply_Temp element of the struct is its offset from the start of the struct (and union).

It's a filthy hack, for which you can blame my old friend Larry :slight_smile:

I would like to predetermine where in the array the start element is, without typing the actual number.

i'm having a hard time understanding your issue

it sounds like there are different locations to use for different types (? panels) of data being communicated.

would having a value indicating the type of data/message in your message help?

we use to use TLVs to describe one or more data

It occurs to me that the void* pointer is an unnecessary complication. This does the same job:

void setup()
{
Serial.begin(115200);           // start serial for output
modbus_registers_t*ptr=NULL;
long a=&(ptr->x.Panel_1_Supply_Temp);
Serial.println(a);
}

Try:
http://www.cplusplus.com/reference/cstddef/offsetof/
Note that the result is in bytes (same as sizeof).

wildbill:
It occurs to me that the void* pointer is an unnecessary complication. This does the same job:

void setup()

{
Serial.begin(115200);           // start serial for output
modbus_registers_t*ptr=NULL;
long a=&(ptr->x.Panel_1_Supply_Temp);
Serial.println(a);
}

I understand that a bit better. Not sure why you had to define the struct in the union as x. I tend to program in layers. So the full union-struct works with the Modbus programming I have. Now, as several times before, I have added elements and need to adjust the programming in several sketches when I do. Doing this programatically allows me to just recompile the other devices and download without actually changing any code.

gfvalvo:
Try:
http://www.cplusplus.com/reference/cstddef/offsetof/
Note that the result is in bytes (same as sizeof).

They seriously made an instruction to do this!! :o

gcjr:
i'm having a hard time understanding your issue

it sounds like there are different locations to use for different types (? panels) of data being communicated.

would having a value indicating the type of data/message in your message help?

we use to use TLVs to describe one or more data

You would need more (irrelevant) details about modbus telegrams. Just know they need to know where Panel_1_Supply_Temp is in the array, and that value is used as the position in the Modbus communication telegram. If I add an itms or multiple items before Panel_1_Supply_Temp, then the position in the array moves and I have 6+ sketches to manually update. gfvalvo found a instruction that does the exact item (only return in bytes instead of index, but I somewhat expected that and can just divide by the size of uint16_t to convert.

wildbill and gfvalvo. The solutions are great, but they fall into the category of "determined at runtime". Is there anything that can be done preprocessor such that we can save on RAM (ie., global variable) and/or clock cycles (computed each function call)?

gfvalvo:
Try:
http://www.cplusplus.com/reference/cstddef/offsetof/
Note that the result is in bytes (same as sizeof).

Nice. It looks like it's been around for a long time too - I may have missed a trick when I needed to do this.

adwsystems:
wildbill and gfvalvo. The solutions are great, but they fall into the category of "determined at runtime". Is there anything that can be done preprocessor such that we can save on RAM (ie., global variable) and/or clock cycles (computed each function call)?

What makes you say that? 'offsetof' is a pre-processor macro, same as sizeof.

gfvalvo:
What makes you say that? 'offsetof' is a pre-processor macro, same as sizeof.

Because the example had it in the execution portion of the code.

adwsystems:
Because the example had it in the execution portion of the code.

So?

gfvalvo:
So?

So? Nothing. Just in shock and not thinking straight.

gfvalvo:
Try:
http://www.cplusplus.com/reference/cstddef/offsetof/
Note that the result is in bytes (same as sizeof).

I assume the struct inside the union needs a name (like wildbill's solution) in order to use the offset of macro. So I added one. Now none of the sketches compile.

The union is instantiated modbus_registers_t modbus_register and modbus_register.Control_Type (or any member of the struct) does not exist.

Pool_Heater_Panel_V4:673:37: error: 'union modbus_registers_t' has no member named 'Light_Level'

      Serial.println(modbus_register.Light_Level.West);

                                     ^

exit status 1
'union modbus_registers_t' has no member named 'Control_Type'

Am I missing something or is this rewrite going to be ugly?

Can you post or attach an example?

wildbill:
Can you post or attach an example?

I'm sorry. What are you looking for an example of? Which aspect? The error, the code, the structure?

The definition is:

typedef union
 {
  uint16_t au16data[15];
  struct
   {
    uint16_t ON_Setpoint;
    uint16_t OFF_Setpoint;
    uint16_t Control_Type; // absolute/differential
    uint16_t BrightLight_Setpoint;
    uint16_t Dark_Setpoint;

    uint16_t Panel_1_Supply_Temp;
    uint16_t Panel_1_Return_Temp;
    uint16_t Panel_1_Supply_Pres;
    uint16_t Panel_1_Return_Pres;
    uint16_t Panel_1_Flow_Rate;

    uint16_t Panel_2_Supply_Temp;
    uint16_t Panel_2_Return_Temp;
    uint16_t Panel_2_Supply_Pres;
    uint16_t Panel_2_Return_Pres;
    uint16_t Panel_2_Flow_Rate;
   };
 } modbus_registers_t;

modbus_registers_t modbus_register;

The error is

Pool_Heater_Panel_V4:673:37: error: 'union modbus_registers_t' has no member named 'Light_Level'

      Serial.println(modbus_register.Light_Level.West);

                                     ^

exit status 1
'union modbus_registers_t' has no member named 'Control_Type'

The line is

Serial.println(modbus_register.Light_Level.West);

adwsystems:
I'm sorry. What are you looking for an example of?

A complete code that can be copy / pasted into the Arduino IDE and product exactly the same error you're seeing.