Showing posts with label module. Show all posts
Showing posts with label module. Show all posts

Wednesday, August 8, 2018

Espressif ESP32 Tutorial - IR Remote Control using Microsoft Azure




The Project


This tutorial will outline how to create an IR Remote using the ESP32 and then control it from the IoT hub on Microsoft Azure.

Driving an IR remote transmitter using an Arduino is simple, as there is a library, called IRremote.h which does all the hard work. You just need to connect your IR transmitter module signal pin to the appropriate Arduino pin, via a current limiting resistor and you are done. Connecting an Arduino to the cloud takes a bit more work (depending on the model you are using), which is why we wanted to use the ESP32.

Unfortunately, the standard IRRemote.h Arduino library only supports receiving IR signals on the ESP32 not transmitting them. Fortunately, Andreas Spiess has forked the standard library and added ESP32 transmission capability. You will need to download the ESP32-IRremote library, so we can use it with the ESP32. Andreas did this by using ledC PWM. You can now select any pin to use with IRsend(pin). Note this is only for the ESP32, the other board types have defined pins you have to use due to the assigned timers.

The Duinotech Infrared Transmitter Module


The IR transmitting module which I used is the one from Jaycar (branded Duinotech). There is a data sheet available on the Jaycar site but it is fairly sparse and doesn't clearly define the pins on the module.

It appears that this module is based on the KY-005 INFRARED TRANSMITTER MODULE. The specifications for which are:


Operating Voltage 5V
Forward Current 20 ~ 60 mA
Power Consumption 90mW
Operating Temperature -25°C to 80°C [-13°F to 176°F]
Dimensions  18.5mm x 15mm [0.728in x 0.591in]

This being the case, the pin out is as follows:



The signal pin is clearly labeled with an S, the middle pin is GND via a resistor (* which you have to fit yourself to the module) and GND is connected to the third pin (with the "-" adjacent to it). This module is just an infrared diode (which emits at a wavelength of 940 nm).

Thus, we can drive it like any other diode via a current limiting resistor. The value of the resistor depends on what voltage your micro controller digital outputs (DO) are switching, the desired diode forward current and the forward voltage drop characteristic of the diode. So for our design:

VDO = 3.3V
If = 20 mA
Vf = 1.2V (nominally 1.1V but I measured this using my LCR meter)

Then, R = (VDO - Vf) / If
              = (3.3 - 1.2) / 0.02
              = 105 Ω



We will use a 100 Ω resistor in our circuit.



The CIR (Commercial Infrared) Transmission Protocol


As there are usually other sources of infrared radiation (e.g. sunlight and incandescent or LED lights), the 940nm IR transmitter is modulated by a carrier frequency in the 32-40 kHz range. CIR receivers incorporate a bandpass filter tuned to this carrier frequency. This allows the receiver to discriminate between the modulated IR signal and any ambient, unmodulated IR. In effect, the IR receiver is double tuned both to the wavelength of the IR radiation and to the carrier frequency.



Three factors influence CIR range. In order of decreasing importance they are: the power level of the IR emitter, the IR wavelength, and the carrier frequency. An IR emitter's output is proportional to the current through the emitter. Increasing the current will increase the power. Because the duty cycle is usually 50% or less, the emitter can be driven with quite high currents. For optimal range, the IR wavelength of the emitter and receiver should match.



A similar protocol to CIR is IrDA. IrDA was popular in the late 1990's but has largely been replaced by Bluetooth and WiFi. IrDA was designed to be very short range (< 1 m). It does not use any secondary carrier but directly modulates the 850nm IR with the data. Because of this, it is susceptible to interference from ambient IR. In addition, the IrDA transmitter is usually lower power than a CIR transmitter.

An IrDA transmitter with a CIR receiver is a mismatch as is a CIR transmitter with a IrDA receiver. They operate on a different wavelength (940 nm vs 850 nm), IrDA isn't modulated, and the beam angles are different (IrDA limits the beam angle to ±15° while most CIR emitters are ±40° or greater). Such mismatches have major effects on range and reliability.

We will use CIR in our design.

For RF control, both the transmitter and receiver need to be tuned to the same carrier frequency and need to use the same type of modulation. Most RF remotes use ASK (Amplitude Shift Keying) or OOK (On-Off Keying). OOK is really just a special case of ASK. OOK is also called CPCA (Carrier Present, Carrier Absent). You can have a look at the IRremote library to see how this coding is achieved.

Microsoft Azure




Azure is Microsoft's catch all name for their cloud services. It covers over 100 different services. The service of interest to us is IoT Hub. You can use Azure IoT Hub to securely connect, monitor and manage billions of devices to develop Internet of Things (IoT) applications. To get started we will connect just one device!

You will need to sign up for a free Azure account. Follow the link above and do this. For some reason Microsoft make you provide credit card details, even for the free account. Note that when you sign up, the email address you provide becomes the name of the default active directory (which wouldn't be my first preference).

It will be interesting comparing the Microsoft IoT hub functionality with node-red, which is another dashboard option that we have had experience with. At this stage I suspect that node-red is much cheaper (free) and simpler but Azure is more robust, secure and scalable. The key features of IoT hub are:
  1. Bidirectional communication with LOTS of devices. Use device-to-cloud telemetry data to understand the state of your devices and define message routes to other Azure services without writing any code. In cloud-to-device messages, reliably send commands and notifications to your connected devices – and track message delivery with acknowledgement receipts. Device messages are sent in a durable way to accommodate intermittently connected devices.
  2. Authentication per device. Set up individual identities and credentials for each of your connected devices, and help retain the confidentiality of both cloud-to-device and device-to-cloud messages. To maintain the integrity of your system, selectively revoke access rights for specific devices as needed.
  3. Automated device registration. Speed up your IoT deployment by registering and provisioning devices with zero touch in a secure and scalable way. IoT Hub Device Provisioning Service supports any type of IoT device compatible with IoT Hub.
  4. Use IoT Edge. Take advantage of IoT Edge to make hybrid cloud and edge solutions. IoT Edge provides orchestration between code and services so they flow securely between cloud and edge to distribute intelligence across a range of devices. Enable artificial intelligence and other advanced analytics at the edge.
Once you have signed up for Azure, you will be presented with a dashboard similar to that shown above.

Create an IoT Hub




Microsoft call their menus "blades" in Azure. No idea why, maybe because it sounds cooler than menu? Anyway, click on the + Create a resource link on the blade to the left of the dashboard. This will open the Azure Marketplace.

In the Marketplace, click on Internet of Things. This will provide a new list of menu options to the right.


We want IoT Hub at the top. Click on this to setup your hub. For subscription select Free Trial and for Resource Group, Create new.

The free tier is intended for testing and evaluation. It allows 500 devices to be connected to the IoT hub and up to 8,000 messages per day. Each Azure subscription can create one IoT Hub in the free tier.

A resource group is a container that holds related resources for an Azure solution. The resource group can include all the resources for the solution, or only those resources that you want to manage as a group.

Select the Region closest to your location. In Australia the options are East and Southeast, which I think refer to Sydney and Melbourne respectively.

To create an IoT hub, you must name the IoT hub. This name must be unique across all IoT hubs. The IoT hub will be publicly discoverable as a DNS endpoint, so make sure to avoid any sensitive information while naming it. Once created, the name can't be changed.

Click the button at the bottom labelled - Next: Size and Scale >>



For pricing and scale tier, select F1: Free tier. That is all that you can adjust on this screen. Click Review + create.

When all previous steps are complete, you can create the IoT hub. Click Create to start the back-end process to create and deploy the IoT hub with the options you chose.

It can take a few minutes to create the IoT hub as it takes time for the back-end deployment to run on the appropriate location servers. Once your new IoT resource has been created, you can customise your dashboard.


Add an IoT Device



Before a device or module can connect to your IoT hub, there must be an entry for that device in the IoT hub's identity registry. A device must also authenticate with the IoT hub based on credentials stored in the identity registry. The device or module ID stored in the identity registry is case-sensitive.

To add a new IoT device, click on + Add, and the Add Device blade will be displayed.

  • Device ID: A case-sensitive string (up to 128 characters long) of ASCII 7-bit alphanumeric characters.
  • Authentication Type: Symmetric Key or X.509 Certificate. I used Symmetric Key. The differences are:
    • Symmetric Key: a unique identity key (security tokens) for each device, which can be used by the device to communicate with the IoT Hub.
    • X.509 Certificate: uses an on-device X.509 certificate and private key as a means to authenticate the device to the IoT Hub. This authentication method ensures that the private key on the device is not known outside the device at any time, providing a higher level of security.
  • Auto Generate Keys: tick.
  • Connect device to IoT hub: enable.

Click on Save, and your new device will be added to the hub. Click on the device ID of the newly added device to see the security keys and connection strings. You will need the device ID and a copy of the primary connection string for insertion into your ESP32 sketch. Now onto the ESP32.

ESP32 Software


You can download a copy of my ESP32 sketch from the Reefwing Gist.  You will need to fill in your SSID, password and primary connection string where indicated.



I spent quite a bit of time trying different tool chains to get everything configured and talking. My initial preference was to use Eclipse with the Arduino tool chain. This would give me a proper IDE and a remote control library that I knew was compatible with all the Arduino's out there.

Unfortunately, importing custom libraries is a bit problematic for the two Eclipse Arduino plug-ins available. It is theoretically possible but I ran out of patience trying to get it to work. The ESP-IDF has its own remote control library but it is not as well documented as the Arduino library. I'm also not familiar with coding the ESP32's natively.



While looking for a way to receive the messages sent from the ESP32 to the cloud I discovered a plug in for Visual Studio Code. It so happens that there is also a plug in for Arduino. Since I was already using this to monitor my IoT hub traffic, I decided to give it a crack with programming the ESP32. It just worked - I was astonished! It does use the Arduino IDE tool chain, so that may be why it was so seamless as I had already got everything working with that first. If IntelliSense complains about a missing library, right click on the light build and edit the c_cpp_properties.json file which contains the include path.




The other thing you will probably have to do is add:

"output": "../build",

To the .vscode/arduino.json file, which can be found under the work space for your sketch. The same location as the c_cpp_properties.json file.  If output is not set, Arduino will create a new temporary output folder each time it compiles your sketch, which means it cannot reuse the intermediate result of the previous build, leading to long verify/upload time. So it is recommended to set the field. Arduino requires that the output path should not be the workspace itself or subfolder of the workspace, otherwise, it may not work correctly. By default, this option is not set. Again, no idea why, you will get a warning if it isn't set when you verify.



Whether you use the Arduino IDE or Visual Studio Code, you need to download the Azure IoT library: ESP32_AzureIoT - An Azure IoT Hub library for ESP32 devices in Arduino.  Unzip and copy this to your Arduino libraries folder.

As a first test, load the GetStarted.ino sketch from the examples folder in the library you just downloaded. This sketch will connect to the IoT hub and continuously send messages containing fake data. You will need to fill in the following blanks in the sketch:

  • DEVICE_ID - copy from your IoT registered device;
  • connectionString - copy from the primary connection string;
  • ssid - the displayed name for your WiFi network; and
  • password - for your WiFi network.

Connect to your ESP32, check the port and board type, then compile and upload the sketch. Open up the serial monitor at 115,200 baud so that you can see what is happening. The monitor should be displaying something like the following.


To confirm that the IoT hub is receiving these messages have a look at the Azure dashboard and you should see these messages arriving.


If you have Visual Studio Code, you can use the Azure IoT extension to monitor messages to your IoT hub. Just select your device and then right click and Start monitoring D2C (Device to Cloud) message. You can also send messages from the cloud to your device from here.


Once this was working, I updated the code to just send a heart beat message back to the cloud, letting us know that it was still alive. Every time the ESP32 does this, it broadcasts an IR remote control code three times. This is the usual methodology for remote controls. Currently it is just broadcasting the Sony power code, but we will look at ways we can start/stop the broadcast and change the code via Azure.

Cloud to Device Message Lifecycle




To guarantee at-least-once message delivery, IoT Hub persists cloud-to-device messages in per-device queues. Devices must explicitly acknowledge completion for IoT Hub to remove them from the queue. This approach guarantees resiliency against connectivity and device failures.

When the IoT Hub service sends a message to a device, the service sets the message state to Enqueued. When a device wants to receive a message, IoT Hub locks the message (by setting the state to Invisible), which allows other threads on the device to start receiving other messages. When a device thread completes the processing of a message, it notifies IoT Hub by completing the message. IoT Hub then sets the state to Completed.

The max delivery count property on IoT Hub determines the maximum number of times a message can transition between the Enqueued and Invisible states. After that number of transitions, IoT Hub sets the state of the message to Dead lettered.

The diagram above shows the lifecycle state graph for a cloud-to-device message in IoT Hub. Luckily, sending messages is a lot more straight forward than implementing the message lifecycle.

Controlling the ESP32 via Azure


Now that we have our ESP32 talking to Azure and broadcasting an IR code burst every 10 seconds we want to be able to control this via the cloud. The easiest way to do this is using Visual Studio Code again.


If you right click on the device, shown in Explorer under Azure IOT HUB DEVICES, then the window above is displayed. The two ways we will look at communicating with our device via the cloud is:

  1. Cloud to Device (C2D) Messaging; and
  2. Triggering a defined device method.
You can try out both.

Cloud to Device Messaging


After uploading the sketch to your ESP32, open the serial monitor from the Arduino IDE. I found the serial monitor function in Visual Studio Code was a bit dodgy. Select Send C2D (Cloud to Device) Message to Device, and a message entry window will open. Type in whatever you want and hit return.

In the Azure IoT Toolkit output window you should see:

[C2DMessage] Sending message to [ESP32_IRBeacon_1] ...
[C2DMessage] [Success] Message sent to [ESP32_IRBeacon_1]

A second or so later, the following will appear in the Serial Monitor:

Info: >>>Received Message [1], Size=14 Message test message
Message callback:
test message

The function which handles message handling in the ESP32 code is MessageCallback(const char* payLoad, int size). The call back function is set during setup() using:

Esp32MQTTClient_SetMessageCallback(MessageCallback);


Invoke a Direct Method


This is the way that I chose to control the IR beacon (since that is its purpose). You can define what methods you want to support in your code. Currently we are only handling start and stop but it would be trivial to add another method to set the IR code transmitted.

The process for invoking a method is the same as for sending a C2D message. Right click on your device in Explorer and select "Invoke Direct Method". A text entry window will open, type in your method name (e.g. stop) and hit enter. In the Azure IoT Toolkit output window you should see:

[DirectMethod] Invokeing Direct Method [stop] to [ESP32_IRBeacon_1] ...
[DirectMethod] Invokeing Direct Method [start] to [ESP32_IRBeacon_1] ...

Yes whoever wrote this code couldn't spell invoking! Then in the Serial monitor:

Info: Try to invoke method stop
Info: Stop sending IR burst and heart beat

The function which handles method handling in the ESP32 code is DeviceMethodCallback(). This call back function is set during setup() using:

Esp32MQTTClient_SetDeviceMethodCallback(DeviceMethodCallback);


Conclusion




If all you want is a dashboard for your IoT application then Node-Red is MUCH simpler to implement. If you need an industrial strength solution then you need something like Azure IoT hub.

Actually, using Visual Studio Code (VSC) was a pleasure, and this will be my go to IDE for Arduino from now on. Controlling and Monitoring your IoT devices via VSC was also very easy once you work out how it operates. The user interface is not very discoverable, you need to hit F1 to access most of the Arduino and Azure commands.

I will publish a short follow up article providing a simple PCB to mount the diagnostic LED's, IR transmitting module and the ARM/DEBUG switch. It is a simple enough circuit that you could do it on a breadboard or veroboard. Note that the Duinotech ESP32 doesn't leave any pins free on one side of a standard breadboard, due to its width. See the photo above. This is one reason I decided to use a PCB.


Friday, March 17, 2017

HC-SR04 Ultrasonic Sensor Python Class for Raspberry Pi

The HC-SR04




The HC - SR04 ultrasonic ranging module provides 2cm - 400cm non-contact
measurement, with ranging accuracy up to 3mm. The module includes ultrasonic transmitters, receiver and control circuitry. The time difference between transmission and reception of ultrasonic signals is calculated. Using the speed of sound and ‘Speed = Distance/Time‘ equation, the distance between the source and target can be easily calculated.

Credit to Vivek and his article on the same subject for the diagrams.




Wiring the HC-SR04 to a Raspberry Pi


The module has 4 pins:

  • VCC - 5V Supply
  • TRIG - Trigger Pulse Input
  • ECHO - Echo Pulse Output
  • GND - 0V Ground 

Wiring is straight forward with one exception, note that the sensor operates at 5V not the 3.3V of the Raspberry Pi. Connecting the ECHO pulse pin directly to the Raspberry Pi would be a BAD idea and could damage the Pi. We need to use a voltage divider or a logic level converter module to drop the logic level from the HC-SR04 to a maximum of 3.3V. Current draw for the sensor is 15 mA.

As we have a spare logic level converter, we will use that. Connections for the logic converter are shown below.


For the voltage divider option: Vout = Vin x R2/(R1+R2) = 5 x 10000/(4700 + 10000) = 3.4V






Python Class for the HC-SR04 Ultrasonic Sensor



To utilise the HC-SR04:

  1. Provide a trigger signal to TRIG input, it requires a HIGH signal of at least 10μS duration.
  2. This enables the module to transmit eight 40KHz ultrasonic bursts.
  3. If there is an obstacle in-front of the module, it will reflect those ultrasonic waves
  4. If the signal comes back, the ECHO output of the module will be HIGH for a duration of time taken for sending and receiving ultrasonic signals. The pulse width ranges from 150μS to 25mS depending upon the distance of the obstacle from the sensor and it will be about 38ms if there is no obstacle.
  5. Obstacle distance = (high level time × velocity of sound (343.21 m/s at sea level and 20°C) / 2
  6. Allow at least 60 ms between measurements.





Time taken by the pulse is actually for return travel of the ultrasonic signals. Therefore Time is taken as Time/2.

Distance = Speed * Time/2

Speed of sound at sea level = 343.21 m/s or 34321 cm/s

Thus, Distance = 17160.5 * Time (unit cm).

As we are using the ultrasonic sensor with our Raspberry Pi robot, we have created a python class that can be easily imported and used. Note the calibration function which can be used to help correct for things like altitude and temperature.

We have included a simple low pass filter function which is equivalent to an exponentially weighted moving average. This is useful for smoothing the distance values returned from the sensor. The higher the value of beta, the greater the smoothing.

#!/usr/bin/python
# RS_UltraSonic.py - Ultrasonic Distance Sensor Class for the Raspberry Pi 
#
# 15 March 2017 - 1.0 Original Issue
#
# Reefwing Software
# Simplified BSD Licence - see bottom of file.

import RPi.GPIO as GPIO
import os, signal

from time import sleep, time

# Private Attributes
__CALIBRATE      = "1"
__TEST           = "2"
__FILTER         = "3"
__QUIT           = "q"

class UltraSonic():
    # Ultrasonic sensor class 
    
    def __init__(self, TRIG, ECHO, offset = 0.5):
        # Create a new sensor instance
        self.TRIG = TRIG
        self.ECHO = ECHO
        self.offset = offset                             # Sensor calibration factor
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.TRIG, GPIO.OUT)                  # Set pin as GPIO output
        GPIO.setup(self.ECHO, GPIO.IN)                   # Set pin as GPIO input

    def __str__(self):
        # Return string representation of sensor
        return "Ultrasonic Sensor: TRIG - {0}, ECHO - {1}, Offset: {2} cm".format(self.TRIG, self.ECHO, self.offset)

    def ping(self):
        # Get distance measurement
        GPIO.output(self.TRIG, GPIO.LOW)                 # Set TRIG LOW
        sleep(0.1)                                       # Min gap between measurements        
        # Create 10 us pulse on TRIG
        GPIO.output(self.TRIG, GPIO.HIGH)                # Set TRIG HIGH
        sleep(0.00001)                                   # Delay 10 us
        GPIO.output(self.TRIG, GPIO.LOW)                 # Set TRIG LOW
        # Measure return echo pulse duration
        while GPIO.input(self.ECHO) == GPIO.LOW:         # Wait until ECHO is LOW
            pulse_start = time()                         # Save pulse start time

        while GPIO.input(self.ECHO) == GPIO.HIGH:        # Wait until ECHO is HIGH
            pulse_end = time()                           # Save pulse end time

        pulse_duration = pulse_end - pulse_start 
        # Distance = 17160.5 * Time (unit cm) at sea level and 20C
        distance = pulse_duration * 17160.5              # Calculate distance
        distance = round(distance, 2)                    # Round to two decimal points

        if distance > 2 and distance < 400:              # Check distance is in sensor range
            distance = distance + self.offset
            print("Distance: ", distance," cm")
        else:
            distance = 0
            print("No obstacle")                         # Nothing detected by sensor
        return distance

    def calibrate(self):
        # Calibrate sensor distance measurement
        while True:
            self.ping()
            response = input("Enter Offset (q = quit): ")
            if response == __QUIT:
                break;
            sensor.offset = float(response)
            print(sensor)
            
    @staticmethod
    def low_pass_filter(value, previous_value, beta):
        # Simple infinite-impulse-response (IIR) single-pole low-pass filter.
        # ß = discrete-time smoothing parameter (determines smoothness). 0 < ß < 1
        # LPF: Y(n) = (1-ß)*Y(n-1) + (ß*X(n))) = Y(n-1) - (ß*(Y(n-1)-X(n)))
        smooth_value = previous_value - (beta * (previous_value - value))
        return smooth_value
        

def main():
    sensor = UltraSonic(8, 7)       # create a new sensor instance on GPIO pins 7 & 8
    print(sensor)

    def endProcess(signum = None, frame = None):
        # Called on process termination. 
        if signum is not None:
            SIGNAL_NAMES_DICT = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n )
            print("signal {} received by process with PID {}".format(SIGNAL_NAMES_DICT[signum], os.getpid()))
        print("\n-- Terminating program --")
        print("Cleaning up GPIO...")
        GPIO.cleanup()
        print("Done.")
        exit(0)

    # Assign handler for process exit
    signal.signal(signal.SIGTERM, endProcess)
    signal.signal(signal.SIGINT, endProcess)
    signal.signal(signal.SIGHUP, endProcess)
    signal.signal(signal.SIGQUIT, endProcess)

    while True:
        action = input("\nSelect Action - (1) Calibrate, (2) Test, or (3) Filter: ")

        if action == __CALIBRATE:
            sensor.calibrate()
        elif action == __FILTER:
            beta = input("Enter Beta 0 < ß < 1 (q = quit): ")
            filtered_value = 0
            if beta == __QUIT:
                break;
            while True:
                filtered_value = sensor.low_pass_filter(sensor.ping(), filtered_value, float(beta))
                filtered_value = round(filtered_value, 2)
                print("Filtered: ", filtered_value, " cm")
        else:
            sensor.ping()

if __name__ == "__main__":
    # execute only if run as a script
    main()

## Copyright (c) 2017, Reefwing Software
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##
## 1. Redistributions of source code must retain the above copyright notice, this
##   list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright notice,
##   this list of conditions and the following disclaimer in the documentation
##   and/or other materials provided with the distribution.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
## ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



Friday, February 17, 2017

Python Servo Module/Library for Raspberry Pi

Python Servo Library


We will now create a servo wrapper class which will add some convenience methods and attributes using software PWM. It will be designed so that it can be imported into other code or used by itself to calibrate or test a servo.

From here we will start putting together a Python Robot class similar to what we did for AVA in C++.

This library is based on the code we used to calibrate the sensor and still has some jitter. In due course we will look at improving the code using hardware DMA (perhaps using the pigpio library this time).

The code is well commented, so with no further ado, here it is.

# RS_Servo.py - Wrapper Servo Class for Raspberry Pi
#
# 15 February 2017 - 1.0 Original Issue
# 18 February 2017 - 1.1 Modified with @property
#
# Reefwing Software

import RPi.GPIO as GPIO
from time import sleep

# Private Attributes
__CALIBRATE      = "1"
__SET_DUTY_CYCLE = "2"
__SCAN           = "3"
__QUIT           = "q"

class Servo:
    # Servo class wrapper using RPi.GPIO PWM

    # Servo private class attribute - to count servo instances
    __number = 0

    def __init__(self, pin, min_dc=2, max_dc=10, freq=50):
        # Create a new servo instance with default pulse width limits if not provided

        # Configure the Pi to use pin names (i.e. BCM) and allocate I/O
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(pin, GPIO.OUT)

        # Create PWM channel on the servo pin with a frequency of freq - default 50Hz
        self.PWM = GPIO.PWM(pin, freq)

        # Increment Servo instances
        type(self).__number += 1     
        
        # Instance attributes
        self.pin = pin
        self.min_duty_cycle = min_dc
        self.max_duty_cycle = max_dc
        self.duty_cycle = self.get_centre(min_dc, max_dc)
        self.angle = 0

    def __str__(self):
        # Return string representation of servo
        return "Servo: pin - {0}, MIN_DC - {1}, MAX_DC - {2}, DC - {3}".format(self.pin, self.min_duty_cycle, self.max_duty_cycle, self.duty_cycle)

    def start(self):
        # Start PWM
        self.PWM.start(self.duty_cycle)

    def stop(self):
        # Stop PWM
        self.PWM.stop()

    def centre(self):
        # Move servo to the centre position
        centre = self.get_centre(self.min_duty_cycle, self.max_duty_cycle)
        self.duty_cycle = centre

    def min_dc(self):
        # Move servo to minimum duty cycle position
        self.duty_cycle = self.min_duty_cycle

    def max_dc(self):
        # Move servo to maximum duty cycle position
        self.duty_cycle = self.max_duty_cycle

    def scan(self, min_dc=None, max_dc=None):
        # Scans from min_dc to max_dc - defaults to max and min duty cycle
        min_dc = (min_dc or self.min_duty_cycle)
        max_dc = (max_dc or self.max_duty_cycle)
        centre = self.get_centre(min_dc, max_dc)
        self.duty_cycle = min_dc
        sleep(1)
        self.duty_cycle = centre
        sleep(1)
        self.duty_cycle = max_dc
        sleep(1)
        self.duty_cycle = centre
        sleep(1)

    def cal_duty_cycle(self, dc):
        # Set duty cycle for servo - not clamped
        self.PWM.ChangeDutyCycle(dc)

    def cleanup(self):
        # Stop PWM channel for servo and centre
        self.centre()
        sleep(1)
        self.stop()

    @property
    def duty_cycle(self):
        return self.__duty_cycle

    @duty_cycle.setter
    def duty_cycle(self, dc):
        # Set duty cycle for servo - clamped to max and min duty cycle
        dc = self.clamp(dc, self.min_duty_cycle, self.max_duty_cycle)
        self.PWM.ChangeDutyCycle(dc)
        self.__duty_cycle = dc

    @staticmethod
    def get_centre(min_dc, max_dc):
         return min_dc + (max_dc - min_dc)/2

    @staticmethod
    def clamp(dc, min_dc, max_dc):
        return max(min(dc, max_dc), min_dc)
    
    @classmethod
    def count(cls):
        # Returns the number of Servo instances
        return cls.__number

def main():
    try:
        servo = Servo(6)    # create a new servo to be controlled from GPIO pin 5
        servo.start()       # start PWM for servo

        print(servo)
        print("Servo instances: ", Servo.count())

        while True:
            test = input("\nSelect Action - (1) Calibrate, (2) Set max/min duty cycle, or (3) Scan: ")

            if test == __CALIBRATE:
                while True:
                    response = input("Enter Duty Cycle (q = quit): ")
                    if response == __QUIT:
                        break;
                    servo.cal_duty_cycle(float(response))
            elif test == __SET_DUTY_CYCLE:
                min_dc = float(input("Enter minimum duty cycle: "))
                max_dc = float(input("Enter maximum duty cycle: "))
                servo.min_duty_cycle = min_dc
                servo.max_duty_cycle = max_dc
                print(servo)
            else:
                while True:
                    # Scan Servo from min duty cycle to max duty cycle
                    print("\nScanning (CTRL c to exit)...")
                    servo.scan()
            
    except KeyboardInterrupt:
        print("\n-- CTRL-C: Terminating program --")
    finally:
        print("Cleaning up PWM and GPIO...")
        servo.cleanup()
        GPIO.cleanup()
        print("Done.")

if __name__ == "__main__":
    # execute only if run as a script
    main()