Hardware components | ||||||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
| |||||
![]() |
| |||||
![]() |
|
This project is created in context of the lecture “Internet of Things Ecosystems” at the University of Applied Science Esslingen. It covers the invention of an own IoT Idea and Application from the scratch solving a real-world problem. In the design sprints the students learn about design, implementation (full-stack) and business aspects of Internet of Things applications.
IoT Prototyping FrameworkThe IoT Prototyping Framework (IoTPF) is a collection of tools, modules and samples with the aim to empower students and developers creating full-stack IoT prototypes in a short time period.
ContactIdea and Application : Daniel Stümke, Michael Koidis, Daniel Hodyra and Kristian Madunic
Lecture and IoTPF: dionysios.satikidis@gmail.com
InspirationAfter visiting his Sister, after a surgery, at the Hospital one of our team members noticed that the painkillers were not supplied correctly via the Infusion. He had to make the nurse aware of the Situation resulting in unnecessary pain and complications due to the delayed medication.
IdeaI.S.I tackles that Problem by supplying its user with information about the State of the infusion. Which means if its either running correctly or if the infusion is empty. When the infusion turns empty or the medicine stops flowing due to complications like a kink in the tube, the device will send a push-notification to its users.
SketchWeight Cell:
The sensor measures the weight of the infusion bag. Since only the rate of change is used for the evaluation, the empty weight and the initial filling volume need not be known.
Get the weight cell:
https://www.instructables.com/id/Get-a-Hanging-Weight-Sensor-for-Your-Arduino-Proje/
Setup your Photon:
System Overview:
Principle:
To detect whether the infusion bag is empty or the inlet is closed, the Intelligent Gravity Infusion System observes the weight change of the infusion bag over time. The two cases can be distinguished as follows.
Empty infusion bag:
As long as the infusion is running, the change in weight is almost constant. If the infusion bag is almost empty, the rate of change is close to zero within about 20 seconds.
If such a " mild " change is observed, the system assumes that the infusion bag is empty.
Closure of the indwelling cannula or kink in the infusion tube:
In this situation, the discharge speed changes rapidly to a smaller value. The system detects such a "jump" and thus registers the closure or kink.
To inform the User about the Status of the Infusion we decided to provide two different interfaces since Users of our System will most likely be nurses which may not be allowed to carry a mobilephone or a tablet with them all the time.
Android Application
The Android Application was written in Java with Android Studio.
GitRepository:
WebApplication
The web application can be used in for example the employee room. It displays information about all I.S.I. systems installed in the institution.
<html><head>
<title>I.S.I. Overview</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
background-color: #F2EEE9;
font: normal 13px/1.5 Arial, Serif;
color: #333;
}
.wrapper {
width: 705px;
margin: 20px auto;
padding: 20px;
}
h1 {
color: #59c4f2;
font-size: 20px;
font-weight: normal;
text-transform: uppercase;
padding: 8px 0px;
}
.clear {
clear: both;
}
.items {
display: block;
}
.item {
background-color: #fff;
float: left;
margin: 0 10px 10px 0;
width: 205px;
padding: 10px;
}
.item img {
display: block;
margin: auto;
width: 170px;
padding: 15px;
}
h2 {
font-size: 13px;
display: block;
border-bottom: 1px solid #ccc;
margin: 0 0 10px 0;
padding: 0 0 5px 0;
}
label,
button {
border: 1px solid #722A1B;
padding: 4px 14px;
background-color: #fff;
color: #722A1B;
text-transform: uppercase;
margin: 5px 0;
font-weight: bold;
cursor: pointer;
}
label {
float: right;
}
img#logo {
width: 100px;
}
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
margin: auto;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.trigger, .disabled_trigger{
text-transform: uppercase;
margin-top: 10%;
display: block;
}
.disabled_trigger {
opacity: 0.5;
pointer-events: none;
}
.popup__textbox{
color: #b43d54;
line-height: 25px;
font-size: 1.1em;
letter-spacing: 1px;
}
/* Start popup css */
@keyframes bg {
from {opacity: 0;}
to {opacity: 1;}
}
@keyframes inner {
0% {transform: scale(0.8);}
50% {transform: scale(1.06);}
100% {transform: scale(1);}
}
@-webkit-keyframes bg {
from {opacity: 0;}
to {opacity: 1;}
}
@-webkit-keyframes inner {
0% {transform: scale(0.8);}
50% {transform: scale(1.06);}
100% {transform: scale(1);}
}
.popup__check{
display: none;
}
.popup__base, .popup__bg{
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
cursor:zoom-out;
}
.popup__base{
background-color: rgba(0,0,0,0.5);
display: none;
}
.popup__check:checked + .popup__base{
display: block;
animation-name: bg;
animation-duration: .5s;
animation-fill-mode:forwards;
-webkit-animation-name: bg;
-webkit-animation-duration: .5s;
-webkit-animation-fill-mode:forwards;
}
.popup__inner{
position: absolute;
z-index: 10;
width: 70%;
height: 70%;
background-color: #fff;
top: 15%;
left: 15%;
display: block;
cursor:default;
}
.popup__check:checked + .popup__base .popup__inner{
animation-name: inner;
animation-duration: .5s;
animation-fill-mode:forwards;
-webkit-animation-name: inner;
-webkit-animation-duration: .5s;
-webkit-animation-fill-mode:forwards;
}
.popup__textbox{
height: 95%;
width: 95%;
padding-left: 2.5%;
padding-right: 2.5%;
margin-top: 10px;
overflow: auto;
}
.popup__calign{
float: right;
padding-right: 60px;
font-size: 50px;
}
.popup__close{
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
display: block;
position: absolute;
z-index: 10;
text-align: right;
cursor: pointer;
color: #b43d54;
border: none;
padding: 0;
}
</style>
</head>
<body>
<!-- wrapper -->
<div class="wrapper">
<img id="logo" src="logo.png"><h1>System Overview:</h1>
<div class="clear"></div>
<!-- items -->
<div class="items" id="item-container">
</div>
<!--/ items -->
</div>
<!--/ wrapper -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.5.3/bluebird.js"></script>
<script>
let timeLimit = 90;
let infusions = [
{
deviceId: '3e0022000c47353136383631',
accessToken: '1e43056f563df6c892b932875ca1e3c03efaca75',
location: '',
weight: 0,
counter: 0,
online: null,
initialized: null,
ready: false,
debug: false
},
{
deviceId: '123456789012345678901234',
accessToken: 'demo',
location: 'Demo location 1',
weight: 0,
counter: Math.floor(Math.random() * 600),
online: null,
initialized: null,
ready: false,
debug: false
},
{
deviceId: '098765432109876543210987',
accessToken: 'demo',
location: 'Demo location 2',
weight: 0,
counter: Math.floor(Math.random() * 600),
online: null,
initialized: null,
ready: false,
debug: false
}
];
function RefreshFromCloud() {
for(i in infusions) {
if('demo' == infusions[i].accessToken) {
// Debug mode
infusions[i].debug = true;
infusions[i].ready = true;
infusions[i].online = true;
infusions[i].initialized = true;
let newCounter = infusions[i].counter + 10;
if(newCounter >= 10*60) newCounter = 0;
infusions[i].weight = 1;
infusions[i].counter = newCounter;
} else {
// Normal mode
axios.get ("https://api.particle.io/v1/devices/"
+ infusions[i].deviceId
+ "/serialized",
{
params:
{
access_token: infusions[i].accessToken
},
timeout: 1000
})
.then ((function (i, response) {
// Deserialize response
console.log(response);
if(response.data.result.length >= 25) {
let deserialized = DeserializeResponse(response.data.result);
infusions[i] = {...infusions[i], ...deserialized, online: true, ready: true};
console.log("Fetch successfull");
console.log(infusions[i]);
}
}).bind(this, i))
.catch ((function (i, error) {
infusions[i] = {...infusions[i], online: false, ready: true};
console.log("Fetch failed");
console.log(error);
console.log(infusions[i]);
}).bind(this, i));
}
}
}
function DeserializeResponse(serialized) {
let weight = parseFloat(serialized.substr(0,16).trim());
let counter = parseInt(serialized.substr(16,8).trim());
let initialized = serialized.charAt(24) == '1';
let location = initialized ? serialized.substr(25) : "";
return {weight, counter, initialized, location};
}
function RenderItems() {
let output = "";
let id = 1;
for(i in infusions) {
infusion = infusions[i];
output += `
<div class="item" id="infusion-item-${id}">
<div id="infusion-${id}" style="display: none">
<img id="infusion-image-${id}" src="infusion.png" alt="item">
<h2>Id: <span id="infusion-id-${id}"></span></h2>
<p>Ort: <em style="float: right"><span id="infusion-location-${id}"></span></em>
</p>
<p>Status: <em style="float: right"><span id="infusion-status-${id}"><span></em>
</p>
<label class="trigger" id="infusion-button-${id}" for="popup__${id}"></label>
</div>
<div class="loader" id="infusion-load-${id}"></div>
</div>
<input type="checkbox" id="popup__${id}" class="popup__check" />
<div class="popup__base">
<label for="popup__${id}" class="popup__bg"></label>
<div class="popup__inner">
<div class="popup__calign">
<label for="popup__${id}" class="popup__close">+</label>
</div>
<div class="popup__textbox">
<h1><span id="popup-headline-${id}"></span></h1>
<p>
<u>Trage hier die Werte ein:</u>
<p>
<p>
Ort: <input type="Text" id="input-location-${id}"/>
</p>
<br/>
<button type="button" id="input-button-${id}">bernehmen</button>
</div>
</div>
</div>
`;
id++;
}
let container = document.getElementById('item-container');
container.innerHTML = output;
}
RenderItems();
function Update() {
RefreshFromCloud();
let id = 1;
for(i in infusions) {
infusion = infusions[i];
if(infusion.ready) {
// Hide loading animation
let loader = document.getElementById(`infusion-load-${id}`);
loader.style.display = "none";
let infusionBlock = document.getElementById(`infusion-${id}`);
infusionBlock.style.display = "block";
let infusionId = document.getElementById(`infusion-id-${id}`);
infusionId.innerHTML = infusion.deviceId;
if(infusion.online) {
let inputButton = document.getElementById(`input-button-${id}`);
inputButton.onclick = (function(j) {
let inputLocation = document.getElementById(`input-location-${j}`);
let newLocation = inputLocation.value;
if((newLocation = newLocation.trim()) != "") {
// Send new value to device ...
infusions[j-1] = {...infusions[j-1], location: newLocation};
if(!infusions[j-1].debug)
axios.post ("https://api.particle.io/v1/devices/"
+ infusions[j-1].deviceId
+ "/init?access_token="
+ encodeURI(infusions[j-1].accessToken),
`arg=${infusions[j-1].location}`,
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 5000
})
.then (function (response) {
})
.catch (function (error) {
});
// Collapse popup
let checkbox = document.getElementById(`popup__${j}`);
checkbox.checked = false;
} else {
alert("Unvollstndige Eingabe!");
}
}).bind(this, id);
if(infusion.initialized) {
let infusionButton = document.getElementById(`infusion-button-${id}`);
infusionButton.innerHTML = "ndern";
infusionButton.className = "trigger";
let popupHeadline = document.getElementById(`popup-headline-${id}`);
popupHeadline.innerHTML = "ndern";
let inputLocation = document.getElementById(`input-location-${id}`);
inputLocation.placeholder = infusion.location;
let infusionStatus = document.getElementById(`infusion-status-${id}`);
if(infusion.counter < 60) {
infusionStatus.innerHTML = "Infusion luft";
} else {
let minutes = Math.floor(infusion.counter / 60);
if(minutes == 1) {
infusionStatus.innerHTML = "Seit " + minutes + " Minute leer";
} else {
infusionStatus.innerHTML = "Seit " + minutes + " Minuten leer";
}
}
// Calculate color.
// Starts with green, transforms to red over yellow.
let red = 0, green = 0, blue = 0;
let maxTime = Math.floor(timeLimit*3/4); // Full red after 3 quarter of time limit
let time = infusion.counter - 60;
if(time < 0) {
time = 0;
} else if(time > maxTime) {
time = maxTime;
}
let f = time / maxTime; // convert to [0 ... 1]
let a = (1.0 - f)*2.0;
let x = Math.floor(a);
let y = Math.floor(255.0*(a-x));
let group = Math.floor(x); // Group: 0->red 1->yellow 2->green
switch(group)
{
case 0:
red=255;
green=y;
blue=0;
break;
case 1:
red=255-y;
green=255;
blue=0;
break;
case 2:
red=0;
green=255;
blue=y;
break;
}
let infusionItem = document.getElementById(`infusion-item-${id}`);
infusionItem.style.backgroundColor = `rgb(${red},${green},${blue})`;
let infusionLocation = document.getElementById(`infusion-location-${id}`);
infusionLocation.innerHTML = infusion.location;
} else {
let infusionButton = document.getElementById(`infusion-button-${id}`);
infusionButton.innerHTML = "Einrichten";
let popupHeadline = document.getElementById(`popup-headline-${id}`);
popupHeadline.innerHTML = "Einrichten";
let infusionStatus = document.getElementById(`infusion-status-${id}`);
infusionStatus.innerHTML = "Nicht eingerichtet";
let infusionLocation = document.getElementById(`infusion-location-${id}`);
infusionLocation.innerHTML = "---";
}
} else {
let infusionButton = document.getElementById(`infusion-button-${id}`);
infusionButton.innerHTML = "Einrichten";
infusionButton.className = "disabled_trigger";
let infusionStatus = document.getElementById(`infusion-status-${id}`);
infusionStatus.innerHTML = "Offline";
let infusionLocation = document.getElementById(`infusion-location-${id}`);
infusionLocation.innerHTML = "---";
}
}
id++;
}
}
setInterval(function() {
Update();
}, 1000);
function OfflineAnimation(colorA, colorB) {
for(let i=0; i<infusions.length; i++) {
if(infusions[i].ready && !infusions[i].online) {
let id = i+1;
let infusionItem = document.getElementById(`infusion-item-${id}`);
infusionItem.style.backgroundColor = colorA;
}
}
setTimeout(OfflineAnimation.bind(this,colorB,colorA), 700);
}
OfflineAnimation("rgb(150,150,150)","rgb(255,255,255)");
</script>
</body></html>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Neo-Circle stuff (copy from: https://github.com/MrDio/IoT-Prototyping-Framework/tree/master/IoT-Eco-NeoPIXEL_Ring_Sample)
//
/**
*
* Wiring for neo-pixel:
*
* PHO (D2) - NEO (Data Input)
* PHO (GND) - NEO (GND)
* PHO (3V3) - NEO (5V)
*
*/
// This #include statement was automatically added by the Particle IDE.
#include <neopixel.h>
#include "HX711ADC.h"
#include "application.h"
#include "math.h"
class IoTEcoSys_NeoPIXEL_Ring
{
private:
public:
IoTEcoSys_NeoPIXEL_Ring(int pixel);
IoTEcoSys_NeoPIXEL_Ring();
bool init();
int setGlRGB( String color);
void neoRingClockWise(int circles, int del, String color);
void neoRingFillClockWise(int showuptime, int del, String color);
void neoRingDoubleCounterClockWise(int circles, int del, String color);
void neoRingCounterClockWise(int circles, int del, String color);
void changeColor(uint32_t color);
};
// IMPORTANT: Set pixel PIN, COUNT, and TYPE
// Supported pixel types: WS2812, WS2812B, WS2812B2, WS2811, TM1803, TM1829, SK6812RGBW
#define PIXEL_PIN D2
#define PIXEL_COUNT 16
#define PIXEL_TYPE SK6812RGBW
#define BRIGHTNESS 250 // 0 - 255
Adafruit_NeoPixel _ring = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
int p1[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
int p2[] = {8,9,10,11,12,13,14,15,16,0,1,2,3,4,5,6,7};
int gl_red = 0;
int gl_green = 0;
int gl_blue = 0;
// Constructor inits NeoPIXEL-Ring related to pixels (default is pin 2)
/*!
\param pixels amount of pixels on ring
\return obj insctance of class
*/
IoTEcoSys_NeoPIXEL_Ring::IoTEcoSys_NeoPIXEL_Ring() {
}
IoTEcoSys_NeoPIXEL_Ring::IoTEcoSys_NeoPIXEL_Ring(int pixel) {
}
//! The init function - nothing to do in this class
/*!
*/
bool IoTEcoSys_NeoPIXEL_Ring::init(){
Serial.begin(9600);
_ring.setBrightness(BRIGHTNESS);
_ring.begin();
_ring.show(); // Initialize all pixels to 'off'
return true;
}
// Set RGB
/*!
\param color red, green, blue, white or off
\return int err = 0, else = 1
*/
int IoTEcoSys_NeoPIXEL_Ring::setGlRGB( String color) {
if(color == "red") {
gl_green = 0;
gl_red = 255;
gl_blue = 0;
//ring.Color(0, 255, 0); // GRB
}
else if(color == "green") {
gl_green = 255;
gl_red = 0;
gl_blue = 0;
//ring.Color(255, 0, 0); // GRB
}
else if(color == "blue") {
gl_green = 0;
gl_red = 0;
gl_blue = 255;
//ring.Color(0, 0, 255);
}
else if(color == "white") {
gl_green = 255;
gl_red = 255;
gl_blue = 255;
_ring.Color(gl_green, gl_red, gl_blue, 255); // GRB+W
}
else if(color == "off") {
gl_green = 0;
gl_red = 0;
gl_blue = 0;
_ring.Color(gl_green, gl_red, gl_blue); // GRB+W
}
}
// NeoPIXEL ring rotate clockwise
/*!
\param circles amount of circles to animate
\param del delay in ms
\param color red, green, blue, white or off
*/
void IoTEcoSys_NeoPIXEL_Ring::neoRingClockWise(int circles, int del, String color){
for(int c=0;c<circles;c++){
setGlRGB("off");
changeColor(_ring.Color(gl_green, gl_red, gl_blue)); // GRB
_ring.show();
for(int i=0;i<16;i++){
setGlRGB(color);
_ring.setPixelColor(15-p1[i],_ring.Color(gl_green, gl_red, gl_blue));
if(i > -1){
_ring.setPixelColor(15-(p1[i]-1),_ring.Color(0, 0, 0));
}
_ring.show();
delay(del);
}
}
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
}
// NeoPIXEL ring fill clockwise
/*!
\param showuptime time to showup in ms
\param del delay in ms
\param color red, green, blue, white or off
*/
void IoTEcoSys_NeoPIXEL_Ring::neoRingFillClockWise(int showuptime, int del, String color){
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
for(int i=0;i<16;i++){
setGlRGB(color);
_ring.setPixelColor(15-p1[i],_ring.Color(gl_green, gl_red, gl_blue));
if(i > -1){
}
_ring.show();
delay(del);
}
delay(showuptime);
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
}
// NeoPIXEL ring double counter clockwise
/*!
\param circles amount of circles to animate
\param del delay in ms
\param color red, green, blue, white or off
*/
void IoTEcoSys_NeoPIXEL_Ring::neoRingDoubleCounterClockWise(int circles, int del, String color){
for(int c=0;c<circles;c++){
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
for(int i=0;i<16;i++){
setGlRGB(color);
_ring.setPixelColor(p1[i],_ring.Color(gl_green, gl_red, gl_blue));
_ring.setPixelColor(p2[i],_ring.Color(gl_green, gl_red, gl_blue));
if(i > -1){
setGlRGB("off");
_ring.setPixelColor(p1[i]-1,_ring.Color(gl_green, gl_red, gl_blue));
_ring.setPixelColor(p2[i]-1,_ring.Color(gl_green, gl_red, gl_blue));
}
_ring.show();
delay(del);
}
}
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
}
// NeoPIXEL ring double clockwise
/*!
\param circles amount of circles to animate
\param del delay in ms
\param color red, green, blue, white or off
*/
void IoTEcoSys_NeoPIXEL_Ring::neoRingCounterClockWise(int circles, int del, String color){
for(int c=0;c<circles;c++){
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
for(int i=0;i<16;i++){
setGlRGB(color);
_ring.setPixelColor(p1[i],_ring.Color(gl_green, gl_red, gl_blue));
if(i > -1){
setGlRGB("off");
_ring.setPixelColor(p1[i]-1,_ring.Color(gl_green, gl_red, gl_blue));
}
_ring.show();
delay(del);
}
}
changeColor(_ring.Color(0, 0, 0)); // GRB
_ring.show();
}
// NeoPIXEL ring change color
/*!
\param color color to change
*/
void IoTEcoSys_NeoPIXEL_Ring::changeColor(uint32_t color) {
for(uint16_t i=0; i < _ring.numPixels(); i++) {
_ring.setPixelColor(i, color);
_ring.show();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// I.S.I. Stuff goes here:
//
// HX711.DOUT - pin #A1
// HX711.PD_SCK - pin #A0
HX711ADC scale(A1, A0); // parameter "gain" is ommited; the default value 128 is used by the library
double scaleValue = 0.0; // Reading from ADC
////////////////////////////////////////////////////////////////////
// Diskreter Tiefpass Filter (FIR)
#define N_FILTER 16
const double A[]={0.001,0.005,0.02,0.04,0.07,0.1,0.1,0.1,0.1,0.1,0.1,0.07,0.04,0.02,0.005,0.001};
double filteredValue = 0.0;
double filter(double x) {
static bool init = false;
static double w[N_FILTER];
// Initialize w's
if(false == init) {
for(int i=0; i<N_FILTER; i++) {
w[i] = 0.0;
}
init = true;
}
for(int i=N_FILTER-1; i>0; i--) {
w[i] = w[i-1];
}
w[0] = x;
double y = 0;
for(int i=0; i<N_FILTER; i++) {
y += (A[i] * w[i]);
}
return y;
}
////////////////////////////////////////////////////////////////////
// Main Program
double deltaMin = 0.01;
double deltaResume = 0.030;
int timeLimit = 90;
int counter = 0;
int initialized = 0;
String location = "";
IoTEcoSys_NeoPIXEL_Ring ring(16);
////////////////////////////////////////////////////////////////////
// Serialization
String serialized = "0000000000000000000000000";
void SerializeData() {
serialized = String::format("%16.5f%8d%d%s", filteredValue, counter, initialized?1:0, location.c_str());
}
////////////////////////////////////////////////////////////////////
// Initialization via cloud
int Initialize(String argLocation) {
location = argLocation;
initialized = true;
return location.length();
}
void setup() {
SerializeData();
Particle.variable("serialized", serialized);
Particle.variable("weight", scaleValue);
Particle.variable("avgWeight", filteredValue);
Particle.variable("counter", counter);
Particle.variable("location", location);
Particle.function("init", Initialize);
scale.set_scale(57000.f); // Original value was 2280. Normalized to kg
scale.tare();
// Initialize Ring
ring.init();
}
unsigned long seconds = 0;
void loop() {
if(initialized) {
// Initialized, run state machines
static unsigned long lastMillis = millis();
if(millis() - lastMillis >= 500) {
Particle.syncTime();
lastMillis = millis();
static int halfSeconds = 0;
if(++halfSeconds == 2) {
halfSeconds = 0;
// Every second
seconds++;
}
// Every 500ms
ReadInputs();
}
// Update statemachines very often
StateMachineCounter();
StateMachineInfusion();
} else {
// Not initialized, blink light
uint32_t color;
color = (uint32_t)(255<<16) | (uint32_t)(255<<8) | (uint32_t)(255);
ring.changeColor(color);
delay(500);
color = 0;
ring.changeColor(color);
delay(500);
}
SerializeData();
}
////////////////////////////////////////////////////////////////////
// Input processing
void ReadInputs() {
scaleValue = scale.get_units(5);
filteredValue = filter(scaleValue);
scale.power_down(); // put the ADC in sleep mode
delay(100);
scale.power_up();
}
////////////////////////////////////////////////////////////////////
// State machine "counter"
void StateMachineCounter() {
// Machine states
static enum { INIT, COUNT } state = INIT;
// Active transition (see docs)
static int transition = 0,
previousTransition = 0;
// Global variables
// Handle states
switch (state) {
case INIT:
{
if (previousTransition) {
// Entry action
}
else {
// Do action
// Check transition conditions
transition = 1;
}
if (transition) {
// Exit action
}
}
break;
case COUNT:
{
// Local variable
static unsigned long t;
if (previousTransition) {
// Entry action
t = seconds;
}
else {
// Do action
// Check transition conditions
if (seconds - t >= 1) {
transition = 2;
}
}
if (transition) {
// Exit action
}
}
break;
}
#ifdef TESTBENCH
const char *state2str[] = {
"INIT",
"COUNT"
};
if (transition) {
PrintTimestamp();
std::cout << "StateMachine: Counter, State: " << state2str[state] << ", Transition: " << transition << std::endl;
}
#endif
// Handle transition
switch (transition) {
case 1:
// Init -> Count
{
// Perform transition action
// Change state
state = COUNT;
}
break;
case 2:
// Count -> Count
{
// Perform transition action
counter++;
// Change state
state = COUNT;
}
break;
}
previousTransition = transition;
// Deactivate transition
transition = 0;
}
////////////////////////////////////////////////////////////////////
// State machine "infusion"
void StateMachineInfusion() {
// Machine states
static enum { INIT, RUNNING, STOPPED } state = INIT;
// Active transition (see docs)
static int transition = 0,
previousTransition = 0;
// Global variables
static double m;
// Handle states
switch (state) {
case INIT:
{
if (previousTransition) {
// Entry action
}
else {
// Do action
// Check transition conditions
transition = 1;
}
if (transition) {
// Exit action
}
}
break;
case RUNNING:
{
// Local variable
if (previousTransition) {
// Entry action
m = filteredValue;
ChangeColorByTime(0);
}
else {
// Do action
// Check transition conditions
if (counter > 30) {
transition = 3;
}
if (filteredValue - m < -1.0 * deltaMin) {
transition = 2;
}
if (filteredValue - m > 0) {
transition = 6;
}
}
if (transition) {
// Exit action
}
}
break;
case STOPPED:
{
// Local variables
static unsigned long t;
if (previousTransition) {
// Entry action
m = filteredValue;
t = seconds;
}
else {
// Do action
ChangeColorByTime(seconds - t);
// Check transition conditions
static unsigned long t0 = seconds;
if (seconds - t0 >= 60) {
// Every 60 seconds
t0 = seconds;
if (filteredValue - m < -1.0 * deltaResume) {
transition = 5;
}
}
if (filteredValue - m > 50) {
transition = 4;
}
}
if (transition) {
// Exit action
counter = 0;
}
}
break;
}
#ifdef TESTBENCH
const char *state2str[] = {
"INIT",
"RUNNING",
"STOPPED"
};
if (transition) {
PrintTimestamp();
std::cout << "StateMachine: Infusion, Counter: " << counter << ", State: " << state2str[state] << ", Transition: " << transition << std::endl;
}
#endif
// Handle transition
switch (transition) {
case 1:
// Init -> Running
{
// Perform transition action
// Change state
state = RUNNING;
}
break;
case 2:
// Running -> Running
{
// Perform transition action
counter = 0;
// Change state
state = RUNNING;
}
break;
case 3:
// Running -> Stopped
{
// Perform transition action
// Change state
state = STOPPED;
}
break;
case 4:
// Stopped -> Running
{
// Perform transition action
// Change state
state = RUNNING;
}
break;
case 5:
// Stopped -> Running
{
// Perform transition action
// Change state
state = RUNNING;
}
break;
case 6:
// Running -> Running
{
// Perform transition action
counter = 0;
// Change state
state = RUNNING;
}
break;
}
previousTransition = transition;
// Deactivate transition
transition = 0;
}
////////////////////////////////////////////////////////////////////
// Time to color conversion
void ChangeColorByTime(int t) {
// Calculate color.
// Starts with green, transforms to red over yellow.
int red = 0, green = 0, blue = 0;
int maxTime = timeLimit*3/4; // Full red after this time
int currentTime = t;
if(currentTime > maxTime) {
currentTime = maxTime;
}
float f = (float)currentTime / maxTime; // convert to [0 ... 1]
float a = (/*1.f - */f)*2.f;
float x = floor(a);
float y = floor(255.f*(a-x));
int group = (int)x; // Group: 0->red 1->yellow 2->green
switch(group)
{
case 0:
red=255;
green=(int)y;
blue=0;
break;
case 1:
red=255-(int)y;
green=255;
blue=0;
break;
case 2:
red=0;
green=255;
blue=(int)y;
break;
}
uint32_t color = (uint32_t)(red<<16) | (uint32_t)(green<<8) | (uint32_t)(blue);
ring.changeColor(color);
}
#include <stdio.h>
double scaleValue = 0.0; // Reading from adc (scale)
// Diskreter Tiefpass Filter
#define N_FILTER 16
const double A[]={0.001,0.005,0.02,0.04,0.07,0.1,0.1,0.1,0.1,0.1,0.1,0.07,0.04,0.02,0.005,0.001};
double filteredValue = 0.0;
double filter(double x) {
static double w[N_FILTER] = {
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
};
for(int i=N_FILTER-1; i>0; i--) {
w[i] = w[i-1];
}
w[0] = x;
double y = 0;
for(int i=0; i<N_FILTER; i++) {
y += (A[i] * w[i]);
}
return y;
}
#include "mex.h"
void mexFunction( int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]){
int samples = mxGetNumberOfElements(prhs[0]);
double *x = mxGetData(prhs[0]);
double *y = malloc(sizeof(double) * samples);
for(int i=0; i<samples; i++) {
y[i] = filter(x[i]);
mexPrintf("%f => %f\n", x[i], y[i]);
}
plhs[0] = mxCreateDoubleMatrix(samples, 1, mxREAL);
memcpy(mxGetPr(plhs[0]), y, samples * sizeof(double));
free(y);
}
Comments