This setup was inspired by a blog post Using GitHub Actions for Greengrass v2 Continuous Deployment after a talk at IoT Builders Session. After the discussion, I wanted to try out the Greengrass Continues Deployment setup using the Github Actions and realized I had the treadmill, which I might be able to retrofit for this purpose.
In the setup, I'll use the same principles for deploying a Greengrass component with GitHub Actions and OIDC, which will allow me to develop and test a Greengrass component that publishes treadmill speed measurements to AWS IoT Core and saves them in Amazon Timestream using IoT Rules. After that, I'll configure an Amazon Managed Grafana to visualize and analyze how far I've run as well as my current, maximum, and average speed.
As of now I’m at 20.5 km/h max speed 🐌
The Hardware Setup
Personally, I’ve been trying to increase my sprinting speed and have been training on a curved treadmill, which doesn’t have any motors or require a power supply. One of the reasons I like running on this type of self-powered treadmill is that it allows me to run naturally on the balls of my feet. This motion creates momentum to turn the treadmill belt by pushing your body forward. The point of contact is significantly ahead of the center of mass; thus, the experience of support is different than with other non-motorized treadmill options and more similar to that of running on the ground.
So after the previously mentioned talk with Nathan, I thought it would be nice to have a dashboard that tells me how much I’ve run and provides me with my current and maximal speed.
I’ve noticed that there is a magnetic reed-switch that is triggered on each revolution of the conveyor drum, thus making the speed measurement setup easy to wire, so I decided to give it a try and setup my Greengrass CI/CD based on the mentioned blog and Github. Additionally, I had a spare Raspberry Pi Zero W with a battery module laying around, and I wanted to try using it with Greengrass.
However, I needed to figure out how to determine the running speed based on the number of revolutions per second. In order to do this, I had to use the original speed meter display that came with my treadmill and simulate reed switch impulses by providing a 1Hz signal.
This gave me a running (walking) speed of 2.2 km/h. I then went on to test other frequencies ranging from 2 Hz to 17 Hz.
The conclusion was that, as expected, there was a linear distribution between number of revolutions and treadmill speed, and the factor of conversion is 2.2, which made the code in RPi quite easy to write. Before jumping into the code, I had to wire the reed switch and connect it properly to the Raspberry using the wiring diagram below.
In this setup, every time a magnet crosses near the reed switch, I get a falling edge signal from 3.3V to 0V; thus, the time between falling edges gives one revolution of the conveyor drum.
Once the magnet rotates further, the voltage rises back to 3.3V, and we have a rising edge that will stay there for the next cycle until the magnet's magnetic field interacts with the reed switch, as shown in the image above.
The Speed Measurement Code
The code below can be used to get the average speed over a period of time and print it out. This is the initial version I’ve used to compare the results measured by the RPi and the original speed meter display, but as mentioned above, it measures the time between two falling edges and divides this by the determined conversion factor in order to get the current speed.
import time
import RPi.GPIO as GPIO
BUTTON_GPIO = 18
# 1Hz is 2.2km/h
CONVERSION_FACTOR = 2.2
start = time.time()
# Used to calculate average speed
speeds = deque([0, 0, 0, 0, 0])
# Callback on the signal falling edge
def reed_switch_callback(channel):
global start
# Elapsed time between previous falling edge
elapsed = time.time() - start
start = time.time()
speed = CONVERSION_FACTOR / elapsed
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON_GPIO, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(BUTTON_GPIO, GPIO.FALLING, callback=reed_switch_callback, bouncetime=10)
while True:
no_activity = time.time() - start
if no_activity > 1:
# if there is no activity for more then 1s
# this means we are bellow 2.2km/h so we add 0km/h measurement
speeds.append(0)
speeds.popleft()
av_speed = sum(speeds) / len(speeds)
print(f'average speed: {av_speed}')
time.sleep(0.5)
The Greengrass Setup
Since I've been running this on a Raspberry Pi Zero W, before installing Greengrass I had to install openjdk-8-jre
as newer versions of java will not run on the ARMv6 instruction set:
sudo apt update
sudo apt install openjdk-8-jre
After this, I had to run the following on the device, while making sure I had AWS Credentials setup on the device before running it, as instructed here.
curl -s https://d2s8p88vqu9w66.cloudfront.net/releases/greengrass-nucleus-latest.zip > greengrass-nucleus-latest.zip && unzip greengrass-nucleus-latest.zip -d GreengrassCore
export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
export AWS_REGION="eu-west-1"
export GREENGRASS_THING_GROUP="home"
export GREENGRASS_THING_NAME="home-treadmill"
# For more: https://docs.aws.amazon.com/greengrass/v2/developerguide/getting-started.html#install-greengrass-v2
sudo -E java \
-Droot="/greengrass/v2" \
-Dlog.store=FILE -jar ./GreengrassCore/lib/Greengrass.jar \
--aws-region ${AWS_REGION} \
--thing-name ${GREENGRASS_THING_NAME} \
--thing-group-name ${GREENGRASS_THING_GROUP} \
--thing-policy-name GreengrassV2IoTThingPolicy \
--tes-role-name GreengrassV2TokenExchangeRole \
--tes-role-alias-name GreengrassCoreTokenExchangeRoleAlias \
--component-default-user ggc_user:ggc_group \
--provision true \
--setup-system-service true \
--deploy-dev-tools true
This sets up the Greengrass on the device with the appropriate Thing Group and Thing Name, in this scenario, home
and home-treadmill
respectively.
As for the Greengrass components continuous deployment (CD) part where I use Github Actions, I’ve followed the instructions provided in the blog post, and extended the application to publish the speed measurements to the AWS IoT topic home/treadmill/speed
using the awsiot.greengrasscoreipc.client
.
The extended code looks like this:
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0
import time
import traceback
import json
import signal
import sys
import RPi.GPIO as GPIO
import time
from collections import deque
import awsiot.greengrasscoreipc
import awsiot.greengrasscoreipc.client as client
from awsiot.greengrasscoreipc.model import (
IoTCoreMessage,
QOS,
PublishToIoTCoreRequest
)
BUTTON_GPIO = 18
# 1Hz is 2.2km/h
CONVERSION_FACTOR = 2.2
start = time.time()
# Used to calculate average speed
speeds = deque([0, 0, 0, 0, 0])
TIMEOUT = 10
ipc_client = awsiot.greengrasscoreipc.connect()
topic = "home/treadmill/speed"
qos = QOS.AT_MOST_ONCE
def signal_handler(sig, frame):
GPIO.cleanup()
sys.exit(0)
def reed_switch_callback(channel):
global start
# Elapsed time between previous falling edge
elapsed = time.time() - start
start = time.time()
speed = CONVERSION_FACTOR / elapsed
# Discard the noise as we can't measure speed above 36
if speed < 36:
speeds.append(speed)
speeds.popleft()
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON_GPIO, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(BUTTON_GPIO, GPIO.FALLING, callback=reed_switch_callback, bouncetime=10)
signal.signal(signal.SIGINT, signal_handler)
while True:
no_activity = time.time() - start
if no_activity > 1:
# if there is no activity for more then 1s
# this means we are bellow 2.2km/h so we add 0km/h measurement
speeds.append(0)
speeds.popleft()
av_speed = sum(speeds) / len(speeds)
if av_speed > 0:
print(f'average speed: {av_speed}')
request = PublishToIoTCoreRequest()
request.topic_name = topic
message = json.dumps({
'time': time.time(),
'speed': av_speed
})
request.payload = bytes(message, "utf-8")
request.qos = qos
operation = ipc_client.new_publish_to_iot_core()
operation.activate(request)
future_response = operation.get_response()
future_response.result(TIMEOUT)
time.sleep(0.5)
Additionally, I’ve tried to compare my measurements back with the readings from the original speed meter display and got the same precession, which meant that even with the overhead of Greengrass, the system was running just fine.
Using AWS IoT Rules
In order to route the speed measurements coming from the device directly to the Timestream Database Table, I’ve used the AWS IoT Rules with the following select rule:
SELECT speed FROM "home/treadmill/speed" WHERE speed>0
and had the Amazon Timestream Database Table as an Action.
This will interpret the values from a JSON payload message having speed
as a key and put the value in the selected Timestream table.
Here is the Cloudformation stack that sets this up:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
HomeTreadmillRule:
Type: AWS::IoT::TopicRule
Properties:
RuleName: "HomeTreadmill"
TopicRulePayload:
RuleDisabled: false
Sql: SELECT speed FROM "home/treadmill/speed" WHERE speed>0
Actions:
- Timestream:
DatabaseName: "home-treadmill"
TableName: "measurments"
Dimensions:
- Name: "Speed"
Value: 0
RoleArn: !GetAtt HomeTreadmillTopicRuleRole.Arn
HomeTreadmillDB:
Type: AWS::Timestream::Database
Properties:
DatabaseName: "home-treadmill"
HomeTreadmillTable:
Type: AWS::Timestream::Table
Properties:
DatabaseName: !Ref HomeTreadmillDB
RetentionProperties:
MemoryStoreRetentionPeriodInHours: "24"
MagneticStoreRetentionPeriodInDays: "7"
TableName: "measurments"
HomeTreadmillTopicRuleRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- iot.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: AllowTimestream
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- timestream:WriteRecords
Resource:
- !GetAtt HomeTreadmillTable.Arn
- Effect: Allow
Action:
- timestream:DescribeEndpoints
Resource: "*"
Deploy command:
aws cloudformation deploy --template-file cfn/amazon-timestream/timestream.yaml --stack-name home-treadmill-timestream --capabilities CAPABILITY_IAM
Grafana Setup
There are several ways to set up Grafana and use it to gain insights from Timestream table measurements; however, the simplest for me was to create a workspace using Amazon Managed Grafana.
I intend to use it for other projects and give multiple users access to Grafana, which is part of my AWS IAM Identity Center.
To do this, I had to create a role that allows reading from the Timestream:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
AmazonGrafanaServiceRoleHomeTreadmill:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- grafana.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonTimestreamReadOnlyAccess"
Outputs:
RoleAwsAccountId:
Value: !Ref AWS::AccountId
RoleAwsRegion:
Value: !Ref AWS::Region
WorkspaceRoleToAssume:
Value: !GetAtt AmazonGrafanaServiceRoleHomeTreadmill.Arn
with the deploy command:
aws cloudformation deploy --template-file cfn/grafana/grafana-role.yaml --stack-name grafana-role --capabilities CAPABILITY_IAM
and the get the Role ARN in order to use it to create a workspace:
aws cloudformation describe-stacks --stack-name grafana-role --query "Stacks[0].Outputs[0].OutputValue"
The output looked something like this:
"arn:aws:iam::<account id>:role/grafana-role-AmazonGrafanaServiceRoleHomeTreadmill-<id>"
which I’ve used to create a Grafana workspace that is authenticated by AWS_SSO in the current account:
aws grafana create-workspace --account-access-type CURRENT_ACCOUNT --authentication-providers AWS_SSO --permission-type SERVICE_MANAGED --workspace-data-sources TIMESTREAM --workspace-role-arn <ARN from grafana-role stack>
Once that was done, I was able to see the new workspace and get the Grafana endpoint:
aws grafana list-workspaces
{
"workspaces": [
{
"authentication": {
"providers": [
"AWS_SSO"
]
},
"created": "...",
"description": "",
"endpoint": "<id>.grafana-workspace.<region>.amazonaws.com",
"grafanaVersion": "8.4",
"id": "...",
"modified": "...",
"name": "home",
"notificationDestinations": [],
"status": "ACTIVE",
"tags": {}
}
]
}
At this point, everything is preconfigured to create my dashboard, with Timestream as my source. The first panel is used to graph the speed changes:
SELECT time, measure_value::double as speed FROM $__database.$__table WHERE measure_name='speed'
and other three to show Latest Speed:
SELECT measure_value::double as speed FROM $__database.$__table ORDER BY time DESC LIMIT 1
Average Speed
SELECT AVG(measure_value::double) FROM $__database.$__table WHERE measure_name='speed'
Max Speed
SELECT MAX(measure_value::double) FROM $__database.$__table WHERE measure_name='speed'
With the end goal looking like this:
After running on my treadmill I can see the data coming in in real-time which will allow me to measure my progress and hopefully increase my maxim running speed.
Conclusion
As this started just as an inspiration from an IoT Builder Session I’m super happy how the whole process in setting this up lasted just couple of hours and at the end I got something that I’ll be using almost everyday. Additionally, having managed services to help me out on this journey meant I could have just focused on the RPi code and not worry about the complexity of the infrastructure.
If you are interested in more info about the project, or want to try out something similar, take a look at the repo in our IoT Builders Github organisation: https://github.com/aws-iot-builder-tools/ggv2-home-treadmill-example. As well as find a video that follows this blog here: https://youtu.be/o6GhNFYXZKY
Additionally if you have questions, fell free to reach out on LinkedIn or Twitter.
Top comments (0)