Building a Battery Powered Smart Lamp with the Analog Discovery Pro (ADP3450/ADP3250)
In this guide, the processes of designing and building a battery-powered, Bluetooth-connected RGB lamp will be presented. The Analog Discovery Pro in Linux mode will be used to control the final project and to debug the external circuits during the testing stage. To make sure that you have the latest Linux image running on your device, follow this guide: How to Update or Recover Linux Mode on the Analog Discovery Pro (ADP3450/ADP3250)
Planning
Before starting designing the lamp, the goals of the project must be established. The central component of the lamp is an RGB LED which should be switched on/off via an application running on a phone. Communication with the phone can be resolved using Bluetooth or Bluetooth Low Energy, but the Low Energy variant (BLE) can be easier to use in some cases, due to the custom service characteristics (more on this later). To power the lamp, a battery can be used which can be charged from the Analog Discovery Pro (the lamp can't be directly powered from the ADP, as it doesn't provide enough current).
In this prototype, a 5 mm RGB LED will be used as the lamp, which is powered by an old phone battery, but the circuit can be scaled for higher power lamps if needed. Communication with the phone is resolved by the Pmod BLE (Bluetooth Low Energy). The Pmod ALS is used to provide feedback from the lamp and to resolve automatic switching. A full inventory of the components needed is listed below.
Inventory
Hardware
- a smartphone with Android
- Lithium-Polymer (LiPo) battery cell
- RGB LED
- battery charger circuit
-
- OP484 operational amplifier
- 3x 1KΩ resistor
- 3x 180Ω resistor
- 3x 47μF capacitor
- 3x 2N3904 NPN transistor
Software
- WaveForms (for debugging)
- Visual Studio Code, or any other editor of your choice
- a web browser
- a terminal emulator
Note: WaveForms can be installed by following the WaveForms Getting Started Guide.
Creating a Hardware Abstraction Layer (HAL) for the Analog Discovery Pro
A Hardware Abstraction Layer (HAL) is a layer in programming that takes the functions controlling the hardware and makes them more abstract, by generalizing them. In this case, the hardware, the Analog Discovery Pro, is controlled using WaveForms SDK, from a Python script. WaveForms SDK requires a fair amount of stuff to happen to start using an instrument. The SDK uses the ctypes module and most instruments need several initialization functions (like dummy read/write functions in the case of some digital communication protocols) before using them. As the current project uses several instruments, some sort of module is needed to make the interaction with the hardware more straightforward.
You can use the module presented in the Getting Started with WaveForms SDK guide, or you can follow the steps presented there to write your own instrument driver module.
Creating a Module for the Pmods
The Pmod drivers will be created in a similar way to the HAL, but will use it to control the hardware. Download the respective files, then continue at the next section.
As the Pmod BLE communicates via UART, the read and write functions will be implemented in two ways: one method uses the UART instrument to send and receive data, the other uses the logic analyzer and the pattern generator instruments to implement UART communication. This way the device will be able to use the “less smart” method not to interrupt PWM generation for the LEDs. The module can be downloaded here.
The Pmod ALS communicates via SPI, but along the read and write functions which use the protocol instrument, alternate functions using the static I/O instrument must be implemented for the same reasons. The module can be downloaded here.
In the attached packages, not all the functions were tested, so errors might appear in some cases. Use the package on your own responsibility and feel free to modify it.
Testing the Pmods
To make sure that the previously created Python modules works (and to provide examples for later usage), they should be tested.
- Testing the Pmod BLE
-
To be able to test the Pmod, download the BLE Scanner application to your phone. Run the Python script, then start the application. It will ask you to turn on Bluetooth and location. After some time, you should see the Pmod BLE appearing in the scanner.
Note the long code (MAC address) below the name of the device. It will be important later.
Connect to it, then open Custom Service and tap the N (notify) icon in the first custom characteristic. You should see system messages appearing in the output stream of the Python script. Also, the “got it” message should appear at the custom characteristic. To send data, tap the W icon and type in your message.
Note the long code (UUID) of the service and the characteristic you are using. It will be important later.
""" To test the Pmod BLE, all messages received on Bluetooth are displayed, the string "ok" is sent as a response. """ # import modules import Pmod_BLE as ble import WF_SDK as wf # import WaveForms instruments # define pins ble.pins.tx = 4 ble.pins.rx = 3 ble.pins.rst = 5 ble.pins.status = 6 # turn on messages ble.settings.DEBUG = True try: # initialize the interface device_data = wf.device.open() # check for connection errors wf.device.check_error(device_data) ble.open() ble.reset(rx_mode="uart", tx_mode="uart", reopen=True) ble.reboot() while True: # check connection status if ble.get_status(): # receive data data, sys_msg, error = ble.read(blocking=True, rx_mode="logic", reopen=False) # display data and system messages if data != "": print("data: " + data) # display it ble.write_data("ok", tx_mode="pattern", reopen=False) # and send response elif sys_msg != "": print("system: " + sys_msg) elif error != "": print("error: " + error) # display the error except KeyboardInterrupt: pass finally: # close the device ble.close(reset=True) wf.device.close(device_data)
- Testing the Pmod ALS
-
Use the flashlight of a phone to illuminate the sensor while the script is running. Use your hand to cover it to test it in dark.
""" To test the Pmod ALS, the ambient light intensity is read and displayed continuously. """ # import modules import Pmod_ALS as als import WF_SDK as wf # import WaveForms instruments from time import sleep # define pins als.pins.cs = 8 als.pins.sdo = 9 als.pins.sck = 10 try: # initialize the interface device_data = wf.device.open() # check for connection errors wf.device.check_error(device_data) als.open() while True: # display measurements light = als.read_percent(rx_mode="static", reopen=True) print("static: " + str(light) + "%") light = als.read_percent(rx_mode="spi", reopen=True) print("spi: " + str(light) + "%") sleep(0.5) except KeyboardInterrupt: pass finally: # close the device als.close(reset=True) wf.device.close(device_data)
Building the Application
To build an Android application, the MIT App Inventor web tool will be used. Open the site, create a new user, then start a new project. First, create the user interface of the application by drag and dropping user interface elements onto the virtual phone. Use the companion app on your real phone to see how the user interface will look. Alternatively, you can download and import the already created project file, or just download and install on your phone the final application.
- QR Code for the Application
For this application, a switch, three sliders, and several labels are needed. Use the Horizontal Arrangement and Vertical Arrangement blocks from the Layout menu to arrange everything on the screen.
When you are ready, find the Extension menu and import the Bluetooth Low Energy by MIT extension. Drag and drop a BLE component on the screen of the virtual phone.
When you are finished with the user interface, enter Block view. Here use the puzzle pieces to create the logic backbone of your application. Define what happens when the user touches a slider/switch/label, when the Bluetooth module connects/disconnects/receives a message. Define what data do you want to send and how to decode the received information. If you have never created an application before, this is a great way to start.
- Tips and Tricks
-
Use comments on the pieces (small blue circles with a question mark) to make your “code” easy to understand.
In this editor, every separate puzzle piece runs as an interrupt. Use this to your advantage.
Hiding a component and invisible components can be useful. You can use an invisible error message, which is made visible only if an error appears, then hide it again using a timer interrupt, to display connection problems - this can be extremely useful during debugging.
To easily access the BLE device, service and characteristic you need, use the MAC address, Service UUID and Characteristic UUID obtained previously. If you don't have those, you can find the addresses by following this step: Testing the Pmod BLE.
When you are ready, build the application and install it on your phone. To install it, you must enable installation from unknown sources. After installing the app, you might encounter warning messages from Google Play Protect, but just ignore them.
Hardware Setup
Connecting the PMODs
Connect the Pmod ALS and the Pmod BLE to the digital lines of the Analog Discovery Pro. Note the connections as you will have to define them in the code.
Building the LED Driver
To have a more linear control of brightness, not the voltage falling on a LED, but the current through it must be controlled. As the ADP3450 doesn't have current supplies, a voltage controlled current sink has to be built for each color of the RGB LED. The control voltages for these current sinks can be provided by three digital lines, by generating PWM signals and connecting these signals to low-pass filters. In the circuit below, the current through the LED is proportional to the duty cycle of the control signal: $I_{LED}=\frac{D}{100}\frac{V_{CC}}{R_{SET}}$. Don't forget to calculate the power dissipation on the set resistor and choose the resistor accordingly!
You can use the quad op-amp to implement all three current sinks.
Connecting the Charger Circuit
Connect the charger circuit to one USB port in the back of the Analog Discovery Pro, then to the battery and the RGB LEDs.
To be able to measure the voltage of the battery cell, connect the first oscilloscope channel to the negative lead of the battery and the second channel to the positive lead. As the scope channels have a common reference, the difference between the two measurements will give the battery voltage. To check if the charger is connected, or not, connect one scope channel to the input of the charger.
Software Setup
In the following, the structure of the main program file will be detailed. You can follow this guide to write your own control program based on the instructions given here, or you can download the script here.
To be able to use all the previously created modules, you must import them into your script.
To make your code more readable and easier to debug, it is good practice to name your connections (so you don't have to keep in mind which digital, or analog channel is used for what).
Also define every important constant at the beginning of your script. If you initialize these values at the start of your code, it will be easier to modify them during tuning the finished project.
- Importing the Modules, Defining Connections and Constants
-
""" Control an RGB LED with the ADP3450 """ # import modules import Pmod_BLE as ble import Pmod_ALS as als import WF_SDK as wf from time import time # define connections # PMOD BLE pins ble.pins.rx = 3 ble.pins.tx = 4 ble.pins.rst = 5 ble.pins.status = 6 # PMOD ALS pins als.pins.cs = 8 als.pins.sdo = 9 als.pins.sck = 10 # scope channels SC_BAT_P = 1 SC_BAT_N = 2 SC_CHARGE = 4 # LED colors LED_R = 0 LED_G = 1 LED_B = 2 # other parameters scope_average = 10 # how many measurements to average with the scope light_average = 10 # how many measurements to average with the light sensor led_pwm_frequency = 1e03 # in Hz out_data_update = 10 # output data update time [s] ble.settings.DEBUG = True # turn on messages from Pmod BLE als.settings.DEBUG = True # turn on messages from Pmod ALS DEBUG = True # turn on messages # encoding prefixes pre_red = 0b11 pre_green = 0b10 pre_blue = 0b01 pre_bat = 0b11 pre_charge = 0b10 pre_light = 0b01 class flags: red = 0 green = 0 blue = 0 last_red = -1 last_green = -1 last_blue = -1 start_time = 0
Some auxiliary functions might be needed as well, which will be used later in the script. Use these functions to make the script easier to read.
- Auxiliary Functions
-
def rgb_led(red, green, blue, device_data): """ define the LED color in precentage """ wf.pattern.generate(device_data, LED_R, wf.pattern.function.pulse, led_pwm_frequency, duty_cycle=red) wf.pattern.generate(device_data, LED_G, wf.pattern.function.pulse, led_pwm_frequency, duty_cycle=green) wf.pattern.generate(device_data, LED_B, wf.pattern.function.pulse, led_pwm_frequency, duty_cycle=blue) return """-------------------------------------------------------------------""" def decode(data): """ decode incoming Bluetooth data """ # convert to list for character in list(data): # convert character to integer character = ord(character) # check prefixes and extract content if character & 0xC0 == pre_red << 6: flags.red = round((character & 0x3F) / 0x3F * 100) elif character & 0xC0 == pre_green << 6: flags.green = round((character & 0x3F) / 0x3F * 100) elif character & 0xC0 == pre_blue << 6: flags.blue = round((character & 0x3F) / 0x3F * 100) else: pass return """-------------------------------------------------------------------""" def encode(data, prefix, lim_max, lim_min=0): """ encode real numbers between lim_min and lim_max """ try: # clamp between min and max limits data = max(min(data, lim_max), lim_min) # map between 0b000000 and 0b111111 data = (data - lim_min) / (lim_max - lim_min) * 0x3F # append prefix data = (int(data) & 0x3F) | (prefix << 6) return data except: return 0
The “body” of the script is inserted in a try-except structure. This structure runs the code sequence in the “try” block, and if an error (exception) occurs, handles it as defined in the “except” block. In this case the “except” block is empty (the script just finishes when the Ctrl+C combination is pressed), but it is followed by a “finally” block, which runs only once, when the try-except structure is exited. Here, the cleanup procedure can be executed, which will be discussed later.
To be able to use the instruments, you must initialize the HAL created previously, as well as the modules controlling the PMODs. After everything is initialized, turn off the lamp.
- Initialization
-
try: # initialize the interface device_data = wf.device.open() # check for connection errors wf.device.check_error(device_data) if DEBUG: print(device_data.name + " connected") # start the power supplies supplies_data = wf.supplies.data() supplies_data.master_state = True supplies_data.state = True supplies_data.voltage = 3.3 wf.supplies.switch(device_data, supplies_data) if DEBUG: print("power supplies started") # initialize the light sensor als.open() # initialize the Bluetooth module ble.open() ble.reboot() # turn off the lamp rgb_led(0, 0, 0, device_data) if DEBUG: print("entering main loop")
The main part of the script is run in an endless loop, which can be exited only by a keyboard interrupt. The main loop first handles the received data: the script decodes it, then sets the color and brightness of the lamp accordingly. The next section checks if the internal timer signals the end of the update period, reads the light intensity from the Pmod ALS, the battery voltage and the state of the charger (connected/disconnected), then sends these information to the application.
- Main Loop: Receiving Data
-
while True: if ble.get_status(): # process the data and set lamp color data, sys_msg, error = ble.read(blocking=True, rx_mode="logic", reopen=False) if len(data) > 0: # decode incoming data decode(data) # set the color if flags.red != flags.last_red: flags.last_red = flags.red rgb_led(flags.red, flags.green, flags.blue, device_data) if DEBUG: print("red: " + str(flags.red) + "%") elif flags.green != flags.last_green: flags.last_green = flags.green rgb_led(flags.red, flags.green, flags.blue, device_data) if DEBUG: print("green: " + str(flags.green) + "%") elif flags.last_blue != flags.blue: flags.last_blue = flags.blue rgb_led(flags.red, flags.green, flags.blue, device_data) if DEBUG: print("blue: " + str(flags.blue) + "%")
- Main Loop: Sending Data
-
if ble.get_status(): # check timing duration = time() - flags.start_time if duration >= out_data_update: # save current time flags.start_time = time() # measure the light intensity light = 0 for _ in range(light_average): light += als.read_percent(rx_mode="static", reopen=False) light /= light_average # encode and send the light intensity light = encode(light, pre_light, 100) ble.write_data(light, tx_mode="pattern", reopen=False) # read battery voltage batt_n = 0 batt_p = 0 for _ in range(scope_average): batt_n += wf.scope.measure(device_data, SC_BAT_N) batt_n /= scope_average for _ in range(scope_average): batt_p += wf.scope.measure(device_data, SC_BAT_P) batt_p /= scope_average battery_voltage = batt_p - batt_n # encode and send voltage battery_voltage = encode(battery_voltage, pre_bat, 5) ble.write_data(battery_voltage, tx_mode="pattern", reopen=False) # read charger state charger_voltage = 0 for _ in range(scope_average): charger_voltage += wf.scope.measure(device_data, SC_CHARGE) charger_voltage /= scope_average # encode and send voltage charger_voltage = encode(charger_voltage, pre_charge, 5) ble.write_data(charger_voltage, tx_mode="pattern", reopen=False) else: rgb_led(0, 0, 0, device_data)
When the script is finished, turn off the lamp, then close and reset the used instruments and disconnect from the device.
- Cleanup
-
except KeyboardInterrupt: # exit on Ctrl+C if DEBUG: print("keyboard interrupt detected") finally: if DEBUG: print("closing used instruments") # turn off the lamp rgb_led(0, 0, 0, device_data) # close PMODs ble.close(True) als.close(True) # stop and reset the power supplies supplies_data = wf.supplies.data() supplies_data.master_state = False supplies_data.state = False supplies_data.voltage = 0 wf.supplies.switch(device_data, supplies_data) wf.supplies.close(device_data) if DEBUG: print("power supplies stopped") # close device wf.device.close(device_data) if DEBUG: print("script stopped")
Setting Up the Analog Discovery Pro 3450 (Linux Mode)
To be able to run the script without a PC, you will have to boot up the Analog Discovery Pro in Linux Mode. Follow this guide to guide you through the boot process and to connect to the device with a terminal emulator: Getting Started in Linux Mode with the Analog Discovery Pro (ADP3450/ADP3250).
The next step is to connect the ADP3450 to the internet. Follow this guide for the detailed steps: Connecting the Analog Discovery Pro (ADP3450/ADP3250) to the Internet.
Python 3 is already installed on the device, but there are some additional packages, which you will have to install. First install the Python package installer (pip) with the following command (use SSH to send commands to the ADP):
sudo apt install python3-pip
Don't forget to install the HAL created for the WaveForms SDK. You can copy the necessary files in the same directory where the project files are, or you can use the following command to install it from GitHub:
sudo pip3 install git+https://github.com/Digilent/WaveForms-SDK-Getting-Started-PY#egg=WF_SDK
Run the script on boot, by entering these commands in the terminal:
sudo su cd /etc/systemd/system echo -n "" > lamp.service nano lamp.service
In the text editor, enter this snippet as the file's content (don't forget to change the path to the file):
[Unit] Description=Smart Lamp Controller [Service] ExecStart=nohup python3 /home/digilent/Smart-Lamp-Controller/Python/Lamp_Controller.py & [Install] WantedBy=multi-user.target
Save the file with Ctrl+O and exit with Ctrl+X. Enable and try the service by:
systemctl start lamp systemctl enable lamp reboot
Tuning and Testing
Start the script, then start the application on your phone. Wait until the phone discovers the PMOD BLE, then connect to it. Set the lamp color and luminosity on the sliders of the application.
If you want to modify the parameters of the script, stop it with Ctrl+C, modify the parameters, then restart the script. Averaging more measurements leads to a more stable result, with a slower update time.
Next Steps
For more information on WaveForms SDK, see its Resource Center.
For technical support, please visit the Test and Measurement section of the Digilent Forums.