DEV Community

Nenad Ilic for IoT Builders

Posted on • Edited on

Home Treadmill with AWS IoT Greengrass and RPi 0 W

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.

Grafana Dashboard
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.

treadmill

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.

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.

2Hz Signal
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.

Reed Switch Closed
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.

Reed Switch Opened

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)


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: "*"    
Enter fullscreen mode Exit fullscreen mode

Deploy command:

aws cloudformation deploy --template-file cfn/amazon-timestream/timestream.yaml --stack-name home-treadmill-timestream --capabilities CAPABILITY_IAM
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

with the deploy command:

aws cloudformation deploy --template-file cfn/grafana/grafana-role.yaml --stack-name grafana-role --capabilities CAPABILITY_IAM
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

The output looked something like this:

"arn:aws:iam::<account id>:role/grafana-role-AmazonGrafanaServiceRoleHomeTreadmill-<id>"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Once that was done, I was able to see the new workspace and get the Grafana endpoint:

aws grafana list-workspaces
Enter fullscreen mode Exit fullscreen mode
{
    "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": {}
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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:

Grafana SpeedWith the query:

SELECT time, measure_value::double as speed FROM $__database.$__table WHERE  measure_name='speed'
Enter fullscreen mode Exit fullscreen mode

and other three to show Latest Speed:

SELECT measure_value::double as speed FROM $__database.$__table ORDER BY time DESC LIMIT 1
Enter fullscreen mode Exit fullscreen mode

Average Speed

SELECT AVG(measure_value::double) FROM $__database.$__table WHERE measure_name='speed'
Enter fullscreen mode Exit fullscreen mode

Max Speed

SELECT MAX(measure_value::double) FROM $__database.$__table WHERE measure_name='speed'
Enter fullscreen mode Exit fullscreen mode

With the end goal looking like this:
Grafana Dashboard
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)