2. Multiple Sensors - Temperature, Humidity, Ambiance (Light)
Key Concepts
Multiple Sensors Hardware Setup
We can attach multiple sensors (Humidity, Ambiance, Temperature) to Arduino Uno using a Sensor shield.
#include <math.h> // Temperature Analog Sensor int sensorPinT = A0; // select the input pin for the potentiometer int sensorPinL = A4; // select the input pin for the potentiometer double Thermistor(int RawADC) { double Temp; Temp = log(10000.0*((1024.0/RawADC-1))); Temp = 1 / (0.001129148 + (0.000234125 + (0.0000000876741 * Temp * Temp ))* Temp ); Temp = Temp - 273.15; // Convert Kelvin to Celcius //Temp = (Temp * 9.0)/ 5.0 + 32.0; // Convert Celcius to Fahrenheit return Temp; } String getTemperature() { String tempStr = "TEMPC:"; int readVal=analogRead(sensorPinT); double tempC = Thermistor(readVal); // now convert to Fahrenheit double tempF = (tempC * 9.0 / 5.0) + 32.0; tempStr = "TEMPC:" + String(tempC) + "#TEMPF:" + String(tempF); return tempStr; } //KY015 DHT11 Temperature and humidity sensor int DHpin = 8; byte dat [5]; byte read_data () { byte data; for (int i = 0; i < 8; i ++) { if (digitalRead (DHpin) == LOW) { while (digitalRead (DHpin) == LOW); // wait for 50us delayMicroseconds (30); // determine the duration of the high level to determine the data is '0 'or '1' if (digitalRead (DHpin) == HIGH) data |= (1 << (7-i)); // high front and low in the post while (digitalRead (DHpin) == HIGH); // data '1 ', wait for the next one receiver } } return data; } void start_test () { digitalWrite (DHpin, LOW); // bus down, send start signal delay (30); // delay greater than 18ms, so DHT11 start signal can be detected digitalWrite (DHpin, HIGH); delayMicroseconds (40); // Wait for DHT11 response pinMode (DHpin, INPUT); while (digitalRead (DHpin) == HIGH); delayMicroseconds (80); // DHT11 response, pulled the bus 80us if (digitalRead (DHpin) == LOW); delayMicroseconds (80); // DHT11 80us after the bus pulled to start sending data for (int i = 0; i < 4; i ++) // receive temperature and humidity data, the parity bit is not considered dat[i] = read_data (); pinMode (DHpin, OUTPUT); digitalWrite (DHpin, HIGH); // send data once after releasing the bus, wait for the host to open the next Start signal } String getHumidity () { start_test (); String hum = ""; hum = "HUMPCT:" + String(dat [0], DEC) + "." + String(dat [1], DEC); return hum; } int ledPin = 13; // select the pin for the LED String getLight() { String amb = ""; int sensorValue = 0; // variable to store the value coming from the sensor sensorValue = analogRead(sensorPinL); digitalWrite(ledPin, HIGH); delay(sensorValue); digitalWrite(ledPin, LOW); delay(sensorValue); amb = "AMB:" + String(sensorValue, DEC); return amb; } void setup () { Serial.begin (9600); pinMode (DHpin, OUTPUT); pinMode (ledPin, OUTPUT); } void loop() { //TEMPC:25#TEMPF:110#HUMPCT:40#AMB:20 Serial.println(getTemperature() + "#" + getHumidity() + "#" + getLight()); delay (1000); }
SensorData Output Format of
TEMPC:25#TEMPF:110#HUMPCT:40#AMB:20
Sensor Data Key | Description |
---|---|
TEMPC | Temperature in Centigrade |
TEMPF | Temperature in Fahrenheit |
HUMPCT | Humidity Percentage Value |
AMB | Ambiance Light Value. 250 is used as a threshold. Below 250 – low and above – high. |
Modeling Design Overview
Note that there is only ONE physical connection from the Controller (Laptop / Raspberry Pi or other Gateway Running TQLEngine) to the MCU. This connection is via USB serial. In order subscribe and query each of the individual sensors we follow the following steps:
- Create MCUSensor ThingModel to manage receiving data from MCU (and all the sensors).
- Create a Subscriber Action tied to a attribute of MCUSensor Model to handle updates from SensorData.
- Parse the SensorData and store it into the Context using Key as (TEMPC, TEMPF, etc)
- Create individual Sensor Models to update their respective readings from the Context.
Write a ThingFacet
To abstract the interactions with the MCU attached to the controller we write a MCUSensorFacet ThingFacet.
There are two kinds of attributes that are part of a ThingFacet.
- Attributes required to make a protocol specific invocation
- Attributes to store the information received from the MCU as part of the continuous stream.
Protocol Specific Parameters are: InterfacePort, Interface, Baudrate etc.
Let's create Type (Using Def) PeripheralParams to store the list of the protocol specific parameters
<Def Name="PeripheralParams"> <String Name="Peripheral"/> <String Name="InterfacePort"/> <String Name="Interface"/> <String Name="UniqueId" default="56789"/> <String Name="Baudrate"/> <String Name="Format" default="ascii"/> <String Name="Operation"/> <String Name="Payload"/> </Def>
Lets now create a MCUSensorFacet ThingFacet using the PeripheralParams type. The sensor data will be stored in SensorData attribute of type String.
<ThingFacet Name="MCUSensorFacet"> <String Name="SensorData"/> <PeripheralParams Name="PerifParams"/> </ThingFacet>
Write a Action with Workflow
The next step in ThingFacet is writing a Action which contains a workflow responsible for making external protocol specific call and store the output in the appropriate ThingFacet attribute. An important decision while writing Action is correct protocol selection. See the /wiki/spaces/TQLDocs/pages/1179871 in the Working with Things section. For this Multiple Sensors we need PERIF request, which is part of the TQLEngine.
1. Write an Action name as SensorDataReadAction
<ThingFacet Name="MCUSensorFacet"> ... <Action Name="SensorDataReadAction"> ... </Action> </ThingFacet>
2. Create a Workflow within the Action, with one single continuous running Task and waiting for the Event with its ActionArgument.
<Action Name="SensorDataReadAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task name="Main" while="true"> <Event name="Argument" as="ActionArgument"/> ... </Task> </Workflow> </Action>
Here we used three modifiers for this workflow. Limit = "1" means there can be at most one instance of this workflow waiting. Live = "1" means there can be at most one instance of this workflow running. Timeout ="-1" means this workflow will never be timed out. We used a modifier while = "true" with the Task to make the workflow running in a continuous loop, because it needs to run repeatedly, not just once. For more details, refer to workflow modifiers and the lifecycle of a workflow.
The task will be activated by the event handler Event called ActionArgument. "ActionArgument" is the event generated whenever the attribute(s) associated with this Action is modified (See Associate Action with a ThingFacet Attribute). ActionArgument carries all the current values of the ThingFacet attributes, which can be used in the task if needed.
3. Invoke PERIF call: Method of PERIF is Get. We use Invoke modifier Get for this purpose. The parameters required for PERIF Get are passed as Message / Value Container.
<Invoke name="InvokeSerialRead" waitFor="Argument" get="perif://"> <Message> <Value> <InterfacePort> "[%:Event.Argument.PerifParams.InterfacePort.Value:%]" </InterfacePort> <Baudrate> "[%:Event.Argument.PerifParams.Baudrate.Value:%]" </Baudrate> <Interface> "[%:Event.Argument.PerifParams.Interface.Value:%]" </Interface> <UniqueId> "[%:Event.Argument.PerifParams.UniqueId.Value:%]" </UniqueId> <Operation> "[%:Event.Argument.PerifParams.Operation.Value:%]" </Operation> <Peripheral> "[%:Event.Argument.PerifParams.Peripheral.Value:%]" </Peripheral> <Payload> "[%:Event.Argument.PerifParams.Payload.Value:%]" </Payload> <Format> "[%:Event.Argument.PerifParams.Format.Value:%]" </Format> </Value> </Message> </Invoke>
Note that the Event Parameter values are dereferenced using TP Notation. The notation [%: is part of the /wiki/spaces/TEACH/pages/21170773. More details on /wiki/spaces/TEACH/pages/21170773 can be found in the Developer's Guide.
Tag | Resolution |
---|---|
[%:Event.Argument.PerifParams.InterfacePort.Value:%] | InterfacePort value passed when creating/updating model instance. |
4. Store the output value in SensorData Attribute.
<Output Name="Result" As="ActionResult"> <Value> <SensorData> [%:Invoke.InvokeSerialRead.Message.Value/normalize-space(received):%] </SensorData> </Value> </Output>
Associate Action with a ThingFacet Attribute
We will now have to associate a ThingFacet Attribute (State) with the Action using the KnownBy modifier. When State is "KnownBy" PhilipLightAction, any State value changes will activate the PhilipLightAction.
<ThingFacet Name="MCUSensorFacet"> ... <String Name="SensorData" update="auto" KnownBy="SensorDataReadAction"/> </ThingFacet>
The attribute modifier update= "auto" makes sure that once the action associated with this attribute is triggered, its workflow continues to run and wait for subsequent sensor events (not just the first event). This modifier is only used with actionable attributes. For more details, refer to Automatic Action trigger.
Combining ThingFacet with a ThingModel
Finally, in order to use ThingFacet we have to combine it with a ThingModel. We define ThingModel to contain only a unique system identifier.
<ThingModel Name="MCUSensorModel" Combines="MCUSensorFacet"> <Sid Name="SensorId"/> </ThingModel>
MCUSensorFacet can only be instantiated (and write TQL Queries) when it is combined with MCUSensorModel Thing Model.
MCUSensorModel will inherit all the attributes from MCUSensorFacet, in addition to its own attributes.
The ThingFacet MCUSensorFacet hence serves as a reusable component.
More details on the use of "combines" can be found here. Information on Sid can be found here.
Temperature ThingModel
Let's create individual sensor models with respective ThingFacets. The difference between this Facet and regular MCUSensorFacet is that not physical connection is opened to the sensor.
We simply read the values from the Context. Using ContextData.
ContextData - Will have keys TEMPC an TEMPF. See below on who creates this ContextData keys.
<ThingFacet Name="TempSensorFacet"> <Double Name="TempValueInC" KnownBy="TempValueAction"/> <Double Name="TempValueInF" KnownBy="TempValueAction"/> <Action Name="TempValueAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task name="Main" while="true"> <Event name="Argument" as="ActionArgument"/> <Invoke name="ReadValueFromContext" waitFor="Argument"> <FacetScript> <Log Message="Reading TEMPC/TEMPF value from context [:$ContextData.TEMPC:] / [:$ContextData.TEMPF:]"/> <If Condition="$ContextData/not(boolean(TEMPC))"> <SetContextData> <Key>TEMPC</Key> <Value>0</Value> </SetContextData> <SetContextData> <Key>TEMPF</Key> <Value>0</Value> </SetContextData> </If> </FacetScript> </Invoke> <Output Name="Result" As="ActionResult"> <Value> <TempValueInC> [:$ContextData.TEMPC:] </TempValueInC> <TempValueInF> [:$ContextData.TEMPF:] </TempValueInF> </Value> </Output> </Task> </Workflow> </Action> </ThingFacet> <ThingModel Name="TempSensorModel" combines="TempSensorFacet"> <Sid Name="TempSensorId"/> </ThingModel>
Humidity ThingModel
Let's create individual sensor models with respective ThingFacets. The difference between this Facet and regular MCUSensorFacet is that not physical connection is opened to the sensor.
We simply read the values from the Context. Using ContextData.
ContextData - Will have key HUMPCT. See below on who creates this ContextData key.
<ThingFacet Name="HumiditySensorFacet"> <Double Name="HumidityValue" KnownBy="HumidityValueAction"/> <Action Name="HumidityValueAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task name="Main" while="true"> <Event name="Argument" as="ActionArgument"/> <Invoke name="ReadValueFromContext" waitFor="Argument"> <FacetScript> <Log Message="Reading HUMPCT value from context [:$ContextData.HUMPCT:]"/> <If Condition="$ContextData/not(boolean(HUMPCT))"> <SetContextData> <Key>HUMPCT</Key> <Value>0</Value> </SetContextData> </If> </FacetScript> </Invoke> <Output Name="Result" As="ActionResult"> <Value> <HumidityValue> [:$ContextData.HUMPCT:] </HumidityValue> </Value> </Output> </Task> </Workflow> </Action> </ThingFacet> <ThingModel Name="HumiditySensorModel" combines="HumiditySensorFacet"> <Sid Name="HumSensorId"/> </ThingModel>
Ambiance ThingModel
Let's create individual sensor models with respective ThingFacets. The difference between this Facet and regular MCUSensorFacet is that not physical connection is opened to the sensor.
We simply read the values from the Context. Using ContextData.
ContextData - Will have key AMB. See below on who creates this ContextData key.
<ThingFacet Name="AmbianceSensorFacet"> <Double Name="AmbianceValue" KnownBy="AmbianceValueAction"/> <Action Name="AmbianceValueAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task name="Main" while="true"> <Event name="Argument" as="ActionArgument"/> <Invoke name="ReadValueFromContext" waitFor="Argument"> <FacetScript> <Log Message="Reading AMB value from context [:$ContextData.AMB:]"/> <If Condition="$ContextData/not(boolean(AMB))"> <SetContextData> <Key>AMB</Key> <Value>0</Value> </SetContextData> </If> </FacetScript> </Invoke> <Output Name="Result" As="ActionResult"> <Value> <AmbianceValue> [:$ContextData.AMB:] </AmbianceValue> </Value> </Output> </Task> </Workflow> </Action> </ThingFacet> <ThingModel Name="AmbianceSensorModel" combines="AmbianceSensorFacet"> <Sid Name="AmbSensorId"/> </ThingModel>
Update Queries to Trigger Sensor Model Actions
<Macro Name="UpdateTempValueToNull"> <Argument/> <Result> <ExecuteQuery> <QueryString> <Query> <Find format="version"> <TempSensorModel> <TempSensorId ne=""/> </TempSensorModel> </Find> <SetResponseData> <key>Message.Value.Find.Result.TempSensorModel.TempValueInC.Value</key> <value>$Null()</value> </SetResponseData> <SetResponseData> <key>Message.Value.Find.Result.TempSensorModel.TempValueInF.Value</key> <value>$Null()</value> </SetResponseData> <Update> <from>Result</from> <Include>$Response.Message.Value.Find</Include> </Update> </Query> </QueryString> </ExecuteQuery> </Result> </Macro> <Macro Name="UpdateHumidityValueToNull"> <Argument/> <Result> <ExecuteQuery> <QueryString> <Query> <Find format="version"> <HumiditySensorModel> <HumSensorId ne=""/> </HumiditySensorModel> </Find> <SetResponseData> <key>Message.Value.Find.Result.HumiditySensorModel.HumidityValue.Value</key> <value>$Null()</value> </SetResponseData> <Update> <from>Result</from> <Include>$Response.Message.Value.Find</Include> </Update> </Query> </QueryString> </ExecuteQuery> </Result> </Macro> <Macro Name="UpdateAmbianceValueToNull"> <Argument/> <Result> <ExecuteQuery> <QueryString> <Query> <Find format="version"> <AmbianceSensorModel> <AmbSensorId ne=""/> </AmbianceSensorModel> </Find> <SetResponseData> <key>Message.Value.Find.Result.AmbianceSensorModel.AmbianceValue.Value</key> <value>$Null()</value> </SetResponseData> <Update> <from>Result</from> <Include>$Response.Message.Value.Find</Include> </Update> </Query> </QueryString> </ExecuteQuery> </Result> </Macro>
Subscribe to SensorData
In order to distribute the sensor data to different Sensor Models (Temp, Humidity and Ambiance) we need following building blocks:
The building blocks to Checking Weather Periodically are:
- Ability to Subscribe to SensorData attribute change.
- Parse the Sensor Data.
TQLEngine allows us to run subscribe to model changes from the Model itself using <OnRequest> Tag. We need to provide the Target ID to make this request. Since subscription can be called any number of times, it is always a good idea to wrap calling of a Subscription request in a generic Macro Definition. Let's call this SubscribeToTQL.
Generic Macro to Subscribe to a change in Model
<Macro Name="SubscribeToTQL"> <Argument> <TopicName>TQL.*</TopicName> <TopicId>GenericTopicID</TopicId> <ActionName/> </Argument> <Result> <DoRequest target="[:RuntimeParams.TopicFacetIDName:]"> <Process> <Message type="xml"> <Value> <Subscribe sid="[:$Macro.Argument.TopicId:]" topic="[:$Macro.Argument.TopicName:]"> <Action> [:$Macro.Argument.ActionName:] </Action> </Subscribe> </Value> </Message> </Process> </DoRequest> </Result> </Macro>
Macro to Execute a TQL Query
TQLEngine allows us to run TQL Query from the Model itself using <OnRequest> Tag. We need to provide the Target ID to make this request. Since TQL Query can be called any number of times, it is always a good idea to wrap calling of a TQL Query request in a generic Macro Definition. Let's call this ExecuteQuery.
<Macro Name="ExecuteQuery"> <Argument> <QueryString> <Query/> </QueryString> </Argument> <Result> <OnRequest> <Target>[:RuntimeParams.FacetIDName:]</Target> <Process> <Message> <Value>[:$Macro.Argument.QueryString:]</Value> </Message> </Process> </OnRequest> </Result> </Macro>
Parse SensorData Using JavaScript
TQLEngine allows us to use JavaScript code that can executed as part of the model flow. In this example we will use JavaScript to Parse the SensorData string.
<Macro Name="ProxySerialResponse"> <Argument></Argument> <Result> <ExecuteQuery> <QueryString> <Query> <Find format="known"> <MCUSensorModel> <SensorId ne=""/> </MCUSensorModel> </Find> </Query> </QueryString> </ExecuteQuery> <JavaScript> var str = "[:$Response.Message.Value.Find.Result.MCUSensorModel.SensorData.Known:]"; sffLog.info("***************SensorData**************" + str); var sensorToken = str.split("#"); var updateFacetData = ListMap.static.newInstance(); for (i=0; i<sensorToken.length; i++) { sval = sensorToken[i].split(":"); sffContext.setContextData(sval[0], sval[1]); } updateFacetData.instanceAdd("UpdateTempValueToNull"); updateFacetData.instanceAdd("UpdateHumidityValueToNull"); updateFacetData.instanceAdd("UpdateAmbianceValueToNull"); sffLog.info(updateFacetData); updateFacetData; </JavaScript> </Result> </Macro>
Start / Stop Subscription Action
So far we have all the building blocks required to subscribe, parse and distribute. We need a mechanism to start and stop the subscription. We can achieve this using a attribute and an associated action. For this purpose let's add an attribute called InitSubscribers with an action InitSubAction to MCUSensorFacet. The Pseudo logic to be implemented in this action is:
if InitSubscribers is true
subscribe using SubscribeToTQL Macro
else
unsubscribe
<String Name="InitSubscribers" default="false" KnownBy="InitSubAction"/> <Action Name="InitSubAction"> <Workflow Limit="1" Live="1" Timeout="-1"> <Task name="Main" while="true"> <Event name="Argument" as="ActionArgument"/> <Invoke name="InvokeSubs" waitFor="Argument"> <FacetScript> <If Condition="/'[%:Event.Argument.InitSubscribers.Value:%]' eq 'true'"> <Log Message="Subscribing to: *Atomiton.Sensor.MCUSensorModel.SensorData.[%:Event.Argument.SensorId:%]*"/> <SubscribeToTQL> <TopicName>*Atomiton.Sensor.MCUSensorModel.SensorData.[%:Event.Argument.SensorId:%]*</TopicName> <ActionName> <ProxySerialResponse/> </ActionName> </SubscribeToTQL> </If> </FacetScript> </Invoke> <Output Name="Result" As="ActionResult"> <Value> <InitSubscribers> [%:Event.Argument.InitSubscribers.Value:%] </InitSubscribers> </Value> </Output> </Task> </Workflow> </Action>
Query and subscription
Queries we need are:
- Find Sensor Information
- Initialize MCU Sensor Model
- Initialize Other Sensor Models
- Start Subscribers
- Externally subscribe to Sensor Model
- Externally subscribe to Other Sensors (Temperature)
Queries
<Query> <Find format="version,known"> <MCUSensorModel> <sensorId ne=""/> </MCUSensorModel> </Find> <Find format="version,known"> <TempSensorModel> <TempSensorId ne=""/> </TempSensorModel> </Find> <Find format="version,known"> <HumiditySensorModel> <HumSensorId ne=""/> </HumiditySensorModel> </Find> <Find format="version,known"> <AmbianceSensorModel> <AmbSensorId ne=""/> </AmbianceSensorModel> </Find> </Query>
<Query> <DeleteAll format="version,current"> <MCUSensorModel> <sensorId ne=""/> </MCUSensorModel> </DeleteAll> <Save format="version,current"> <!-- This will read --> <MCUSensorModel> <PerifParams> <Peripheral> serial </Peripheral> <Baudrate> 9600 </Baudrate> <InterfacePort> /dev/cu.usbserial-A1025R0Y </InterfacePort> <Interface> serial </Interface> <Format> ascii </Format> <Operation> receive </Operation> <UniqueId> 76522 </UniqueId> <Payload> $Null() </Payload> </PerifParams> <SensorData> $Null() </SensorData> <InitSubscribers> false </InitSubscribers> </MCUSensorModel> </Save> </Query>
<Query> <DeleteAll format="version,current"> <TempSensorModel> <TempSensorId ne=""/> </TempSensorModel> </DeleteAll> <DeleteAll format="version,current"> <HumiditySensorModel> <HumSensorId ne=""/> </HumiditySensorModel> </DeleteAll> <DeleteAll format="version,current"> <AmbianceSensorModel> <AmbSensorId ne=""/> </AmbianceSensorModel> </DeleteAll> <Create> <TempSensorModel> <TempValueInC> 0 </TempValueInC> <TempValueInF> 0 </TempValueInF> </TempSensorModel> </Create> <Create> <HumiditySensorModel> <HumidityValue> 0 </HumidityValue> </HumiditySensorModel> </Create> <Create> <AmbianceSensorModel> <AmbianceValue> 0 </AmbianceValue> </AmbianceSensorModel> </Create> </Query>
<Query> <Find format="version"> <MCUSensorModel> <SensorId ne=""/> </MCUSensorModel> </Find> <SetResponseData> <key> Message.Value.Find.Result.MCUSensorModel.InitSubscribers.Value </key> <value> true </value> </SetResponseData> <Update> <from> Result </from> <Include> $Response.Message.Value.Find </Include> </Update> </Query>
<Query Storage='TqlSubscription'> <Save> <TqlSubscription Label='MCUSensorModel' sid='20'> <Topic> *Atomiton.Sensor.MCUSensorModel* </Topic> </TqlSubscription> </Save> </Query>
<Query Storage='TqlSubscription'> <Save> <TqlSubscription Label='MCUSensorModel' sid='21'> <Topic> *Atomiton.Sensor.TempSensorModel* </Topic> </TqlSubscription> </Save> </Query>
Source Code
Import into TQLStudio
ProjectName | Import Link |
---|---|
Multiple Sensors | MultipleSensors |
Complete Source Code