Weather Station command protocol

I just received an AAG RS485 Weather Station and I'd like some advice on communicating with it. It uses a binary command string with a three character header "+ws" followed by a length byte, RS485 node address, a mode and type byte, variable length data and "NOP" bytes and an 8 bit checksum. I wanted a function to handle the different length command strings. This is what I came up with for starters. Attached is a description of four typical commands. I am using 0x00 for a "NOP".

/* Routines to handle sending variable length commands to AAG RS485 Weather Station
 "Type" determines how many arguments are passed to the function that writes the bytes
 out to the interface. Using SoftwareSerial with an additional pin DD to control the
 RS485 chip. The checksum is calculated on the fly by summing the bytes into an 8 bit integer
 and ignoring the overflow.
 18 August 2011 EJ
 */

#include <stdarg.h>
#include <SoftwareSerial.h>

const int DD = 5; // data direction pin 5, xmit = HIGH, recv = LOW
const int RXpin = 6;
const int TXpin = 7;
SoftwareSerial RS485(RXpin, TXpin);
int xmit = HIGH;
int recv = LOW;

uint8_t head[3] = {
  0x2B, 0x77, 0x73}; // "+ws"

void sendCommand(uint8_t* Head, uint8_t Length, uint8_t Node, uint8_t Mode, uint8_t Type, ...){
  uint8_t chksum;
  va_list argp;
  int _count;
  int _mode;
  int _status;
  int _level;
  int _threshold;
  int _speed;
  int _direction;
  va_start(argp, Type);
  digitalWrite(DD, xmit); //set RS485 to transmit

    for (int i = 0; i < 3; i++){
    RS485.write(*Head++);
  }
  
  RS485.write(Length);

  RS485.write(Node);
  chksum = chksum + Node;

  RS485.write(Mode);
  chksum = chksum + Mode;

  RS485.write(Type);
  chksum = chksum + Type;


  switch(Type)
  {
  case 0xa1:
    _count = va_arg(argp, int);
    while (_count > 0) {
      RS485.write(0x00);
      _count--;  
    }
    break;

  case 0xa2:
    _mode = va_arg(argp, int);
    RS485.write(_mode);
    _status = va_arg(argp, int);
    RS485.write(_status);
    _level = va_arg(argp, int);
    RS485.write(_level);
    _threshold = va_arg(argp, int);
    RS485.write(_threshold);
    break;

  case 0xa3:
    _speed = va_arg(argp, int);
    RS485.write(_speed);
    while (_count > 0) {
      RS485.write(0x00);
      _count--;  
    }
    break;

  case 0xa4:
    _direction = va_arg(argp, int);
    RS485.write(_direction);  
    break;
  }
  va_end(argp);

  RS485.write(chksum);

  digitalWrite(DD, recv); //set RS485 to receive
}


void setup() {

  pinMode(DD,OUTPUT);
  digitalWrite(DD, recv);

  RS485.begin(9600);

// Example of three types of commands

  sendCommand(head, 7, 0, 0xeb, 0xa1,4); // read al the data
  sendCommand(head, 7, 0, 0xeb, 0xa2,0,0,0,0); // turn off the LEDS
  sendCommand(head, 6, 0, 0xeb, 0xa3, 100, 2); // set the speed cal to 100

}

void loop() {
}

Can anyone see any problems or suggest a better/different way?

Link to the command protocol: http://www.aag.com.mx/aagusa/contents/en-us/Description%20of%20WSV3%20Interface%20(485).pdf

wsv3 protocol.tiff (126 KB)

Before I look at the code I have a question about the above packet format. How can it show hardcoded values for the checksum when the data varies?

EDIT: One thing I noticed is that you don't appear to be adding the variable args to the checksum.


Rob

I knew someone would ask that. The screen capture you see is from my spreadsheet that calculates the checksum based on the data entered. Sorry, I should have been more clear.

OK, I just edited my first post.


Rob

Graynomad:
Before I look at the code I have a question about the above packet format. How can it show hardcoded values for the checksum when the data varies?

EDIT: One thing I noticed is that you don't appear to be adding the variable args to the checksum.


Rob

Ah, you're right! I did in my RS232 version that I printed to the screen and forgot to add to this version. I'll fix that.

Okay, I believe that takes care of that. Is there an easier/more elegant way to handle these variable length commands than the variable argument list that I am using? I only showed a few of the commands but they are all like those. I will have to have a case for each "Type".

/* Routines to handle sending variable length commands to AAG RS485 Weather Station
 "Type" determines how many arguments are passed to the function that writes the bytes
 out to the interface. Using SoftwareSerial with an additional pin DD to control the
 RS485 chip. The checksum is calculated on the fly by summing the bytes into an 8 bit integer
 and ignoring the overflow.
 18 August 2011 EJ
 */

#include <stdarg.h>
#include <SoftwareSerial.h>

const int DD = 5; // data direction pin 5, xmit = HIGH, recv = LOW
const int RXpin = 6;
const int TXpin = 7;
SoftwareSerial RS485(RXpin, TXpin);
int xmit = HIGH;
int recv = LOW;

uint8_t head[3] = {
  0x2B, 0x77, 0x73}; // "+ws"

void sendCommand(uint8_t* Head, uint8_t Length, uint8_t Node, uint8_t Mode, uint8_t Type, ...){
  uint8_t chksum;
  va_list argp;
  int _count;
  int _mode;
  int _status;
  int _level;
  int _threshold;
  int _speed;
  int _direction;
  va_start(argp, Type);
  digitalWrite(DD, xmit); //set RS485 to transmit

    for (int i = 0; i < 3; i++){
    RS485.write(*Head++);
  }

  RS485.write(Length);

  RS485.write(Node);
  chksum = chksum + Node;

  RS485.write(Mode);
  chksum = chksum + Mode;

  RS485.write(Type);
  chksum = chksum + Type;


  switch(Type)
  {
  case 0xa1:
    _count = va_arg(argp, int);
    while (_count > 0) {
      RS485.write(0x00);
      _count--;  
    }
    break;

  case 0xa2:
    _mode = va_arg(argp, int);
    RS485.write(_mode);
    chksum = chksum + _mode;
    _status = va_arg(argp, int);
    RS485.write(_status);
    chksum = chksum + _status;
    _level = va_arg(argp, int);
    RS485.write(_level);
    chksum = chksum + _level;
    _threshold = va_arg(argp, int);
    RS485.write(_threshold);
    chksum = chksum + _threshold;
    break;

  case 0xa3:
    _speed = va_arg(argp, int);
    RS485.write(_speed);
    chksum = chksum + _speed;
    while (_count > 0) {
      RS485.write(0x00);
      _count--;  
    }
    break;

  case 0xa4:
    _direction = va_arg(argp, int);
    RS485.write(_direction); 
    chksum = chksum + _direction;   
    break;
  }
  va_end(argp);

  RS485.write(chksum);

  digitalWrite(DD, recv); //set RS485 to receive
}


void setup() {

  pinMode(DD,OUTPUT);
  digitalWrite(DD, recv);

  RS485.begin(9600);

  sendCommand(head, 7, 0, 0xeb, 0xa1,4); // read all the data
  sendCommand(head, 7, 0, 0xeb, 0xa2,0,0,0,0); // turn off the LEDS
  sendCommand(head, 6, 0, 0xeb, 0xa3, 100, 2); // set the speed cal to 100


}

void loop() {

}

You don't need all those local variables in sendCommand().

  case 0xa4:
    _direction = va_arg(argp, int);
    RS485.write(_direction);  
    break;

can be

  case 0xa4:
   RS485.write(va_arg(argp, int));  
    break;

Also

 case 0xa3:
    _speed = va_arg(argp, int);
    RS485.write(_speed);
    while (_count > 0) {
      RS485.write(0x00);
      _count--;  
    }
    break;

count is not initialised.


Rob

Is there an easier/more elegant way to handle these variable length commands

I'd be inclined to make them NOT variable length, eg

  sendCommand(head, 7, 0, 0xeb, 0xa1,4,0,0,0); // read all the data
  sendCommand(head, 7, 0, 0xeb, 0xa2,0,0,0,0); // turn off the LEDS
  sendCommand(head, 6, 0, 0xeb, 0xa3, 100, 2,0,0); // set the speed cal to 100

Rob

You don't need all those local variables in sendCommand().

Okay. I had never used va_list and was doing it step-wise in case I had to troubleshoot. I'll change that in the final version.

count is not initialised.

Oops, I passed it as an argument in another case, I'll either do that here or hard code it.

I'd be inclined to make them NOT variable length, eg

Okay, I'm not following you there. I can't just pad them all to the same length, the device expects a certain size packet for each command type. I wanted to handle them all with one function.

You don't need all those local variables in sendCommand().

I thought every time I call va_arg(), it increments its pointer. Don't I still need the local variables?

Yes, but if va_arg can return a value into a variable is can also return it as the argument of a function call, no need to temp store it in a variable.

RS485.write(va_arg(argp, int));

Unless there's something funky about va_arg I've forgotten about (which is possible, I haven't used it for about 15 years :))


Rob

Graynomad:
Yes, but if va_arg can return a value into a variable is can also return it as the argument of a function call, no need to temp store it in a variable.

RS485.write(va_arg(argp, int));

Unless there's something funky about va_arg I've forgotten about (which is possible, I haven't used it for about 15 years :))


Rob

I need to use it to calculate chksum as well is the reason I thought I needed a local variable. If I use the function again, I thought it would return the next argument instead of the one I need.

Now that I have done it this way, it's probably more code efficient to just build constant arrays for the commands that don't have variable data and use this technique to handle the ones that do have variable data.

The documentation is so poorly translated that I didn't really have a good understanding of the whole picture until I went through and organized it all in a spread sheet. At least now I have gained the knowledge of how to code functions with variable length argument lists. :slight_smile:

You've been great help so far. Thank you.

Posts crossed.

Sorry, I though they were all the same length. No matter though because you have the packet length so you can either fill an array then have sendCommand() read from the array. Or always have the max number of parms to sendCommand() but only send what you need. I'm inclined to the first approach, something like this

#define PCK_OFFSET_LENGTH	3
#define PCK_OFFSET_NODE 	4
#define PCK_OFFSET_TYPE 	5
#define PCK_OFFSET_D0		6
#define PCK_OFFSET_D1		7
#define PCK_OFFSET_D2		8
#define PCK_OFFSET_D3		9

#define PCK_TYPE_READ		0xa1
#define PCK_TYPE_LEDOFF		0xa2
#define PCK_TYPE_CALSPEED	0xa3


uint8_t default_packet [] = {'+', 'w', 's', 0, 0, 0xEB, 0, 0, 0, 0};   
uint8_t packet [sizeof(default_packet)];   
  
void setup() {

  pinMode(DD,OUTPUT);
  digitalWrite(DD, recv);

  RS485.begin(9600);

  initPacket (4, PCK_TYPE_READ);
  sendCommand();
  
  initPacket (7, PCK_TYPE_LEDOFF);
  sendCommand(); 
  
  initPacket (6, PCK_TYPE_CALSPEED);
  packet[PCK_OFFSET_D0] = 100;
  packet[PCK_OFFSET_D1] = 2;
  sendCommand(); 
 
}

void initPacket (uint8_t length, uint8_t type) { 
	memcpy (packet, default_packet, sizeof(default_packet));
	packet[PCK_OFFSET_LENGTH] = length;
	packet[PCK_OFFSET_TYPE] = type;
 }

void sendCommand () {
	uint8_t chksum = 0;

	for (int i = 0; i < sizeof(default_packet); i++) {
	    RS485.write(packet[i]);
		chksum += packet[i];
	}
	RS485.write(chksum);
}

Rob

No matter though because you have the packet length so you can either fill an array then have sendCommand() read from the array. Or always have the max number of parms to sendCommand() but only send what you need. I'm inclined to the first approach, something like this

I'll give that a try. Right now I see a jalapeño cheeseburger in my future. :smiley: Thanks again!