Skip to content

Commit

Permalink
LoRa fixes (eclipse#288)
Browse files Browse the repository at this point in the history
* Fixed duplicate datastructures defined for cayenne and added LoRa to the documentation.
* Implement ADR
* Add configuration for EU/US module
* Validate configuration ranges
  • Loading branch information
wegendt-bosch authored Mar 1, 2019
1 parent adc9ac0 commit 01be753
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,11 @@ class BaseUtils {
return zip(xs.iterator, ys.iterator);
}
}
}
}
def static <T> T castOrNull(Object o, Class<T> clazz) {
if(clazz.isInstance(o)) {
return clazz.cast(o);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.eclipse.mita.base.types.SumAlternative
import org.eclipse.mita.base.types.SumType
import org.eclipse.mita.base.util.BaseUtils
import org.eclipse.mita.program.ArrayLiteral
import org.eclipse.mita.program.ConfigurationItemValue
import org.eclipse.mita.program.ValueRange
import org.eclipse.mita.program.VariableDeclaration
import org.eclipse.mita.program.model.ModelUtils
Expand Down Expand Up @@ -165,6 +166,10 @@ class StaticValueInferrer {
return #[lower, upper];
}

static dispatch def Object infer(ConfigurationItemValue configItemValue, (EObject) => void inferenceBlockerAcceptor) {
configItemValue.value.infer(inferenceBlockerAcceptor);
}

static dispatch def Object infer(Void expression, (EObject) => void inferenceBlockerAcceptor) {
inferenceBlockerAcceptor.apply(null);
return null;
Expand Down
18 changes: 16 additions & 2 deletions platforms/org.eclipse.mita.platform.xdk110/1.0.0/xdk110.platform
Original file line number Diff line number Diff line change
Expand Up @@ -703,16 +703,30 @@ alt CayennePayload {
| GpsLocation: {Latitude: int32, Longitude: int32, Altitude: int32}
}

enum Region {
EU, US
}

connectivity named-singleton LoRa {
generator "org.eclipse.mita.platform.xdk110.connectivity.LoraGenerator"
validator "org.eclipse.mita.platform.xdk110.connectivity.LoraValidator"

required configuration-item region: Region
required configuration-item loraAppKey: array<uint8>
required configuration-item loraAppEui: array<uint8>

configuration-item loraDeviceEui: array<uint8>
configuration-item adaptiveDataRate: bool = true

configuration-item bandFrequency: uint16
configuration-item rx2Frequency: uint32
configuration-item rx2DataRate: uint8
configuration-item dataRate: uint8 = 3



signal raw(portNum: uint8 = 1, confirmation: LoRaMessageConfirmation = LoRaMessageConfirmation.Confirmed): array<uint8>
signal cayenne(portNum: uint8 = 1, confirmation: LoRaMessageConfirmation = LoRaMessageConfirmation.Confirmed): array<CayennePayload>
signal raw(portNum: uint8 = 1, confirmation: LoRaMessageConfirmation = LoRaMessageConfirmation.Unconfirmed): array<uint8>
signal cayenne(portNum: uint8 = 1, confirmation: LoRaMessageConfirmation = LoRaMessageConfirmation.Unconfirmed): array<CayennePayload>
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ class LoraGenerator extends AbstractSystemResourceGenerator {
Retcode_RaiseError(retcode);
}
}
«IF setup.signalInstances.exists[it.instanceOf.name == "cayenne"]»
// required data structures for cayenne
// translation CayennePayload_enum -> cayenne used buffer size in bytes
static const uint8_t payloadSizes[] = {
3, 3, 4, 4, 4, 3, 4, 3, 8, 4, 8, 11
};
// translation CayennePayload_enum -> CayenneLPPSerializer_DataType_T
static const CayenneLPPSerializer_DataType_T cayenneDataTypes[] = {
CAYENNE_LLP_SERIALIZER_DIGITAL_INPUT,
CAYENNE_LLP_SERIALIZER_DIGITAL_OUTPUT,
CAYENNE_LLP_SERIALIZER_ANALOG_INPUT,
CAYENNE_LLP_SERIALIZER_ANALOG_OUTPUT,
CAYENNE_LLP_SERIALIZER_ILLUMINANCE_SENSOR,
CAYENNE_LLP_SERIALIZER_PRESENCE_SENSOR,
CAYENNE_LLP_SERIALIZER_TEMPERATURE_SENSOR,
CAYENNE_LLP_SERIALIZER_HUMIDITY_SENSOR,
CAYENNE_LLP_SERIALIZER_ACCELEROMETER,
CAYENNE_LLP_SERIALIZER_BAROMETER,
CAYENNE_LLP_SERIALIZER_GYROMETER,
CAYENNE_LLP_SERIALIZER_GPS_LOCATION
};
«ENDIF»
'''
)
.addHeader("FreeRTOS.h", true, IncludePath.HIGH_PRIORITY)
Expand All @@ -109,17 +131,37 @@ class LoraGenerator extends AbstractSystemResourceGenerator {

}

protected val defaultValues = #{
"EU" -> #{
"bandFrequency" -> 868,
"rx2Frequency" -> 869525,
"rx2DataRate" -> 0,
"dataRate" -> 3
},
"US" -> #{
"bandFrequency" -> 915,
"rx2Frequency" -> 923300,
"rx2DataRate" -> 8,
"dataRate" -> 3
}
}

override generateEnable() {
val appEui = StaticValueInferrer.infer(configuration.getExpression("loraAppEui"), []);
val appKey = StaticValueInferrer.infer(configuration.getExpression("loraAppKey"), []);
val deviceEui = StaticValueInferrer.infer(configuration.getExpression("loraDeviceEui"), []);
val regionName = configuration.getEnumerator("region")?.name;
val bandFrequency = configuration.getInteger("bandFrequency") ?: defaultValues.get(regionName)?.get("bandFrequency");
val rx2Frequency = configuration.getInteger("rx2Frequency" ) ?: defaultValues.get(regionName)?.get("rx2Frequency" );
val rx2DataRate = configuration.getInteger("rx2DataRate" ) ?: defaultValues.get(regionName)?.get("rx2DataRate" );
val dataRate = configuration.getInteger("dataRate" ) ?: defaultValues.get(regionName)?.get("dataRate" );
if(appEui instanceof List) {
if(appKey instanceof List) {
return codeFragmentProvider.create('''
Retcode_T exception = RETCODE_OK;
exception = LoRaDevice_Init(LoRaCallbackFunc, 868);
exception = LoRaDevice_Init(LoRaCallbackFunc, «bandFrequency»);
«generateLoggingExceptionHandler("LoRa", "device init")»
exception = LoRaDevice_SetRxWindow2(0, 869525000);
exception = LoRaDevice_SetRxWindow2(«rx2DataRate», «rx2Frequency»000);
«generateLoggingExceptionHandler("LoRa", "set rx window")»
«IF deviceEui instanceof List»
exception = LoRaDevice_SetDevEUI(devEUI);
Expand Down Expand Up @@ -151,6 +193,9 @@ class LoraGenerator extends AbstractSystemResourceGenerator {
exception = LoRaDevice_SetRadioCodingRate(codingRate);
«generateLoggingExceptionHandler("LoRa", "set radio coding rate")»
exception = LoRaDevice_SetADR(«configuration.getExpression("adaptiveDataRate").code»);
«generateLoggingExceptionHandler("LoRa", "set adaptive data rate")»
exception = LoRaDevice_SaveConfig();
«generateLoggingExceptionHandler("LoRa", "save config")»
Expand Down Expand Up @@ -181,7 +226,7 @@ class LoraGenerator extends AbstractSystemResourceGenerator {
if (RETCODE_OK == exception)
{
// Set Data Rate to 3 (increase amount of data to send) and send the data via LoRa
exception = LoRa_SetDataRate(3);
exception = LoRa_SetDataRate(«dataRate»);
}
if (RETCODE_OK != exception)
{
Expand Down Expand Up @@ -343,34 +388,12 @@ class LoraGenerator extends AbstractSystemResourceGenerator {
break;
}
exception = CayenneLPPSerializer_SingleInstance(&cayenneLPPSerializerInput, &cayenneLPPSerializerOutput);
«generateLoggingExceptionHandler("Cayenne", "conversion")»
cayenneLPPSerializerOutput.BufferPointer += cayenneLPPSerializerOutput.BufferFilledLength;
}
return «sendName»(«portNum.code», dataBuffer, bufferSize);
''')
.addHeader("xdk110Types.h", false)
.addHeader("XDK_CayenneLPPSerializer.h", true)
.setPreamble('''
// translation CayennePayload_enum -> cayenne used buffer size in bytes
static const uint8_t payloadSizes[] = {
3, 3, 4, 4, 4, 3, 4, 3, 8, 4, 8, 11
};
// translation CayennePayload_enum -> CayenneLPPSerializer_DataType_T
static const CayenneLPPSerializer_DataType_T cayenneDataTypes[] = {
CAYENNE_LLP_SERIALIZER_DIGITAL_INPUT,
CAYENNE_LLP_SERIALIZER_DIGITAL_OUTPUT,
CAYENNE_LLP_SERIALIZER_ANALOG_INPUT,
CAYENNE_LLP_SERIALIZER_ANALOG_OUTPUT,
CAYENNE_LLP_SERIALIZER_ILLUMINANCE_SENSOR,
CAYENNE_LLP_SERIALIZER_PRESENCE_SENSOR,
CAYENNE_LLP_SERIALIZER_TEMPERATURE_SENSOR,
CAYENNE_LLP_SERIALIZER_HUMIDITY_SENSOR,
CAYENNE_LLP_SERIALIZER_ACCELEROMETER,
CAYENNE_LLP_SERIALIZER_BAROMETER,
CAYENNE_LLP_SERIALIZER_GYROMETER,
CAYENNE_LLP_SERIALIZER_GPS_LOCATION
};
''')
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,40 @@
package org.eclipse.mita.platform.xdk110.connectivity

import java.util.List
import java.util.Map
import org.eclipse.emf.ecore.EObject
import org.eclipse.mita.base.types.Enumerator
import org.eclipse.mita.program.Program
import org.eclipse.mita.program.SystemResourceSetup
import org.eclipse.mita.program.inferrer.StaticValueInferrer
import org.eclipse.mita.program.validation.IResourceValidator
import org.eclipse.xtext.validation.ValidationMessageAcceptor

import static extension org.eclipse.mita.base.util.BaseUtils.castOrNull
import java.util.function.IntPredicate
import java.util.function.Predicate

class LoraValidator implements IResourceValidator {
protected val rangeChecks = #{
"EU" -> #{
"bandFrequency" -> [int it | if(!(#[433, 868].contains(it))) {"one of 433, 868"}],
"rx2Frequency" -> [int it | if(!((433050 <= it && it <= 434790) || (863000 <= it && it <= 870000))) {"either between 433050 and 434790 or between 863000 and 870000"}],
"rx2DataRate" -> [int it | if(!(0 <= it && it <= 7)) {"between 0 and 7"}],
"dataRate" -> [int it | if(!(0 <= it && it <= 7)) {"between 0 and 7"}]
},
"US" -> #{
"bandFrequency" -> [int it | if(!(it == 915)) {"exactly 915"}],
"rx2Frequency" -> [int it | if(!(923300 <= it && it <= 927500)) {"between 923300 and 927500"}],
"rx2DataRate" -> [int it | if(!(8 <= it && it <= 13)) {"between 8 and 13"}],
"dataRate" -> [int it | if(!(0 <= it && it <= 4)) {"between 0 and 4"}]
}
}

protected val bandAndRx2Checks = #{
"EU" -> [int fBand, int fRx2 | ((fBand / 100) as int) == ((fRx2 / 100000) as int)],
"US" -> [a,b | true]
}

override validate(Program program, EObject context, ValidationMessageAcceptor acceptor) {
if(context instanceof SystemResourceSetup) {
val loraAppKey = context.configurationItemValues.findFirst[ it.item.name == "loraAppKey"];
Expand All @@ -39,7 +65,38 @@ class LoraValidator implements IResourceValidator {
acceptor.acceptError("Configured value must be an array", k_v.key.value, null, 0, "");
}
}

val region = StaticValueInferrer.infer(
context.configurationItemValues.findFirst[ it.item.name == "region"], []
).castOrNull(Enumerator)?.name;
val checks = rangeChecks.get(region);
if(checks !== null) {
checks.entrySet.forEach[name_check |
val name = name_check.key;
val check = name_check.value;

val configItemValue = context.configurationItemValues.findFirst[it.item.name == name];
val value = StaticValueInferrer.infer(configItemValue, []).castOrNull(Integer);
if(value !== null) {
val msg = check.apply(value);
if(msg !== null) {
acceptor.acceptError(value + " not in range for region " + region + ". Should be " + msg, configItemValue, null, 0, "");
}
}
]
}
val bandRx2Check = bandAndRx2Checks.get(region);
val fBandItem = context.configurationItemValues.findFirst[ it.item.name == "bandFrequency"];
val fRx2Item = context.configurationItemValues.findFirst[ it.item.name == "rx2Frequency"];
val fBand = StaticValueInferrer.infer(fBandItem, []).castOrNull(Integer);
val fRx2 = StaticValueInferrer.infer(fRx2Item, []).castOrNull(Integer);

if(bandRx2Check !== null && fBand !== null && fRx2 !== null && !bandRx2Check.apply(fBand, fRx2)) {
val msg = "bandFrequency and rx2Frequency don't fit together";
acceptor.acceptError(msg, fBandItem, null, 0, "");
acceptor.acceptError(msg, fRx2Item, null, 0, "");
}
}
}
}
}

86 changes: 84 additions & 2 deletions website/site/content/platforms/xdk110.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Sensors | Connectivities | Buses | IO
[Pressure]({{<ref "/platforms/xdk110.md#environment-bme280">}}) | [MQTT]({{<ref "/platforms/xdk110.md#mqtt">}})
[Temperature]({{<ref "/platforms/xdk110.md#environment-bme280">}}) | [Eclipse Hono over MQTT]({{<ref "/platforms/xdk110.md#eclipse-hono">}})
[Noise sensor]({{<ref "/platforms/xdk110.md#noise-sensor">}}) | [REST over HTTP]({{<ref "/platforms/xdk110.md#rest-over-http">}})
[Two buttons]({{<ref "/platforms/xdk110.md#buttons">}}) |
[Two buttons]({{<ref "/platforms/xdk110.md#buttons">}}) | [LoRa]({{<ref "/platforms/xdk110.md#lora"}})

For the gyroscope you can choose from three different variants:

Expand Down Expand Up @@ -518,14 +518,96 @@ every 100 milliseconds {
**Required** |`endpointBase: string` | The server URL base to which REST requests are made.
| `headerContent: string` | A custom header which is added to each HTTP request. Example: `"X-Auth: MySecretToken\nX-Version: 1.0"`.

#### Signal
#### Signals

Name | Description | Parameters |
--------------------------------|---------------------------------------|------------|------------
`resource: string` | A REST resource on the server. | `endpoint: string` | The REST path to the resource.
|| `writeMethod: HttpMethod` | Which method to use when writing. Default: `POST`
|| `readMethod: HttpMethod` | Which method to use when reading. Default: `GET`

### LoRa

LoRa is a low-power network for IoT. Since bandwidth is usually extremely limited you should only send small messages infrequently.
In this implementation there are two ways to send data: either raw bytes or [CayenneLPP](https://mydevices.com/cayenne/docs/lora/#lora-cayenne-low-power-payload) messages, a serializable predefined format.

Receiving messages is currently not supported.

### Example

```TypeScript
setup lora: LoRa {
region = EU;
loraAppKey = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
loraAppEui = [0,0,0,0,0,0,0,0];

var r = raw(0, Unconfirmed);
var c = cayenne(1, Confirmed);
}

every 1 hour {
let data = new array<CayennePayload>(1);
data[0] = CayennePayload.Accelerometer(
accelerometer.x_axis.read() as int16,
accelerometer.y_axis.read() as int16,
accelerometer.z_axis.read() as int16
);
lora.c.write(data);
}

every 10 minutes {
let data: array<uint8> = [0xCA, 0xFE];
lora.r.write(data);
}
```

#### Configuration

| Name | Description
---|---------------------------------|------------
**Required** |`region: Region` | Whether you are using the EU or US LoRa module.
**Required** |`loraAppKey: array<uint8>` | The key of your application in big-endian format (MSB first).
**Required** |`loraAppEui: array<uint8>` | The EUI of your application in big-endian format (MSB first).
| `loraDeviceEui: array<uint8>` | Optionally set the device's EUI. If you don't set this the predefined EUI of your LoRa board will be used.
| `adaptiveDataRate: bool` | Control whether adaptive data rate is turned on. Default: `true`.
| `bandFrequency: uint16` | Configure the band frequency according to your gateway **in MHz**. Valid values are for the EU module: 433 MHz and 868 MHz, and for the US module 915 MHz. Defaults: EU: 868, US: 915
| `rx2Frequency: uint32` | Configure the Rx2 frequency according to your gateway **in kHz**. Valid values are for the EU module: 433050 kHz to 434790 kHz and 863000 kHz to 870000 kHz, and for the US module: 923300 kHz to 927500 kHz. Defaults: EU: 869525 kHz, US: 923300 kHz
| `rx2DataRate: uint8` | Configure the Rx2 data rate according to your gateway. Valid values are for the EU module: 0 to 7, and for the US module: 8 to 13. Defaults: EU: 0, US: 8
| `dataRate: uint8` | Configure the data rate according to your gateway. Valid values are for the EU module: 0 to 7, and for the US module: 0 to 4. Default: 3


#### Signals

Name | Description | Parameters |
--------------------------------|---------------------------------------|------------|------------
`raw: array<uint8>` | Send data input as raw bytes. | `portNum: uint8` | Which port to use. Default: `1`
|| `confirmation: LoRaMessageConfirmation` | Whether to send confirmed messages. One of `Confirmed` and `Unconfirmed`. Default: `Unconfirmed`
`cayenne: array<CayennePayload>` | Send serialized CayenneLPP messages. | `portNum: uint8` | Which port to use. Default: `1`
|| `confirmation: LoRaMessageConfirmation` | Whether to send confirmed messages. One of `Confirmed` and `Unconfirmed`. Default: `Unconfirmed`

#### Cayenne Messages
The following CayenneLPP messages are supported via a [sum type]({{< ref "/language/types.md#sum-tymes" >}}):

```
alt CayennePayload {
DigitalInput: uint8
| DigitalOutput: uint8
| AnalogInput: int16
| AnalogOutput: int16
| IlluminanceSensor: uint16
| PresenceSensor: uint8
| TemperatureSensor: int16
| HumiditySensor: uint8
| Accelerometer: int16, int16, int16
| Barometer: uint16
| Gyrometer: int16, int16, int16
| GpsLocation: {Latitude: int32, Longitude: int32, Altitude: int32}
}
```

Constructing them works like in the example above or as described in the documentation of [sum types]({{< ref "/language/types.md#sum-tymes" >}}).
For example to create a GPS location message type `CayennePayload.GpsLocation(Latitude=lat, Altitude=alt, Longitude=lon)`.

## Buses

### GPIO
Expand Down

0 comments on commit 01be753

Please sign in to comment.