@descartes
Some more context for you…
I have a working Azure IoT Central/Azure IoT Hubs HTTP Integration which TTN has tweeted about. It has been stress & soak tested with 1000’s of devices and 100,000’s of messages.
I have built an MQTT data API client which subscribes to uplink and publishes to downlink topics etc…
I have a client for the restful TTN Application Manager API and I have been looking at whether it is worth building a GRPC version.
Over the last couple of days I have done some more research and answered my own question.
I had a look at the Go code and learnt a lot as I figured out how the bit I was interested in worked. I haven’t done any Go coding so it took a while to get comfortable with the syntax.
In core/handler/cayennelpp/encoder.go there were
func (e *Encoder) Encode(fields map[string]interface{}, fPort uint8) (byte, bool, error) and func (d *Decoder) Decode(payload []byte, fPort uint8) (map[string]interface{}, bool, error)
Which was a positive sign.
Then in core/handler/convert_fields.go there are these two methods (I assume they are called methods in Go)
> // ConvertFieldsUp converts the payload to fields using the application's payload formatter
> func (h *handler) ConvertFieldsUp(ctx ttnlog.Interface, _ *pb_broker.DeduplicatedUplinkMessage, appUp *types.UplinkMessage, dev *device.Device) error {
> // Find Application
and
> // ConvertFieldsDown converts the fields into a payload
> func (h *handler) ConvertFieldsDown(ctx ttnlog.Interface, appDown *types.DownlinkMessage, ttnDown *pb_broker.DownlinkMessage, _ *device.Device) error {
Then further down in the second method is this call
var encoder PayloadEncoder
switch app.PayloadFormat {
case application.PayloadFormatCustom:
encoder = &CustomDownlinkFunctions{
Encoder: app.CustomEncoder,
Logger: functions.Ignore,
}
case application.PayloadFormatCayenneLPP:
encoder = &cayennelpp.Encoder{}
default:
return nil
}var encoder PayloadEncoder
switch app.PayloadFormat {
case application.PayloadFormatCustom:
encoder = &CustomDownlinkFunctions{
Encoder: app.CustomEncoder,
Logger: functions.Ignore,
}
case application.PayloadFormatCayenneLPP:
encoder = &cayennelpp.Encoder{}
default:
return nil
}
Which I think calls
// Encode encodes the fields to CayenneLPP
func (e *Encoder) Encode(fields map[string]interface{}, fPort uint8) ([]byte, bool, error) {
encoder := protocol.NewEncoder()
for name, value := range fields {
key, channel, err := parseName(name)
if err != nil {
continue
}
switch key {
case valueKey:
if val, ok := value.(float64); ok {
encoder.AddPort(channel, float32(val))
}
}
}
return encoder.Bytes(), true, nil
}
Then right down at the very bottom of the call stack in keys.go
func parseName(name string) (string, uint8, error) {
parts := strings.Split(name, "_")
if len(parts) < 2 {
return "", 0, errors.New("Invalid name")
}
key := strings.Join(parts[:len(parts)-1], "_")
if key == "" {
return "", 0, errors.New("Invalid key")
}
channel, err := strconv.Atoi(parts[len(parts)-1])
if err != nil {
return "", 0, err
}
if channel < 0 || channel > 255 {
return "", 0, errors.New("Invalid range")
}
return key, uint8(channel), nil
}
At this point I started to hit the limits of my Go skills but with some trial and error (well mainly error) I figured it out…
The field names need to be formatted like this (ignore the C# implementation details). On each alternate line are the bytes which arrive on the *duino LoRaWAN device.
Dictionary<string, object> payloadFields = new Dictionary<string, object>();
payloadFields.Add(“value_0”, 0.0);
//00-00-00
payloadFields.Add(“value_1”, 1.0);
//01-00-64
payloadFields.Add(“value_2”, 2.0);
//02-00-C8
payloadFields.Add(“value_3”, 3.0);
//03-01-2C
payloadFields.Add(“value_4”, 4.0);
//04-01-90
payloadFields.Add(“value_0”, -0.0);
//00-00-00
payloadFields.Add(“value_1”, -1.0);
//01-FF-9C
payloadFields.Add(“value_2”, -2.0);
//02-FF-38
payloadFields.Add(“value_3”, -3.0);
//03-FE-D4
payloadFields.Add(“value_4”, -4.0);
//04-FE-70
The downlink payload values are sent as 2 byte floats with a sign bit, 100 multiplier and can be decoded on an Arduino with code this
byte data[] = {0xff,0x38} ; // bytes which represent -2
float value = lpp.getValue( data, 2, 100, 1);
Serial.print("value:");
Serial.println(value);
So for readers who have the stamina to get to the end of this long post it is possible to use the baked in Cayenne Encoder/Decoder to send payload fields to a device…
@KiwiBryn