Table of Contents
|
Introduction
Developing your own solution requires several steps to be accomplished:
- Sensor Board
- Sensor Side Code (BASIC code)
- Server Side Code (Python code)
On this example we will develop a slave monitor node for an analog temperature sensor.
This means the node will periodically read the sensor, store the reading into the history and then from time to time became visible and wait for SensorSDK Server to connect to it.
Sensor Board
On this guide we will use a simple sensor so we can avoid developing our own board. For this example we will use a sensor called TC1047 which is an analog temperature sensor with a slope of 10mV/°C and provides 500mV at 0°C. As the sensor is packed in a SOT-23 you might want to get one of this probes mounted in PCB.
As the sensor is analog and works under quite a wide voltage range we can connect it quite easily following this table:
AIRcableSMD PIN | AIRcableSMD Signal | TC1047 PIN | TC1047 Signal |
---|---|---|---|
18 | GND | 1 | VSS |
6 | AIO(1) | 2 | VOUT |
4 | VBATT | 3 | VCC |
The AIRcableSMD can't do more than 1.8V on AIO, if your sensor is over this voltage then you will not be able to read it, and you will need to use a voltage divider, the AIRcableSMD will not get damaged with voltages under 5V, but try not to reach the limit.
Make sure you don't power the TC1047 with more than 5.5V otherwise you will burn it out.
Sensor Side Code
Introduction
For this example we will reuse a few of the SensorSDK BASIC Services, the pieces we will reuse are:
- base
provides with basic button handling, lcd screen displaying (including scrolling), sensor display, interrupt handling, analog inputs reading, indevice debug menu
- shell
provides with communications protocol and history persistence
- monitor
configures the device to work as a monitor node, setup basic button interface, some basic history handling, visible handling.
- stream=slave
set the template shared variable stream to slave so our BASIC code gets rendered using slave spp as stream for communication, also by setting stream to slave makes monitor piece to use slave and not master for communication establishing.
Generation Script
In order to do this we need a little Cheetah that does all this configuration:
#*
Example usage, this will generate
monitor generic linear testing code as SLAVE
*#
#set global $stream="slave"
#include "base/AIRcable.bas"
#include "shell/AIRcable.bas"
#include "monitor/AIRcable.bas"
#include "generic-linear/AIRcable.bas"
In this example we're adding our sensor code too, and calling it AIRcable.bas and storing it under generic-linear.
We can try creating a first testing code by running:
python parser.py slave-generic-linear-monitor
This will fail if generic-linear/AIRcable.bas doesn't exist. If it works it will print out the code, you will need to redirect the output if you want to be able to read it later. But let's keep that part by now.
Let's try to summarize what we have done so far: we told SensorSDK to use a slave stream, this means the sensor node will became visible, setup the communication channel and then wait for the server side to connect to it. We also added base code, without it we would have to write lots of lines of code for every kind of sensor we're going to deploy. Shell implements all the communication framework (again less code for us to write), monitor makes sure our device became visible and tell us code when to read and display sensors and then it comes our sensor code. So let's take a look at it.
Sensor Code
Let's first take a look at the code:
#*
sample: generic linear sensor handler
any generic linear sensor can be attached
to one of the analog inputs. SensorSDK will
store reading at $13, we need to store
message information on $10 and display stuff
on line $11.
This code can handle generic sensors whose
output is like VAL = mV / SLOPE + OFFSET
We call this mode Linear-A
Linear-B mode is VAL = mv * SLOPE + OFFSET
reading update handler starts at line 510
reading display handler needs to start at
line 595
*#
## type
19 MONITOR-GENERIC-LINEAR
## set our interrupt points
30 GOTO 510;
31 GOTO 595;
## over ride a few lines
941 IF U-V>=300 THEN 944;
942 IF V>=U THEN 944;
943 GOTO 910;
944 V=U;
## $500 stores our mode
## $501 stores our slope
## $502 stores our offset
## for the TC1047
## we have: Vo=10mV/C * T + 500mV
## or: T = Vo / 10 - 50
## where Vo is in mV
## conversion formula
500 A
501 10
502 -50
## we'll store reading in $509
509 READING
## update reading.
510 $509 = $13[5];
511 $509[4] = 0;
512 M = atoi $509;
513 A = atoi $501;
514 B = atoi $502;
515 IF $500[0]=66THEN 518;
516 M=M/A+B
517 GOTO 519;
518 M=M*A+B
519 PRINTV"NEXT
520 GOSUB 660
521 $0="LIN|"
522 PRINTV$509
523 FOR A=500 TO 502
524 PRINTV"|"
525 PRINTV \$A
526 NEXT A
527 $10=$0
528 $0=""
529 RETURN
## display value generator
595 $0="LIN "
596 PRINTV M
597 PRINTV "%C"
598 $11 = $0
599 RETURN
SensorSDK Interrupt Table
As you know SensorSDK was created to be as simple as possible (off course for an embedded application on a really constrained device). The idea is that SensorSDK expects user code to be executed when SensorSDK thinks it's necessary, in this case we have a few interrupt points been handled by monitor code and a few others handled by this sensor it self. The complete interrupt table is defined as:
## user @INIT pointer
20 RETURN
## user @ALARM pointer
21 RETURN
## user @IDLE
22 RETURN
## sensor reading
30 RETURN
## sensor display
31 RETURN
## left long button press
34 RETURN
## middle long button press
35 RETURN
## right long button press
36 RETURN
## left short button press
37 RETURN
## middle short button press
38 RETURN
## right short button press
39 RETURN
Now our code looks like
## @init handled by monitor code
20 GOTO 990;
## @alarm handled by monitor code
21 GOTO 900;
## @idle handled by monitor code
22 GOTO 955;
## read sensor handled by sensor code
30 GOTO 510;
## display sensor handled by sensor code
31 GOTO 595;
## button presses handled by monitor code
34 GOTO 235;
35 GOTO 410;
36 GOTO 430;
37 GOTO 460;
38 GOTO 700;
39 GOTO 400;
As you can see out of 11 interrupts for some basic sensing we only need to implement 2, easy isn't it?
Reading From Analog Sensor
Now let's work a bit more on how we measure our sensor
## for the TC1047
## we have: Vo=10mV/C * T + 500mV
## or: T = Vo / 10 - 50
## where Vo is in mV
## conversion formula
500 A
501 10
502 -50
This generic code could be modified to be used for any linear sensor that you will find out there, but by default we set our parameters for the TC1047, linear sensors can be described in categorized in two groups:
- < 1 slope: we call this mode A, in this case we're doing reading=reading_mv/slope + offset
- > 1 slope: we call this mode B, in this case we're doing reading=reading_mv*slope + offset
## update reading.
510 $509 = $13[5];
511 $509[4] = 0;
512 M = atoi $509;
513 A = atoi $501;
514 B = atoi $502;
On this piece of code we first take out from the shared sensor reading line ($13) the 4 bytes that will represent our sensor, then we trim it to only 4 characters, and finally we convert it to an integer, otherwise we can't do any math.
The other two lines just parse the slope and the offset, if you known which kind of sensor you're going to use, and you know your sensor will not need to be calibrated again then you could just omit 513 and 514 and replace line 516 or 518 (depending if you're in mode A or B) variables A and B with slope and offset values.
We keep it in different lines so we can later keep a trace over calibration values.
515 IF $500[0]=66THEN 518;
516 M=M/A+B
517 GOTO 519;
518 M=M*A+B
This little piece of the code does the conversion, based on the mode your sensor needs, and store the reading into variable M so we can later display it.. Take into account that if your slope is > 1 then you might face integer overflow in this case we recommend you do a bit more of math and not this simple calc, otherwise you will loose a lot of precisions, don't bother if you don't want this value displayed on the screen.
519 PRINTV"NEXT
520 GOSUB 660
521 $0="LIN|"
522 PRINTV$509
523 FOR A=500 TO 502
524 PRINTV"|"
525 PRINTV \$A
526 NEXT A
527 $10=$0
528 $0=""
529 RETURN
Ok this piece of code is the one that seems to be more difficult but actually it isn't.
SensorSDK transmission protocol is quite simple, lines like this one are transfered:
BATT|batt reading|SECS|seconds since last reading|<user reading>
Problem is that this lines have a maximum length of 80 characters, happens easily when you have more than one sensor attached, or in this case send extra parameters, if you want to use over 80 characters then you need to set <user reading> to NEXT
When interrupt 30 gets called $0 is holding this: BATT|batt reading|SECS|seconds since last reading| so what the code does is printing NEXT to $0 and then pushing into history (GOSUB 660), then we set our line to our content, which looks like:
LIN|<mv reading>|<slope>|<offset>|
Finally we set $10 to $0 because that's what SensorSDK expects from us.
In case you don't need to send extra parameters you could simplify this code quite a bit, in that case code for TC1047 would look like:
## temp storage
508 TEMP
## reading holder
509 READING
510 $509 = $13[5];
511 $509[4] = 0;
512 M = atoi $509;
513 M=(M/10)-50
514 $508=$0
515 $0="TAMB|"
515 PRINTV M
516 $10=$0
517 $0=$508
518 RETURN
Displaying reading in LCD display
The last piece of code we will describe is how we display data in the LCD display, code for this is shown here:
## display value generator
595 $0="LIN "
596 PRINTV M
597 PRINTV "%C"
598 $11 = $0
599 RETURN
Suppose our reading is 10°C then our screen should show:
LIN 10°C
The process is quite simple, we do it by string concatenating "LIN " our reading (stored in variable M) and "%C", % gets rendered as ° in the LCD screen. Finally we store our reading to $11 as the SensorSDK expects it.
Resulting code
We have packaged all the code used for this example, the way you can get your code out (supposing you're inside the BASIC folder from the package)
[user@computer:BASIC]$ python parser.py slave-generic-linear-monitor > AIRcable.bas
Now if you want to get your code cleaned out of comments, and duplicated lines, you can execute this:
[user@computer:BASIC]$ python parser.py slave-generic-linear-monitor | python clean.py > AIRcable.bas
Good thing about using clean.py is that it will inform you of lines overriding and you can check you're not harming code.
Server Side Code
Last part we will cover is Server Side plug-in code. We will do this part the other way than we did for the BASIC code first download the package code:
[monitor generic linear server side code]
Lets now take a look at our folder structure:
|-> monitorgenericlinear/
|-> __init__.py
|-> models.py
\-> sdkadmin.py
|-> build.sh
|-> clean.sh
\-> setup.py
In this structure we have a few development helper files (build.sh, clean.sh), the python packing specification (setup.py) and the plug-in it self (monitorgenericlinear)
Development helpers
The build.sh and clean.sh scripts can be used while you're doing development to simplify packing and cleaning up your source tree. You can use them easily by calling:
> bash build.sh
> bash clean.sh
Python Packing Specification
Python egg files are created according to some rules, our setup.py implements this rules as:
from setuptools import setup
from monitorgenericlinear import __version__
setup(name="SensorSDK Generic Linear Sensor",
version=__version__,
packages=['monitorgenericlinear',],
summary="SensorSDK Generic Linear Sensor",
description="""SensorSDK plugin""",
long_description="""A sample plugin for generic linear sensor monitoring""",
author="Naranjo Manuel Francisco",
author_email= "manuel@aircable.net",
license="GPL2",
url="http://code.google.com/p/aircable/",
)
All the action here happens in the setup call which passes our configuration to setuptools and then lets it do it's work. It mainly defines a few help text that will be appended to the egg file, it's interesting to notice it takes the version number dynamically by reading it from the plugin it-self this way you don't have to worry about version number been in sync in two files at the same time. Also check we tell the setup function to find it's package code in 'monitorgenericlinear' without it, it doesn't know where to get your code.
Plug-in specification
Each SensorSDK plug-in must define a few variables, which are defined in monitorgenericlinear/init.py. Here you can see how it's defined.
__version_info__=('0','1','1')
__version__ = '.'.join(__version_info__)
# SensorSDK plugin
provides = {
'name': 'SensorSDK plugin - Generic Linear Monitor',# friendly name
'enabled': True, # enable me please
'sensorsdk': True, # expose me as a sensorsdk plugin
'django_app': True, # we provide an application so we can
# define models
}
We define a few variables here:
- version number
- provides:
- a friendly name
- enable the plugin
- expose as a sensorsdk plugin (so sensorsdk knows it needs to talk to us)
- tell django we provide a django app so we can not only provide models but also our own admin page.
Models
For every SensorSDK plug-in we need to define two basic models that inherit from SensorSDK models: a device model and a record model, in this example we will name this classes as: GenericLinearDevice and GenericLinearRecord, each of this classes add a few fields so they're then stored into the database.
[[note]]
This code is a resume of the real code
[[/note]]
try:
from sensorsdk import models
except:
from plugins.sensorsdk import models
from django.db import models as mod
class GenericLinearDevice(models.SensorSDKRemoteDevice):
slope = mod.FloatField(default=10)
offset = mod.FloatField(default=10)
units = mod.CharField(default="Celcius Degrees", max_length=40)
def getValue(self, reading):
out = int(reading) * self.slope + self.offset
return out
@staticmethod
def getMode():
'''Let the sensorsdk engine know which node we are handling'''
return "monitor-generic-linear"
@staticmethod
def getChartVariables():
return ['reading', 'reading_mv', 'battery']
@staticmethod
def getRecordClass():
return GenericLinearRecord
class GenericLinearRecord(models.SensorSDKRecord):
reading = mod.FloatField()
reading_mv = mod.IntegerField()
slope = mod.FloatField(default=10)
offset = mod.FloatField(default=10)
@staticmethod
def getMode():
'''Let the sensorsdk engine know which node we are handling'''
return "monitor-generic-linear"
@staticmethod
def parsereading(device=None, seconds=None, battery=None, reading=None, dongle=None):
'''This method expects to get a valid reading, generating a record out of it'''
.....
record.save()
Each of the classes extending SensorSDK needs to implements a few methods (if you don't SensorSDK will let you know as it will throw errors).
- GenericLinearDevice implements:
- getValue: this method gets a millivolt reading and should return the real value of the reading based on offset and slope.
- getMode: this method returns the string monitor-generic-linear which is first been sent by the node when the connection is firstly established (line 19 of the BASIC code)
- getChartVariables: this method returns a list of strings which can be then charted by the SensorSDK charting system.
- getRecordClass: this method returns the class used for storing a reading.
- GenericLinearRecord implements:
- getMode: this method is the same you find in GenericLinearDevice
- parsereading: this method will get called for each of the lines in the history received from the remote node, this method is responsible for then storing the record.
History parsing and data storing
from re import compile
...
lin=compile(r'LIN\|(?P<value>\d+).*\|(?P<mode>[AB])\|(?P<slope>[+-]?.*)\|(?P<offset>[+-]?.*)$')
class GenericLinearRecord(models.SensorSDKRecord):
.......
@staticmethod
def parsereading(device=None, seconds=None, battery=None, reading=None, dongle=None):
'''This method expects to get a valid reading, generating a record out of it'''
#extract parameters from reading string
m = lin.match(reading)
if not m:
logger.error("NO MATCH %s" % reading)
return
m = m.groupdict()
value = int(m['value'])
mode = m['mode']
slope = int(m['slope'])
offset = int(m['offset'])
if mode=='A':
slope = 1.0/slope
#find ambient device, or create if there's none yet created
device,created=GenericLinearDevice.objects.get_or_create(
address=device,
defaults={
'friendly_name': _('Auto Discovered Generic Linear Sensor'),
'sensor': _('Temperature'),
'mode': _('Monitor'),
'slope': slope,
'offset': offset
})
reading = device.getValue(value)
record = GenericLinearRecord()
record.slope = slope
record.offset = offset
record.remote=device
record.dongle=dongle
record.reading = reading
record.reading_mv = value
record.time=datetime.fromtimestamp(seconds)
record.battery=battery
record.save()
This method will firstly check if the reading line it got matches the regular expression that describes this line, if so it extract the parameters from it. Convert strings to integers when needed. And then adapts the slope value depending if mode is A or B (only on the first mode there's an adaption).
Then the system will try to determinate if there is a GenericLinearDevice matching the bluetooth address from which this reading was taken. If it's not it will fall back to the default values, where as you can see are based on values gotten from the reading (so slope and offset match the settings).
Once the device has been created it asks the GenericLinearDevice instance to get the real reading (temperature in this example) out of the milli volts reading.
Finally it creates a new GenericLinearRecord instance, fills up all the available fields and calls the save method. A few things are important here first that some of this fields aren't defined by our class, they had been inherited from SensorSDK classes and then also that you need to call the save method, otherwise you will not get any data recorded.