MODBUS RTU-RS485 ISSUE Temperature and Humidity, cannot read both at same time - sensors

I have a Temperature and Humidity Sensor (R444A01) connected to a LogicMachine (LM5LP2) via MODBUS RTU (RS485 port).
Sensor R444A01 Datasheet (Please bear in mind we are talking about a non-expensive device, poorly documented and with no support, aside from some User Reviews and Vendor Specifications)
This is my (very simple) code:
{
"manufacturer": "Embedded Systems",
"description": "Sensor R444A01",
"mapping": [
{
"name": "Temperature",
"bus_datatype": "float16",
"datatype": "int16",
"type": "register",
"address": 0,
"value_multiplier": 0.1,
"units": "C"
},
{
"name": "Humidity",
"bus_datatype": "float16",
"datatype": "uint16",
"type": "register",
"address": 1,
"value_multiplier": 0.1,
"units": "%"
}
]
}
The issue that I'm facing is that, when mapping these 2 addresses to a KNX address, if I only map 1 address, then I can read it, but if I map both of them, then I can only read "Temperature" (which happens to be the first address in my code). Here's a picture of what I see:
KNX address mapping for MODBUS addresses
Apparently, for Humidity value, the LogicMachine reads the lowest possible number an int16 can give (32768), even though the received data should be uint16 datatype as it's Humidity (a percentage value) that we are talking about.
Finally, here is what the Modbus Poll Log says (suggesting that I should be able to read both the Temperature and Humidity values):
[18/05/2021 21:51:32]
Modbus Request (COM10)
Address: 1
Function: 3 (0x03) - Read Holding Registers
Starting Address: 0
Quantity: 2
Checksum: 50187(OK)
[18/05/2021 21:51:32]
Modbus Response (COM10)
Address: 1
Function: 3 (0x03) - Read Holding Registers
Byte Count: 4
Values: 00 f3 01 ea
Register0: 243
Register1: 490
Checksum: 35359(OK)
Don't know if anybody has any idea why this could be happening, but I appreciate any responses.
Thank you very much.

As #Marcos G. pointed out in the question's comments, it turns out that the only way to succesfully ask the Sensor R444A01 about the values of multiple registers is to read these registers on a single query, instead of 1 query per 1 register.
Therefore, I needed to make use of the following keys: "read_count" and "read_offset".
Here is the correct code in order to read both Temperature and Humidity values on a single query:
{
  "manufacturer": "Embedded Systems",
  "description": "Sensor R444A01",
  "mapping": [
    {
      "name": "Temperature",
      "bus_datatype": "float16",
      "type": "register",
      "address": 0,
      "read_offset": 0,
      "read_count": 2,
      "value_multiplier": 0.1,
      "units": "ºC"
    },
    {
      "name": "Humidity",
      "bus_datatype": "float16",
      "type": "register",
      "address": 0,
      "read_offset": 1,
      "read_count": 2,
      "value_multiplier": 0.1,
      "units": "%"
    }
  ]
}

Related

How to mock Athena query results values with Moto3 for a specific table?

I am using pytest and moto3 to test some code similar to this:
response = athena_client.start_query_execution(
QueryString='SELECT * FROM xyz',
QueryExecutionContext={'Database': myDb},
ResultConfiguration={'OutputLocation': someLocation},
WorkGroup=myWG
)
execution_id = response['QueryExecutionId']
if response['QueryExecution']['Status']['State'] == 'SUCCEEDED':
response = athena_client.get_query_results(
QueryExecutionId=execution_id
)
results = response['ResultSet']['Rows']
...etc
In my test I need that the values from results = response['ResultSet']['Rows'] are controlled by the test. I am using some code like this:
backend = athena_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
rows = [{"Data": [{"VarCharValue": "xyz"}]}, {"Data": [{"VarCharValue": ...}, etc]}]
column_info = [
{
"CatalogName": "string",
"SchemaName": "string",
"TableName": "xyz",
"Name": "string",
"Label": "string",
"Type": "string",
"Precision": 123,
"Scale": 123,
"Nullable": "NOT_NULL",
"CaseSensitive": True,
}
]
results = QueryResults(rows=rows, column_info=column_info)
backend.query_results[NEEDED_QUERY_EXECUTION_ID] = results
but that is not working as I guess NEEDED_QUERY_EXECUTION_ID is not known before from the test. How can I control it?
UPDATE
Based on suggestion I tried to use:
results = QueryResults(rows=rows, column_info=column_info)
d = defaultdict(lambda: results.to_dict())
backend.query_results = d
to force a return of values, but it seems not working as from the moto3's models.AthenaBackend.get_query_results, I have this code:
results = (
self.query_results[exec_id]
if exec_id in self.query_results
else QueryResults(rows=[], column_info=[])
)
return results
which will fail as the if condition won't be satifsfied.
Extending the solution of the defaultdict, you could create a custom dictionary that contains all execution_ids, and always returns the same object:
class QueryDict(dict):
def __contains__(self, item):
return True
def __getitem__(self, item):
rows = [{"Data": [{"VarCharValue": "xyz"}]}, {"Data": [{"VarCharValue": "..."}]}]
column_info = [
{
"CatalogName": "string",
"SchemaName": "string",
"TableName": "xyz",
"Name": "string",
"Label": "string",
"Type": "string",
"Precision": 123,
"Scale": 123,
"Nullable": "NOT_NULL",
"CaseSensitive": True,
}
]
return QueryResults(rows=rows, column_info=column_info)
backend = athena_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
backend.query_results = QueryDict()
An alternative solution to using custom dictionaries would to be seed Moto.
Seeding Moto ensures that it will always generate the same 'random' identifiers, which means you always know what the value of NEEDED_QUERY_EXECUTION_ID is going to be.
backend = athena_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
rows = [{"Data": [{"VarCharValue": "xyz"}]}, {"Data": [{"VarCharValue": "..."}]}]
column_info = [...]
results = QueryResults(rows=rows, column_info=column_info)
backend.query_results["bdd640fb-0667-4ad1-9c80-317fa3b1799d"] = results
import requests
requests.post("http://motoapi.amazonaws.com/moto-api/seed?a=42")
# Test - the execution id will always be the same because we just seeded Moto
execution_id = athena_client.start_query_execution(...)
Documentation on seeding Moto can be found here: http://docs.getmoto.org/en/latest/docs/configuration/recorder/index.html#deterministic-identifiers
(It only talks about seeding Moto in the context of recording/replaying requests, but the functionality can be used on it's own.)

Cosmos DB high RU

I do have simple JSON data with an array, when I query array contains or join, the Request Unit is too high.
select value count(0)
From c
where ARRAY_CONTAINS(c.signerDetails, {'displayStatus':0}, true)
and c.orgId = "e27dd002-bad3-4444-aa2b-855e5d4e79c8"
For the above ARRAY_CONTAINS query, we get 1882 RU for 40518 Count
select count(0)
From c WHERE c.orgId = "e27dd002-bad3-4444-aa2b-855e5d4e79c8"
and EXISTS(SELECT VALUE t FROM t IN c.signerDetails WHERE t.displayStatus = 0)
For the above exists query, we do get 1960 Ru for 40518 count
{
"id": "1b675bb2-f783-48a0-93bb-967a990b5204f901795b-d18d-4c2d-a68c-61adf7e8e8da",
"orgId": "e27dd002-bad3-4444-aa2b-855e5d4e79c8", //Partition key
"userDetails": [
{
"userName": "",
"userEmail": "",
"displayStatus": 4,
},
{
"userName": "",
"userEmail": "",
"displayStatus": 1,
}
],
"status": "Completed",
"_ts": 1619535494
}
Please share your thoughts on reducing the RU, or do I need to JSON data format or do I need to consider any other database for this kind of operation.
PS: I do have complex array querying, on the above sample I have removed the same for the code brevity.

Lambda behaves differently when invoked from Console (test) and through AWS Gateway REST API

Problem: Pandas behaves differently when
I call it from the "invoke" test (and via AWS Gateway which is pointed to the said Lambda)
I call it through a REST API via AWS Gateway
Lambda works fine with A) (it returns intended values) but does not work with B) (returns NaN values).
Details: I have a simple python script in a Lambda (python 3.8). With the code, I have uploaded pandas, numpy and a .csv file into the Lambda. I built a rest API with AWS Gateway that uses the Lambda as a resource.
My objective is to load the .csv into a Pandas dataframe when Lambda is requested, then filter the dataframe with the event parameters to find particular values, and return those values.
Sample code below.
Any help would be appreciated.
Code
Lambda:
def lambda_handler(event, context):
# extract filter values from request
vendor = event['Vendor']
# load the csv
data = pd.read_csv('vendor_data.csv')
print("Data shape", data.shape)
# correct types
data.quantity = (data.quantity).apply(lambda x: int(x))
data.loc[:,'Vendor'] = data['Vendor'].astype('str') # I also tried astype('string'), doesn't change
# filter the dataframe with Vendor == vendor
condition = (data.Vendor.str.contains(str(vendor))) #I also tried (data.Vendor == vendor)
data = data.loc[condition]
#verify it worked: it works with 'invoke' but not through rest API
print('First five rows', data[['Vendor']].head())
data = data['quantity'].apply(lambda x: int(x))
sum = data.sum()
return sum
Gateway Rest API
Request model template:
{ "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Request",
"type": "object",
"properties": {
"Vendor": {
"type": "string"
},
},
"required": ["Vendor"]
}
Response model template:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Response",
"type": "object",
"properties": {
"sum": {
"type": "number"
}
}
}
Example test (post to rest API)
PARAMS = {
"Vendor": "2202630"
}
PARAMS = json.dumps(PARAMS)
print(PARAMS)
r = requests.post(url = URL, data = PARAMS)
print(r.content)
results in:
{ "Vendor": "2202630"}
b'{"statusCode": 200, "body": NaN, "event": {"Vendor": "2202630"}, "data": "[]"}'
Example test (through AWS Gateway):
Request:
{
"Vendor": "2202630"
}
Response body:
{
"statusCode": 200,
"body": 599,
"event": {
"Vendor": "22026305"
},
"data": "[50 44 38 45 50 45 38 26 18 31 21 9 53 47 45 39]"
}

Transforms to ArcGIS API geojson output and geopandas series/dataframe

The following GET yields a result you can view via Postman and/or geojson.io (NOTE: you need cookies, which are pasted at the bottom of this post):
url_geojson = "https://services6.arcgis.com/GklOjOaok2jR6aKf/ArcGIS/rest/services/NM_OG_ROWs_Linear_031417/FeatureServer/0/query?f=geojson&where=1%3D1&returnGeometry=true&spatialRel=esriSpatialRelIntersects&outFields=*&orderByFields=FID%20ASC&outSR=102100&resultOffset=0&resultRecordCount=4000&cacheHint=true&quantizationParameters=%7B%22mode%22%3A%22edit%22%7D"
In the following GET call, another GIS API call yields just a json response with additional info in regards to the transformations that need to be applied to the geometry objects generated from the geojson call:
url_json = "https://services6.arcgis.com/GklOjOaok2jR6aKf/ArcGIS/rest/services/NM_OG_ROWs_Linear_031417/FeatureServer/0/query?f=json&where=1%3D1&returnGeometry=true&spatialRel=esriSpatialRelIntersects&outFields=*&orderByFields=FID%20ASC&outSR=102100&resultOffset=0&resultRecordCount=4000&cacheHint=true&quantizationParameters=%7B%22mode%22%3A%22edit%22%7D"
Note that only difference is the f parameter (json and geojson).
The output of the json request has a section with the following output:
{
"objectIdFieldName": "FID",
"uniqueIdField": {
"name": "FID",
"isSystemMaintained": true
},
"globalIdFieldName": "",
"geometryProperties": {
"shapeLengthFieldName": "Shape__Length",
"units": "esriMeters"
},
"geometryType": "esriGeometryPolyline",
"spatialReference": {
"wkid": 102100,
"latestWkid": 3857
},
"transform": {
"originPosition": "upperLeft",
"scale": [
0.0001,
0.0001,
0,
0
],
"translate": [
-20037700,
-30241100,
0,
0
]
},...
I assume these are the parameters that I need to use to change the output coordinates of the geojson request, which are (as a single example) this:
{
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": "EPSG:3857"
}
},
"properties": {
"exceededTransferLimit": true
},
"features": [
{
"type": "Feature",
"id": 1,
"geometry": {
"type": "LineString",
"coordinates": [
[
-11533842.1198518,
3857288.84408179
],
[
-11534147.0371623,
3857067.64072161
]
]
},...
I have managed to take the geojson output and assign to a pandas dataframe and apply the scaling transform with the following command:
main_gdf_clean['geometry'] = GeoSeries.scale(main_gdf_clean['geometry'],
xfact=0.00001, yfact=0.00001,
origin=(0,0))
At this point, I'm lost on how to apply - as listed above in the json output - the translation parameters. I've tried the following command, and it yields wildly incorrect result to geometry objects:
GeoSeries.translate(main_gdf_clean['geometry'], xoff=-20037700.0, yoff=-30241100.0)
Based on what I've presented here, can someone suggest a way to apply the translation transform properly or go about this process in a different way?
header = {'Cookie': 'ASP.NET_SessionId=umzghudgzvz22wpo3a0bgeoq; OCDUserPreference=AAEAAAD/////AQAAAAAAAAAMAgAAAEtFTU5SRC5PQ0QuUGVybWl0dGluZywgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPW51bGwFAQAAACVOTUVNTlJELk9DRC5QZXJtaXR0aW5nLlVzZXJQcmVmZXJlbmNlCQAAAAhQYWdlU2l6ZRJXZWxsU29ydFBhcmFtZXRlcnMWRmFjaWxpdHlTb3J0UGFyYW1ldGVycxxGZWVBcHBsaWNhdGlvblNvcnRQYXJhbWV0ZXJzFkluY2lkZW50U29ydFBhcmFtZXRlcnMRUGl0U29ydFBhcmFtZXRlcnMSVGFua1NvcnRQYXJhbWV0ZXJzGkdlbmVyYXRlZEFwaVNvcnRQYXJhbWV0ZXJzFk9wZXJhdG9yU29ydFBhcmFtZXRlcnMABAQEBAQEBAQIKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAKE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlckxpc3QCAAAAAgAAAGQAAAAJAwAAAAkEAAAACQUAAAAJBgAAAAkHAAAACQgAAAAJCQAAAAkKAAAABQMAAAAoTk1FTU5SRC5PQ0QuUGVybWl0dGluZy5Tb3J0UGFyYW1ldGVyTGlzdAMAAAANTGlzdGAxK19pdGVtcwxMaXN0YDErX3NpemUPTGlzdGAxK192ZXJzaW9uBAAAJk5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlcltdAgAAAAgIAgAAAAkLAAAAAgAAAAIAAAABBAAAAAMAAAAJDAAAAAIAAAACAAAAAQUAAAADAAAACQ0AAAABAAAAAQAAAAEGAAAAAwAAAAkOAAAAAgAAAAIAAAABBwAAAAMAAAAJDwAAAAIAAAACAAAAAQgAAAADAAAACRAAAAACAAAAAgAAAAEJAAAAAwAAAAkRAAAAAwAAAAMAAAABCgAAAAMAAAAJEgAAAAEAAAABAAAABwsAAAAAAQAAAAQAAAAEJE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlcgIAAAAJEwAAAAkUAAAADQIHDAAAAAABAAAABAAAAAQkTk1FTU5SRC5PQ0QuUGVybWl0dGluZy5Tb3J0UGFyYW1ldGVyAgAAAAkVAAAACRYAAAANAgcNAAAAAAEAAAAEAAAABCROTUVNTlJELk9DRC5QZXJtaXR0aW5nLlNvcnRQYXJhbWV0ZXICAAAACRcAAAANAwcOAAAAAAEAAAAEAAAABCROTUVNTlJELk9DRC5QZXJtaXR0aW5nLlNvcnRQYXJhbWV0ZXICAAAACRgAAAAJGQAAAA0CBw8AAAAAAQAAAAQAAAAEJE5NRU1OUkQuT0NELlBlcm1pdHRpbmcuU29ydFBhcmFtZXRlcgIAAAAJGgAAAAkbAAAADQIHEAAAAAABAAAABAAAAAQkTk1FTU5SRC5PQ0QuUGVybWl0dGluZy5Tb3J0UGFyYW1ldGVyAgAAAAkcAAAACR0AAAANAgcRAAAAAAEAAAAEAAAABCROTUVNTlJELk9DRC5QZXJtaXR0aW5nLlNvcnRQYXJhbWV0ZXICAAAACR4AAAAJHwAAAAkgAAAACgcSAAAAAAEAAAAEAAAABCROTUVNTlJELk9DRC5QZXJtaXR0aW5nLlNvcnRQYXJhbWV0ZXICAAAACSEAAAANAwwiAAAATVN5c3RlbS5XZWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iMDNmNWY3ZjExZDUwYTNhBRMAAAAkTk1FTU5SRC5PQ0QuUGVybWl0dGluZy5Tb3J0UGFyYW1ldGVyAwAAAAVfbmFtZQtfZXhwcmVzc2lvbgpfZGlyZWN0aW9uAQEEJ1N5c3RlbS5XZWIuVUkuV2ViQ29udHJvbHMuU29ydERpcmVjdGlvbiIAAAACAAAABiMAAAANV2VsbCBPcGVyYXRvcgYkAAAACm9ncmlkX25hbWUF2////ydTeXN0ZW0uV2ViLlVJLldlYkNvbnRyb2xzLlNvcnREaXJlY3Rpb24BAAAAB3ZhbHVlX18ACCIAAAAAAAAAARQAAAATAAAABiYAAAAJV2VsbCBOYW1lBicAAAAId2VsbG5hbWUB2P///9v///8AAAAAARUAAAATAAAABikAAAAFT2dyaWQGKgAAAAVvZ3JpZAHV////2////wAAAAABFgAAABMAAAAGLAAAAAtGYWNpbGl0eSBJZAYtAAAAAmlkAdL////b////AQAAAAEXAAAAEwAAAAYvAAAACkNyZWF0ZWQgT24GMAAAAAljcmVhdGVkT24Bz////9v///8AAAAAARgAAAATAAAACSkAAAAJKgAAAAHM////2////wAAAAABGQAAABMAAAAGNQAAAAtJbmNpZGVudCBJZAktAAAAAcn////b////AQAAAAEaAAAAEwAAAAkpAAAACSoAAAABxv///9v///8AAAAAARsAAAATAAAABjsAAAAGUGl0IElkBjwAAAAGcGl0X2lkAcP////b////AQAAAAEcAAAAEwAAAAkpAAAACSoAAAABwP///9v///8AAAAAAR0AAAATAAAABkEAAAAHVGFuayBJZAZCAAAAB3RhbmtfaWQBvf///9v///8BAAAAAR4AAAATAAAACSMAAAAJJAAAAAG6////2////wAAAAABHwAAABMAAAAJJgAAAAZIAAAADXByb3BlcnR5X25hbWUBt////9v///8AAAAAASAAAAATAAAABkoAAAALV2VsbCBOdW1iZXIGSwAAAAt3ZWxsX251bWJlcgG0////2////wAAAAABIQAAABMAAAAGTQAAAA1PcGVyYXRvciBOYW1lCSQAAAABsf///9v///8AAAAACw==',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}

Difference among the Azure CosmosDB Query Explorer's results and the Azure Functions results

I executed a following query on the CosmosDB Query Explorer.
SELECT c.id, c.created_at FROM c
WHERE c.created_at_epoch <= 1499871600 - 86400*31
AND (CEILING(1499871600/86400) - CEILING(c.created_at_epoch / 86400)) % 31 = 0
That's result is followings.
[
{
"id": "70251cbf-44b3-4cd9-991f-81127ad78bca",
"created_at": "2017-05-11 18:46:16"
},
{
"id": "0fa31de2-4832-49ea-a0c6-b517d64ede85",
"created_at": "2017-05-11 18:48:22"
},
{
"id": "b9959d15-92e7-41c3-8eff-718c4ab2be6e",
"created_at": "2017-05-11 19:01:43"
}
]
It looks that problem does not exist.
Next, I replaced epoch values that defined statically to placeholders for using it as sqlQuery of the Azure Functions DocumentDB input bindings. Then, I replaced a modulations symbol to %modulationsymbol% for avoiding this issue
SELECT c.id, c.created_at FROM c
WHERE c.created_at_epoch <= {epoch} - 86400*31
AND (CEILING({epoch}/86400) - CEILING(c.created_at_epoch / 86400)) %modulationsymbol% 31 = 0
And I defined modulationsymbol = % as an application setting.
Then, I specified the function as follows.
// index.js
module.exports = function (context, myQueueItem) {
context.log(context.bindings.members, myQueueItem);
context.done();
};
// function.json
{
"bindings": [
{
"name": "myQueueItem",
"type": "queueTrigger",
"direction": "in",
"queueName": "myqueue",
"connection": "MYSTORAGE"
},
{
"type": "documentDB",
"name": "members",
"databaseName": "myproject-db",
"collectionName": "member",
"sqlQuery": "SELECT c.id, c.created_at FROM c WHERE {epoch} - c.created_at_epoch >= 86400*31 AND (CEILING({epoch}/86400) - CEILING(c.created_at_epoch / 86400)) %modulationsymbol% 31 = 0",
"connection": "MYCOSMOSDB",
"direction": "in"
}
],
"disabled": true
}
Afterward, I triggered the function and this result is followings.
2017-07-05T03:57:29.640 Function started (Id=d980521e-d23a-4bda-a730-57a236bcd011)
2017-07-05T03:57:30.594 [] { epoch: 1499871600 }
2017-07-05T03:57:30.594 Function completed (Success, Id=d980521e-d23a-4bda-a730-57a236bcd011, Duration=951ms)
It looks context.bindings.members is a empty list. It differenced from the CosmosDB Query Explorer's result.
Why appeared this differences?
After print out the {epoch}, I found its type is string and the expected type is number instead of string. That is the reason of getting empty list when use the same query.
To fix this issue, you could convert the type to number before using it to filter your query result. Steps below for your reference.
Step 1, Create a UDF which could convert string to number. Script Explorer->Create User Defined Function
function toNumber(ts) {
return parseInt(ts);
}
Step 2, After created the ConvertToNumber function, you could use it to convert the type of {epoch} to number.
SELECT c.id, c.created_at FROM c
WHERE c.created_at_epoch <= udf.ConvertToNumber({epoch}) - 86400*31
AND (CEILING(udf.ConvertToNumber({epoch})/86400) - CEILING(c.created_at_epoch / 86400)) %modulationsymbol% 31 = 0
If you are familiar with C#, you could create the function with C#. Since C# is a strongly typed language. We can define a class which is used to deserialize the message from queue. It will convert the type in language layer.
public class EpochMessage
{
public int epoch { get; set; }
}
The whole function could be like this.
using System;
public static void Run(EpochMessage myQueueItem, TraceWriter log, IEnumerable<dynamic> members)
{
log.Info(context.bindings.members, myQueueItem);
}
public class EpochMessage
{
public int epoch { get; set; }
}

Resources