====== Building a Battery Powered Smart Lamp with the Analog Discovery Pro (ADP3450/ADP3250) ======
~~TechArticle~~
{{ :test-and-measurement:analog-discovery-pro-3x50:app_front.jpeg?200 |}}
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: [[test-and-measurement:analog-discovery-pro-3x50:recover-linux-mode|]]
----
===== 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:pmodble:start|]] (Bluetooth Low Energy). The [[pmod:pmodals:start|]] 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 ==
* [[test-and-measurement:analog-discovery-pro-3x50:start|Analog Discovery Pro (ADP3450/ADP3250)]]
* a smartphone with Android
* [[pmod:pmodble:start|]]
* [[pmod:pmodals:start|]]
* Lithium-Polymer (LiPo) battery cell
* RGB LED
* battery charger circuit
* [[https://www.analog.com/en/design-center/evaluation-hardware-and-software/evaluation-boards-kits/EVAL-ADALP2000.html#eb-overview|ADALP2000 Analog Parts Kit]]
* OP484 operational amplifier
* 3x 1KΩ resistor
* 3x 180Ω resistor
* 3x 47μF capacitor
* 3x 2N3904 NPN transistor
== Software ==
* [[software:waveforms:waveforms-3:start]] (for debugging)
* [[https://code.visualstudio.com/|Visual Studio Code]], or any other editor of your choice
* a web browser
* a terminal emulator
**Note:** //WaveForms can be installed by following the [[software:waveforms:waveforms-3: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 [[test-and-measurement:guides:waveforms-sdk-getting-started|]] guide, or you can follow the steps presented there to write your own instrument driver module.
/*
In the attached package, not all the functions were tested, so errors might appear in some cases. Use the package responsibly and feel free to modify it.
Create a new folder and copy the **dwfconstants.py** file into it from WaveForms' installation path (usually //C:\Program Files (x86)\Digilent\WaveFormsSDK\samples\py\dwfconstants.py//). This file is needed as it contains all the constants needed for every instrument. Now create an initializer file and separate files for every instrument you want to use.
--> Wrapper: __init__.py #
This file imports all the other modules and also gives a name to each one of them. Global initialization and cleanup functions are also implemented here.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" WRAPPER """
# import every submodule
from WaveForms_HAL import WaveForms_HAL_Device as device
from WaveForms_HAL import WaveForms_HAL_Supply as supply
from WaveForms_HAL import WaveForms_HAL_Scope as scope
from WaveForms_HAL import WaveForms_HAL_Wavegen as wavegen
from WaveForms_HAL import WaveForms_HAL_Logic as logic
from WaveForms_HAL import WaveForms_HAL_Pattern as pattern
from WaveForms_HAL import WaveForms_HAL_Pattern_static as static_pattern
from WaveForms_HAL import WaveForms_HAL_Static as digital
from WaveForms_HAL import WaveForms_HAL_SPI as spi
from WaveForms_HAL import WaveForms_HAL_UART as uart
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
# synchronize submodules
def initialize():
# connect to the device
if device.connected != True:
device.open()
# copy device handles
supply.hdwf = device.hdwf
scope.hdwf = device.hdwf
wavegen.hdwf = device.hdwf
logic.hdwf = device.hdwf
pattern.hdwf = device.hdwf
digital.hdwf = device.hdwf
spi.hdwf = device.hdwf
uart.hdwf = device.hdwf
return
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
def close():
# close the device
if device.connected:
# reset all instruments
supply.reset()
scope.reset()
wavegen.reset()
logic.reset()
pattern.reset()
static_pattern.reset()
digital.reset()
spi.reset()
uart.reset()
device.close()
return
"""-------------------------------------------------------------------"""
<--
--> General: WaveForms_HAL_Device.py #
This file contains functions used for opening and closing the device and error checking. A flag showing the current connection status is also included here.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" MAIN FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
connected = False # the current state of the device
"""-------------------------------------------------------------------"""
""" ERROR CHECKING """
"""-------------------------------------------------------------------"""
def check_error():
# check for errors
errno = c_int() # variable for error number
dwf.FDwfGetLastError(errno) # get error number
# if there is an error
if errno != dwfercNoErc:
szerr = create_string_buffer(512) # variable for the error message
dwf.FDwfGetLastErrorMsg(szerr) # get the error message
print(str(szerr.value)) # print the error message
quit() # terminate the program
return
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
def open():
# open the first connected device
dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))
# if there is a problem
if hdwf.value == hdwfNone.value:
# check for errors
check_error()
connected = True # set connection flag
return
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
def close():
dwf.FDwfDeviceClose(hdwf) # close the device
connected = False # set connection flag
return
"""-------------------------------------------------------------------"""
<--
--> Logic Analyzer: WaveForms_HAL_Logic.py #
This file contains functions controlling the logic analyzer. Besides the initialization and cleanup functions, the most important one is the receive_data() function, which reads the states of the digital I/O lines in a buffer according to the initialization parameters. The function accepts a parameter that specifies whether data specific to a digital I/O line should be selected from the dataset or not.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" DIGITAL INPUT FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import numpy
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
data_count = 8192 # buffer size
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
def initialize(base_frequency=100e06, buffer_size=8192):
global data_count
data_count = buffer_size
# set internal clock frequency
internal_frequency = c_double()
dwf.FDwfDigitalInInternalClockInfo(hdwf, byref(internal_frequency))
dwf.FDwfDigitalInDividerSet(hdwf, c_int(
int(internal_frequency.value / base_frequency)))
# set 16-bit sample format
dwf.FDwfDigitalInSampleFormatSet(hdwf, c_int(16))
# set buffer size
dwf.FDwfDigitalInBufferSizeSet(hdwf, c_int(buffer_size))
return
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfDigitalInReset(hdwf) # reset the logic analyzer
return
"""-------------------------------------------------------------------"""
""" RECEIVE ALL DATA """
"""-------------------------------------------------------------------"""
def receive_data(channel=None):
# begin acquisition
dwf.FDwfDigitalInConfigure(hdwf, False, True)
while True:
status = c_byte()
dwf.FDwfDigitalInStatus(hdwf, True, byref(status))
if status.value == stsDone.value:
# exit loop when finished
break
# get samples
buffer = (c_uint16 * data_count)()
dwf.FDwfDigitalInStatusData(hdwf, buffer, 2 * data_count)
buffer = numpy.fromiter(buffer, dtype=numpy.uint16)
buffer = buffer.tolist()
# break out for every pin
result = [[] for _ in range(16)]
for data in buffer:
for index in range(16):
result[index].append(data & (1 << index))
# return only what is necessary
if channel != None:
return result[channel]
else:
return result
"""-------------------------------------------------------------------"""
<--
--> Pattern Generator: WaveForms_HAL_Pattern.py #
The most important part of this file is the function that generates the required signal on the required channel according to the input parameters.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" DIGITAL OUTPUT FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import WaveForms_HAL.WaveForms_HAL_Static as digital
import math
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
# function types
class type:
pulse = DwfDigitalOutTypePulse
custom = DwfDigitalOutTypeCustom
random = DwfDigitalOutTypeRandom
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfDigitalOutReset(hdwf) # reset the pattern generator
return
"""-------------------------------------------------------------------"""
""" GENERATION """
"""-------------------------------------------------------------------"""
def generate(channel, frequency, function, duty_cycle=50, data=None):
# reset pin
digital.stop(channel)
# get internal clock frequency
internal_frequency = c_double()
dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(internal_frequency))
# get counter value range
counter_limit = c_uint()
dwf.FDwfDigitalOutCounterInfo(hdwf, c_int(0), 0, byref(counter_limit))
# calculate the divider for the given frequency
divider = int(math.ceil(internal_frequency.value /
frequency / counter_limit.value))
# calculate counter steps to get the required frequency
steps = int(round(internal_frequency.value / frequency / divider))
# calculate steps for low and high parts of the period
high_steps = int(steps * duty_cycle / 100)
low_steps = int(steps - high_steps)
# enable the respective channel
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(channel), c_int(1))
# set output type
dwf.FDwfDigitalOutTypeSet(hdwf, c_int(channel), function)
# set frequency
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(channel), c_int(divider))
if function == type.pulse:
# set duty cycle
dwf.FDwfDigitalOutCounterSet(hdwf, c_int(
channel), c_int(low_steps), c_int(high_steps))
elif function == type.custom:
# format data
buffer = (c_ubyte * ((len(data) + 7) >> 3))(0)
for index in range(len(data)):
if data[index] != 0:
buffer[index >> 3] |= 1 << (index & 7)
# load data
dwf.FDwfDigitalOutDataSet(hdwf, c_int(
channel), byref(buffer), c_int(len(data)))
# start generating the signal
dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))
return
"""-------------------------------------------------------------------"""
<--
--> Static Pattern Generator: WaveForms_HAL_Pattern_static.py #
Due to hardware limitations, if one instrument is initialized which uses a certain hardware part (like the digital I/O lines), all the other instruments which use the same hardware lose control over it, so, for example, the pattern generator, the UART master, and the SPI master instruments can't be used at the same time. However, the digital I/O lines still can be controlled in a static way, by using bitmasks.
To make use of this possibility, this library realizes the pattern generator functions using the static I/O instrument instead of the pattern generator. While this module can be used at the same time as other digital instruments, it has a very limited speed, which is not enough for the scope of this project.
The current version of this module uses multithreading to realize several operations in parallel (actually the processor just switches very fast between the tasks).
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" DIGITAL OUTPUT FUNCTIONS """
# import necessary modules
import WaveForms_HAL.WaveForms_HAL_Static as digital
import time
import threading
import random
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# list for processes
processes = [None, None, None, None,
None, None, None, None,
None, None, None, None,
None, None, None, None]
# class containing all channel parameters
class channel_data:
frequency = None
duty_cycle = None
signal_data = None
function_type = None
thread = None
stop = None
# function types
class type:
pulse = 0
custom = 1
random = 2
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# stop all threads
for index in range(16):
stop(index)
# reset the instrument
digital.reset() # reset the pattern generator
return
"""-------------------------------------------------------------------"""
""" STOP GENERATION """
"""-------------------------------------------------------------------"""
def stop(channel):
global processes
# stop the generation
if processes[channel] != None:
processes[channel].stop = True
processes[channel].thread.join()
processes[channel] = None
# reset the pin
digital.stop(channel)
return
"""-------------------------------------------------------------------"""
""" SET GENERATION DATA """
"""-------------------------------------------------------------------"""
def generate(channel, frequency, function, duty_cycle=None, data=None):
global processes
# stop running thread
if processes[channel] != None:
stop(channel)
# set pin as output
digital.set_output(channel)
# save current channel data
current_ch = channel_data()
current_ch.frequency = frequency
current_ch.duty_cycle = duty_cycle
current_ch.signal_data = data
current_ch.function_type = function
current_ch.stop = False
current_ch.thread = threading.Thread(target=channel_handler, args=(channel,))
# update data table
processes[channel] = current_ch
# start thread
processes[channel].thread.start()
return
"""-------------------------------------------------------------------"""
""" CHANNEL HANDLER FUNCTION """
"""-------------------------------------------------------------------"""
def channel_handler(channel):
global processes
loop_index = 0
# start looping
while processes[channel].stop != True:
# measure start time
start_time = time.time()
if processes[channel].function_type == type.pulse:
# calculate waiting times
period = 1 / processes[channel].frequency
high_period = period * processes[channel].duty_cycle / 100
low_period = period - high_period
# generate low cycle
digital.write(channel, False)
# wait
duration = time.time() - start_time
if duration < low_period:
time.sleep(low_period - duration)
# record starting time
start_time = time.time()
# generate high cycle
digital.write(channel, True)
# wait
duration = time.time() - start_time
if duration < high_period:
time.sleep(high_period - duration)
elif processes[channel].function_type == type.custom:
# generate custom signal
data = processes[channel].signal_data
digital.write(channel, data[loop_index % len(data)])
# wait
duration = time.time() - start_time
period = 1 / processes[channel].frequency
if duration < period:
time.sleep(period - duration)
else:
# generate random signal
digital.write(channel, random.choice([True, False]))
# wait
duration = time.time() - start_time
period = 1 / processes[channel].frequency
if duration < period:
time.sleep(period - duration)
# increment loop index
loop_index += 1
return
"""-------------------------------------------------------------------"""
<--
--> Oscilloscope: WaveForms_HAL_Scope.py #
This file contains functions that can command the oscilloscope to measure voltages on a specified channel or to fill a buffer with the recorded data point according to the settings with which the instrument was initialized.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" ANALOG INPUT FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import numpy
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
# triggering flag
trigger = False
# buffer size
data_count = 8192
# possible trigger options
class trig:
# sources
class src:
no = (trigsrcNone, )
scope_ch = [(trigsrcDetectorAnalogIn, 0), (trigsrcDetectorAnalogIn, 1),
(trigsrcDetectorAnalogIn, 2), (trigsrcDetectorAnalogIn, 3)]
digital_ch = [(trigsrcDetectorDigitalIn, 0), (trigsrcDetectorDigitalIn, 1),
(trigsrcDetectorDigitalIn, 2), (trigsrcDetectorDigitalIn, 3),
(trigsrcDetectorDigitalIn, 4), (trigsrcDetectorDigitalIn, 5),
(trigsrcDetectorDigitalIn, 6), (trigsrcDetectorDigitalIn, 7),
(trigsrcDetectorDigitalIn, 8), (trigsrcDetectorDigitalIn, 9),
(trigsrcDetectorDigitalIn, 10), (trigsrcDetectorDigitalIn, 11),
(trigsrcDetectorDigitalIn, 12), (trigsrcDetectorDigitalIn, 13),
(trigsrcDetectorDigitalIn, 14), (trigsrcDetectorDigitalIn, 15)]
external_ch = [(trigsrcExternal1, ), (trigsrcExternal2, ),
(trigsrcExternal3, ), (trigsrcExternal4, )]
# types
class type:
edge = trigtypeEdge
pulse = trigtypePulse
transition = trigtypeTransition
# edges
class edge:
rising = trigcondRisingPositive
falling = trigcondFallingNegative
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
def initialize(offset=0, range=10, buffer_size=8192, frequency=20e06, trigger=False, trigger_src=trig.src.no, trigger_type=trig.type.edge, trigger_timeout=0, trigger_lvl=0, trigger_edge=trig.edge.rising):
global data_count
data_count = buffer_size
# enable all channels
dwf.FDwfAnalogInChannelEnableSet(hdwf, c_int(0), c_bool(True))
# set offset voltage
dwf.FDwfAnalogInChannelOffsetSet(hdwf, c_int(0), c_double(offset))
# set range
dwf.FDwfAnalogInChannelRangeSet(hdwf, c_int(0), c_double(range / 2))
# set the buffer size
dwf.FDwfAnalogInBufferSizeSet(hdwf, c_int(buffer_size))
# set the acquisition frequency
dwf.FDwfAnalogInFrequencySet(hdwf, c_double(frequency))
# disable averaging
dwf.FDwfAnalogInChannelFilterSet(hdwf, c_int(-1), filterDecimate)
# set up triggering
if trigger:
# enable/disable auto triggering
dwf.FDwfAnalogInTriggerAutoTimeoutSet(hdwf, c_double(trigger_timeout))
# set trigger source
dwf.FDwfAnalogInTriggerSourceSet(hdwf, trigger_src[0])
if trigger_src[1] != None:
dwf.FDwfAnalogInTriggerChannelSet(hdwf, c_int(trigger_src[1]))
# set trigger type
dwf.FDwfAnalogInTriggerTypeSet(hdwf, trigger_type)
# set trigger level
dwf.FDwfAnalogInTriggerLevelSet(hdwf, c_double(trigger_lvl))
# set trigger edge
dwf.FDwfAnalogInTriggerConditionSet(hdwf, trigger_edge)
return
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfAnalogInReset(hdwf) # reset the oscilloscope
return
"""-------------------------------------------------------------------"""
""" MEASURE VOLTAGE """
"""-------------------------------------------------------------------"""
def measure(channel):
# set up the instrument
dwf.FDwfAnalogInConfigure(hdwf, c_bool(False), c_bool(False))
# read data to buffer
dwf.FDwfAnalogInStatus(hdwf, False, None)
# extract data from buffer
voltage = c_double()
dwf.FDwfAnalogInStatusSample(hdwf, c_int(channel - 1), byref(voltage))
return voltage.value
"""-------------------------------------------------------------------"""
""" RECEIVE DATA """
"""-------------------------------------------------------------------"""
def receive(channel):
# set up the instrument
dwf.FDwfAnalogInConfigure(hdwf, c_bool(False), c_bool(True))
# read data to buffer
while True:
status = c_byte()
dwf.FDwfAnalogInStatus(hdwf, True, byref(status))
if status.value == DwfStateDone.value:
# exit loop when ready
break
# copy buffer
buffer = (c_double * data_count)()
dwf.FDwfAnalogInStatusData(hdwf, c_int(channel - 1), buffer, data_count)
# convert list
buffer = numpy.fromiter(buffer, dtype=numpy.float)
buffer = buffer.tolist()
return buffer
"""-------------------------------------------------------------------"""
<--
--> Serial Peripheral Interface (SPI) Master: WaveForms_HAL_SPI.py #
The Analog Discovery Pro can initiate SPI communication on any of the digital pins, after which it can exchange data with the connected slave device.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" SPI FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
# endianness
class bit_order:
MSB_first = 1
LSB_first = 0
# pins
class pins:
cs = None
sck = None
mosi = None
miso = None
"""-------------------------------------------------------------------"""
""" SPI INITIALIZATION """
"""-------------------------------------------------------------------"""
def initialize(cs_pin, sck_pin, miso_pin=None, mosi_pin=None, frequency=1e06, mode=0, order=bit_order.MSB_first):
# save pin numbers globally
pins.cs = cs_pin
pins.sck = sck_pin
pins.mosi = mosi_pin
pins.miso = miso_pin
dwf.FDwfDigitalSpiFrequencySet(hdwf, c_double(
frequency)) # set the clock frequency
dwf.FDwfDigitalSpiClockSet(hdwf, c_int(sck_pin)) # set the clock pin
if mosi_pin != None:
dwf.FDwfDigitalSpiDataSet(hdwf, c_int(
0), c_int(mosi_pin)) # set the mosi pin
dwf.FDwfDigitalSpiIdleSet(hdwf, c_int(
0), DwfDigitalOutIdleZet) # set the initial state
if miso_pin != None:
dwf.FDwfDigitalSpiDataSet(hdwf, c_int(
1), c_int(miso_pin)) # set the miso pin
dwf.FDwfDigitalSpiIdleSet(hdwf, c_int(
1), DwfDigitalOutIdleZet) # set the initial state
dwf.FDwfDigitalSpiModeSet(hdwf, c_int(mode)) # set the SPI mode
dwf.FDwfDigitalSpiOrderSet(hdwf, c_int(order)) # set endianness
dwf.FDwfDigitalSpiSelect(hdwf, c_int(
cs_pin), c_int(1)) # set the cs pin HIGH
dwf.FDwfDigitalSpiWriteOne(hdwf, c_int(
1), c_int(0), c_int(0)) # dummy write
return
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfDigitalSpiReset(hdwf) # reset the SPI interface
return
"""-------------------------------------------------------------------"""
""" SPI SENDING """
"""-------------------------------------------------------------------"""
def send(data):
# cast data
if type(data) == int:
data = "".join(chr(data))
elif type(data) == list:
data = "".join(chr(element) for element in data)
# enable the chip select line
dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(0))
# create buffer to write
data = (c_ubyte * len(data))(*[c_ubyte(ord(character)) for character in data])
dwf.FDwfDigitalSpiWrite(hdwf, c_int(1), c_int(8), data, c_int(
len(data))) # write array of 8 bit elements
# disable the chip select line
dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(1))
return
"""-------------------------------------------------------------------"""
""" SPI RECEIVING """
"""-------------------------------------------------------------------"""
def receive(count):
# enable the chip select line
dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(0))
# create buffer to store data
buffer = (c_ubyte*count)()
dwf.FDwfDigitalSpiRead(hdwf, c_int(1), c_int(8), buffer, c_int(
len(buffer))) # read array of 8 bit elements
# disable the chip select line
dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(1))
# decode data
data = [str(bin(element))[2:] for element in buffer]
data = [int(element, 2) for element in data]
data = "".join(chr(element) for element in data)
return data
"""-------------------------------------------------------------------"""
""" SPI EXCHANGE """
"""-------------------------------------------------------------------"""
def exchange(data, count):
# cast data
if type(data) == int:
data = "".join(chr(data))
elif type(data) == list:
data = "".join(chr(element) for element in data)
# enable the chip select line
dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(0))
# create buffer to write
tx_buff = (c_ubyte * len(data)).from_buffer_copy(data)
# create buffer to store data
rx_buff = (c_ubyte*count)()
dwf.FDwfDigitalSpiWriteRead(hdwf, c_int(1), c_int(8), tx_buff, c_int(
len(tx_buff)), rx_buff, c_int(len(rx_buff))) # write to MOSI and read from MISO
# decode data
data = [str(bin(element))[2:] for element in rx_buff]
data = [int(element, 2) for element in data]
data = "".join(chr(element) for element in data)
# disable the chip select line
dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(1))
return data
"""-------------------------------------------------------------------"""
<--
--> Static I/O: WaveForms_HAL_Static.py #
This module can read/write the state of a digital I/O line and can set the line as input, or output.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" DIGITAL I/O FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfDigitalIOReset(hdwf) # reset the digital I/O
return
"""-------------------------------------------------------------------"""
""" RESET PIN """
"""-------------------------------------------------------------------"""
def stop(pin):
# set pin to LOW
write(pin, False)
# disable static I/O
set_output(pin, False)
return
"""-------------------------------------------------------------------"""
""" READING A DIGITAL PIN """
"""-------------------------------------------------------------------"""
def read(pin):
# read a digital pin
# load internal buffer with current state of the pins
dwf.FDwfDigitalIOStatus(hdwf)
data = c_uint32() # variable for this current state
# get the current state of the pins
dwf.FDwfDigitalIOInputStatus(hdwf, byref(data))
# convert the state to a 16 character binary string
data = list(bin(data.value)[2:].zfill(16))
# check the required bit
if data[15 - pin] != "0":
# if it is one, return True (HIGH)
return True
else:
# else return False (LOW)
return False
"""-------------------------------------------------------------------"""
""" SETTING A DIGITAL PIN AS OUTPUT """
"""-------------------------------------------------------------------"""
def set_output(pin, state=True):
# set a pin as output
# load current state of the output enable buffer
mask = c_uint16()
dwf.FDwfDigitalIOOutputEnableGet(hdwf, byref(mask))
# set bit mask
mask = set_mask(pin, mask, state)
# set the pin to output
dwf.FDwfDigitalIOOutputEnableSet(hdwf, c_int(mask))
return
"""-------------------------------------------------------------------"""
""" WRITING A DIGITAL PIN """
"""-------------------------------------------------------------------"""
def write(pin, state):
# set a pin as output
# load current state of the output state buffer
mask = c_uint16()
dwf.FDwfDigitalIOOutputGet(hdwf, byref(mask))
# set bit mask
mask = set_mask(pin, mask, state)
# set the pin state
dwf.FDwfDigitalIOOutputSet(hdwf, c_int(mask))
return
"""-------------------------------------------------------------------"""
""" SET MASK """
"""-------------------------------------------------------------------"""
def set_mask(pin, mask, value):
# convert to list
mask = list(bin(mask.value)[2:].zfill(16))
# set bit
if value:
mask[15 - pin] = "1"
else:
mask[15 - pin] = "0"
# convert to number
mask = "".join(element for element in mask)
mask = int(mask, 2)
return mask
"""-------------------------------------------------------------------"""
<--
--> Supplies: WaveForms_HAL_Supply.py #
This module can turn on/off the 3.3V power supply
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" POWER SUPPLY FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfAnalogIOReset(hdwf) # reset the power supplies
return
"""-------------------------------------------------------------------"""
""" COMMANDING THE POWER SUPPLIES """
"""-------------------------------------------------------------------"""
def switch(state):
# start/stop the power supplies
# set the output voltage to 3.3V
dwf.FDwfAnalogIOChannelNodeSet(hdwf, c_int(0), c_int(0), c_double(3.3))
# start/stop the supplies
dwf.FDwfAnalogIOEnableSet(hdwf, c_int(state))
return
"""-------------------------------------------------------------------"""
<--
--> Universal Asynchronous Receiver-Transmitter (UART) Master: WaveForms_HAL_UART.py #
This module can be used to initialize UART communication on any of the digital I/O lines, then send/receive data on those lines.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" UART FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
# possible parity settings
class parity:
none = 0
odd = 1
even = 2
"""-------------------------------------------------------------------"""
""" UART INITIALIZATION """
"""-------------------------------------------------------------------"""
def initialize(rx_pin, tx_pin, baud_rate=9600, parity=parity.none, data_bits=8, stop_bits=1):
dwf.FDwfDigitalUartRateSet(hdwf, c_double(baud_rate)) # set baud rate
dwf.FDwfDigitalUartTxSet(hdwf, c_int(tx_pin)) # set tx pin
dwf.FDwfDigitalUartRxSet(hdwf, c_int(rx_pin)) # set rx pin
dwf.FDwfDigitalUartBitsSet(hdwf, c_int(data_bits)) # set data bit count
# set parity bit requirements
dwf.FDwfDigitalUartParitySet(hdwf, c_int(parity))
dwf.FDwfDigitalUartStopSet(hdwf, c_double(stop_bits)) # set stop bit count
# initialize tx with idle levels
reset_tx()
reset_rx()
return
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfDigitalUartReset(hdwf) # reset the UART interface
return
"""-------------------------------------------------------------------"""
""" RESET RX """
"""-------------------------------------------------------------------"""
def reset_rx():
# initialize rx (dummy receive)
dummy_buffer = c_int(0)
dummy_parity_flag = c_int(0)
dwf.FDwfDigitalUartRx(hdwf, None, c_int(0), byref(dummy_buffer), byref(
dummy_parity_flag))
return
"""-------------------------------------------------------------------"""
""" RESET TX """
"""-------------------------------------------------------------------"""
def reset_tx():
# initialize tx with idle level (dummy send)
dwf.FDwfDigitalUartTx(hdwf, None, c_int(0))
return
"""-------------------------------------------------------------------"""
""" UART SENDING """
"""-------------------------------------------------------------------"""
def send(data):
# cast data
if type(data) == int:
data = "".join(chr(data))
elif type(data) == list:
data = "".join(chr(element) for element in data)
# encode the string into a string buffer
data = create_string_buffer(data.encode("UTF-8"))
# send text, trim zero ending
dwf.FDwfDigitalUartTx(hdwf, data, c_int(sizeof(data)-1))
return
"""-------------------------------------------------------------------"""
""" UART RECEIVING """
"""-------------------------------------------------------------------"""
def receive(chunk="", error="no error"):
data = create_string_buffer(8193) # create empty string buffer
count = c_int(0) # character counter
parity_flag = c_int(0) # parity check result
dwf.FDwfDigitalUartRx(hdwf, data, c_int(
sizeof(data)-1), byref(count), byref(parity_flag)) # read up to 8k characters
if count.value > 0:
data[count.value] = 0 # add zero ending
# make a string from the string buffer
data = list(data.value)
data = "".join(chr(element) for element in data)
# attach previous data
data = chunk + data
# decode parity check results
if parity_flag.value == 0 and error == "no error":
parity_flag = "no error"
elif parity_flag.value < 0:
parity_flag = "buffer overflow"
elif parity_flag.value > 0:
parity_flag = "parity error: {}".format(parity_flag.value)
else:
parity_flag = error
# propagate previous results
data, parity_flag = receive(chunk=data, error=parity_flag)
else:
data = chunk
parity_flag = error
return data, parity_flag
"""-------------------------------------------------------------------"""
<--
--> Waveform Generator: WaveForms_HAL_Wavegen.py #
This module can be used to generate analog signals with the required parameters. Possible functions are also present in the module as class members.
""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
""" ANALOG OUTPUT FUNCTIONS """
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
dwf = cdll.LoadLibrary("libdwf.so")
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
# global variables
hdwf = c_int() # device handle
# function types
class type:
custom = funcCustom
sine = funcSine
square = funcSquare
triangle = funcTriangle
noise = funcNoise
dc = funcDC
pulse = funcPulse
trapezium = funcTrapezium
sine_power = funcSinePower
class ramp:
up = funcRampUp
down = funcRampDown
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
def reset():
# reset the instrument
dwf.FDwfAnalogOutReset(hdwf) # reset the waveform generator
return
"""-------------------------------------------------------------------"""
""" GENERATION """
"""-------------------------------------------------------------------"""
def generate(channel, function, amplitude, frequency=1e03, symmetry=50, offset=0, data=None, run_time=0, wait_time=0, repeat_time=0):
# enable channel
channel -= 1
dwf.FDwfAnalogOutNodeEnableSet(
hdwf, channel, AnalogOutNodeCarrier, c_bool(True))
# set function type
dwf.FDwfAnalogOutNodeFunctionSet(
hdwf, channel, AnalogOutNodeCarrier, function)
if function == type.custom:
dwf.FDwfAnalogOutNodeDataSet(
hdwf, channel, AnalogOutNodeCarrier, data, len(data))
# set frequency
dwf.FDwfAnalogOutNodeFrequencySet(
hdwf, channel, AnalogOutNodeCarrier, c_double(frequency))
# set amplitude
dwf.FDwfAnalogOutNodeAmplitudeSet(
hdwf, channel, AnalogOutNodeCarrier, c_double(amplitude))
# set offset
if function == type.dc:
dwf.FDwfAnalogOutNodeOffsetSet(
hdwf, channel, AnalogOutNodeCarrier, c_double(amplitude))
else:
dwf.FDwfAnalogOutNodeOffsetSet(
hdwf, channel, AnalogOutNodeCarrier, c_double(offset))
# set symmetry
dwf.FDwfAnalogOutNodeSymmetrySet(
hdwf, channel, AnalogOutNodeCarrier, c_double(symmetry))
# set running time limit
dwf.FDwfAnalogOutRunSet(hdwf, channel, c_double(run_time))
# set wait time before start
dwf.FDwfAnalogOutWaitSet(hdwf, channel, c_double(wait_time))
# set number of repeating cycles
dwf.FDwfAnalogOutRepeatSet(hdwf, channel, c_int(repeat_time))
# start
dwf.FDwfAnalogOutConfigure(hdwf, channel, c_bool(True))
return
"""-------------------------------------------------------------------"""
<--
*/
----
===== 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 {{ :test-and-measurement:analog-discovery-pro-3x50:pmod_ble.zip |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 {{ :test-and-measurement:analog-discovery-pro-3x50:pmod_als.zip |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 [[https://play.google.com/store/apps/details?id=com.macdom.ble.blescanner|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.
{{ :test-and-measurement:analog-discovery-pro-3x50:ble_test_device.jpeg?200 |}}
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.
{{ :test-and-measurement:analog-discovery-pro-3x50:ble_test_services.jpeg?200 |}}
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 [[https://appinventor.mit.edu/|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 {{ :test-and-measurement:analog-discovery-pro-3x50:digilent_lamp_controller_project.zip |project file}}, or just download and install on your phone the final {{ :test-and-measurement:analog-discovery-pro-3x50:digilent_lamp_controller_apk.zip |application}}.
--> QR Code for the Application #
{{ :test-and-measurement:analog-discovery-pro-3x50:smart-lamp-controller-apk.png?700 |}}
<--
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 [[https://appinventor2.droidim.com/mdocs-posts/bluetooth-low-energy-by-mit|Bluetooth Low Energy by MIT]] extension. Drag and drop a BLE component on the screen of the virtual phone.
{{ :test-and-measurement:analog-discovery-pro-3x50:app_builder_front.png?600 |}}
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.
{{ :test-and-measurement:analog-discovery-pro-3x50:app_builder_back.png?600 |}}
--> 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: [[test-and-measurement:analog-discovery-pro-3x50:smart-lamp#testing_the_modules|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.
{{ :test-and-measurement:analog-discovery-pro-3x50:pmods_to_adp3450_v2.png?400 |}}
=== 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.
{{ :test-and-measurement:analog-discovery-pro-3x50:charger_to_adp3450.png?600 |}}
/*
=== Connecting the RGB LED ===
Finally, connect the RGB LED (and the current limiting resistors calculated to your LED) between the positive lead of the battery and the remaining three MOSFETs of the Pmod OD1. In this circuit, only RGB LEDs with separate anodes and cathodes, or with a common anode and separate cathodes can be used.
{{ :test-and-measurement:analog-discovery-pro-3x50:rgb_led_bb.png?600 |}}
*/
----
===== 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 {{ :test-and-measurement:analog-discovery-pro-3x50:lamp_controller.zip |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: [[test-and-measurement:analog-discovery-pro-3x50:linux-mode|]].
The next step is to connect the ADP3450 to the internet. Follow this guide for the detailed steps: [[test-and-measurement:analog-discovery-pro-3x50:connect-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 [[software:waveforms:waveforms-sdk:start|Resource Center]].
For technical support, please visit the [[https://forum.digilent.com/forum/8-test-and-measurement/|Test and Measurement]] section of the Digilent Forums.