ADCPi- how to run without being root? - python

I have a Pi Model A, running the latest version of Raspbian. Plugged into it is an ADC-Pi (https://www.abelectronics.co.uk/products/3/Raspberry-Pi/17/ADC-Pi-V2---Raspberry-Pi-Analogue-to-Digital-converter) with various analog sensors.
Running the demo code (which is below)
If I use './adc_demo.py' it works fine
If I use 'sudo python3 adc_demo.py' I get the error 'Import error: No module named quick2wire.i2c'.
What can I do so I can run it using the latter statement? I have another script that runs a motor through the GPIO pins on the pi, and that needs to be ran as root- and I'm trying to merge the two scripts together.
adc_demo.py
#!/usr/bin/env python3
# read abelectronics ADC Pi board inputs
# uses quick2wire from http://quick2wire.com/
# See http://elinux.org/RPi_ADC_I2C_Python for full setup instructions
import quick2wire.i2c as i2c
import re
import time
adc_address1 = 0x68
adc_address2 = 0x69
adc_channel1 = 0x98
adc_channel2 = 0xB8
adc_channel3 = 0xD8
adc_channel4 = 0xF8
for line in open('/proc/cpuinfo').readlines():
m = re.match('(.*?)\s*:\s*(.*)', line)
if m:
(name, value) = (m.group(1), m.group(2))
if name == "Revision":
if value [-4:] in ('0002', '0003'):
i2c_bus = 0
else:
i2c_bus = 1
break
with i2c.I2CMaster(i2c_bus) as bus:
def getadcreading(address, channel):
bus.transaction(i2c.writing_bytes(address, channel))
time.sleep(0.05)
h, l, r = bus.transaction(i2c.reading(address,3))[0]
time.sleep(0.05)
h, l, r = bus.transaction(i2c.reading(address,3))[0]
t = (h << 8) | l
v = t * 0.000154
if v < 5.5:
return v
else: # must be a floating input
return 0.00
while True:
print("1: %f" % getadcreading(adc_address1, adc_channel1))
print("2: %f" % getadcreading(adc_address1, adc_channel2))
print("3: %f" % getadcreading(adc_address1, adc_channel3))
print("4: %f" % getadcreading(adc_address1, adc_channel4))
print("5: %f" % getadcreading(adc_address2, adc_channel1))
print("6: %f" % getadcreading(adc_address2, adc_channel2))
print("7: %f" % getadcreading(adc_address2, adc_channel3))
print("8: %f" % getadcreading(adc_address2, adc_channel4))
time.sleep(1)

Related

Python Overpass API nodes

What exactly are those nodes in the line
"way(50.746,7.154,50.748,7.157) ["highway"];"
I tried to find documentation but still didn't understand.The following is a usage example
import overpy
api = overpy.Overpass()
# fetch all ways and nodes
result = api.query("""
way(50.746,7.154,50.748,7.157) ["highway"];
(._;>;);
out body;
""")
for way in result.ways:
print("Name: %s" % way.tags.get("name", "n/a"))
print(" Highway: %s" % way.tags.get("highway", "n/a"))
print(" Nodes:")
for node in way.nodes:
print(" Lat: %f, Lon: %f" % (node.lat, node.lon))

long and short strategy with macd indicator in Backtrader

I just switched from Matlab to python and even newer to the backtrader library for backtestingtrading strategies. My questions might seem obvious.
My problem seems similar to this :
https://community.backtrader.com/topic/2857/wanted-exit-long-and-open-short-on-the-same-bar-and-vice-versa
and this :
https://community.backtrader.com/topic/2797/self-close-does-not-clear-position
The code below is a simple MACD strategy.
Here is the code :
# -*- coding: utf-8 -*-
"""
"""
import backtrader as bt
import argparse
import backtrader.feeds as btFeeds
import numpy as np
import yfinance as yf
import pandas as pd
import talib
class SimpleMACDStrat(bt.Strategy):
def __init__(self):
#Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
self.order = None
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} {txt}')
#Print date and close
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if order.status in [order.Completed]:
if order.isbuy():
self.log('LONG EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# Write down: no pending order
self.order = None
def next(self):
self.log("Close: '{0}'" .format(self.data.adj_close[0]))
print('%f %f %f %f %f %f %f %f %f %f %f %f %f' % (self.data.Indexx[0],self.data.open[0],
self.data.high[0],self.data.low[0],
self.data.close[0],self.data.adj_close[0],
self.data.volume[0],self.data.EMA_100[0],
self.data.RSI[0], self.data.CCI[0],
self.data.MACD_macd[0],self.data.MACD_sign[0],self.data.MACD_hist[0]))
if self.order:
return
if self.data.MACD_hist[0]>0:
if self.position.size<0 and self.data.MACD_hist[-1]<0 :
self.close()
self.log('CLOSE SHORT POSITION, %.2f' % self.dataclose[0])
elif self.position.size==0:
self.order=self.buy()
self.log('OPEN LONG POSITION, %.2f' % self.dataclose[0])
elif self.data.MACD_hist[0]<0:
if self.position.size>0 and self.data.MACD_hist[-1]>0:
self.order=self.close()
self.log('CLOSE LONG POSITION, %.2f' % self.dataclose[0])
elif self.position.size==0:
self.order=self.sell()
self.log('OPEN SHORT POSITION, %.2f' % self.dataclose[0])
print('')
class BasicIndicatorsFeeded(btFeeds.PandasData):
lines = ('Indexx', 'adj_close', 'EMA_100', 'RSI', 'CCI', 'MACD_macd', 'MACD_sign', 'MACD_hist',)
params = ( ('Indexx', 0), ('adj_close', 5), ('volume', 6),
('EMA_100', 7), ('RSI', 8), ('CCI', 9),
('MACD_macd', 10), ('MACD_sign', 11), ('MACD_hist', 12),)
if __name__ == '__main__':
cerebro = bt.Cerebro()
#Add data feed to Cerebro
data1 = yf.download("AAPL",start="2021-08-09", end="2021-12-21",group_by="ticker")
data1.insert(0,'Indexx',' ')
data1['Indexx']=range(len(data1))
data1['EMA_100']=talib.EMA(data1['Adj Close'],100)
data1['RSI']=talib.RSI(data1['Adj Close'],14)
data1['CCI']=talib.CCI(data1['High'], data1['Low'], data1['Adj Close'], timeperiod=14)
data1['MACD_macd']=talib.MACD(data1['Adj Close'], fastperiod=12, slowperiod=26, signalperiod=9)[0]
data1['MACD_sign']=talib.MACD(data1['Adj Close'], fastperiod=12, slowperiod=26, signalperiod=9)[1]
data1['MACD_hist']=talib.MACD(data1['Adj Close'], fastperiod=12, slowperiod=26, signalperiod=9)[2]
# data1['Long_position']
# Run Cerebro Engine
cerebro.broker.setcash(8000000000)
start_portfolio_value = cerebro.broker.getvalue()
cerebro.addstrategy(SimpleMACDStrat)
data = BasicIndicatorsFeeded(dataname=data1)
cerebro.adddata(data)
cerebro.run()
cerebro.plot()
# print(data1)
print('-------------------')
#print('%f' %data)
# print(data)
end_portfolio_value = cerebro.broker.getvalue()
pnl = end_portfolio_value - start_portfolio_value
print(f'Starting Portfolio Value: {start_portfolio_value:2f}')
print(f'Final Portfolio Value: {end_portfolio_value:2f}')
print(f'PnL: {pnl:.2f}')
Here are the results :
results
On 2021-11-10, macd_hist goes from postive to negative. We are expecting that the next day( 2021-11-11):
a)the long position is closed and right after and
b)a short position is opened
1)We see that a) is actually closed the same day. Isn't it supposed to happen the next ?
2)Also a sell is executed the day after, which is not supposed to happen.
Any suggestion for 1) and 2) would be more then welcome. Thanks.
Abbe
EDIT :
Btw, I'm aware the idea can be coded that way (only def next) :
def next(self):
#print('%f' % (self.datas[0].Indexxx[0])
self.log("Close: '{0}'" .format(self.data.adj_close[0]))
print('%f %f %f %f %f %f %f %f %f %f %f %f %f' % (self.data.Indexx[0],self.data.open[0],
self.data.high[0],self.data.low[0],
self.data.close[0],self.data.adj_close[0],
self.data.volume[0],self.data.EMA_100[0],
self.data.RSI[0], self.data.CCI[0],
self.data.MACD_macd[0],self.data.MACD_sign[0],self.data.MACD_hist[0]))
if self.order:
return
print(self.position)
if self.data.MACD_hist[0]>0 and self.data.MACD_hist[-1]<0:
self.order=self.buy()
self.log('CLOSE SHORT POSITION and open long, %.2f' % self.dataclose[0])
if self.data.MACD_hist[0]<0 and self.data.MACD_hist[-1]>0:
self.order=self.sell()
self.log('CLOSE LONG POSITION and open short, %.2f' % self.dataclose[0])
print('')
But I really want to separate the
self.close()
and for instance the
self.buy()
That would allow me later to use different conditions for closing a position and opening one.
Thanks a lot for any inputs, ideas, remarks.
Abbe
In your code you are showing the following:
if self.data.MACD_hist[0]>0:
if self.position.size<0 and self.data.MACD_hist[-1]<0 :
self.close()
self.log('CLOSE SHORT POSITION, %.2f' % self.dataclose[0])
elif self.position.size==0:
self.order=self.buy()
self.log('OPEN LONG POSITION, %.2f' % self.dataclose[0])
elif self.data.MACD_hist[0]<0:
if self.position.size>0 and self.data.MACD_hist[-1]>0:
self.order=self.close()
self.log('CLOSE LONG POSITION, %.2f' % self.dataclose[0])
elif self.position.size==0:
self.order=self.sell()
self.log('OPEN SHORT POSITION, %.2f' % self.dataclose[0])
Based on the logic, it is only possible for one of these conditions to be met in next.
You are indicating that you would like to have the close and entry separate. You need to change your elif to if. Also, if you are using a criteria of self.position.size == 0, this will not happen until the close is executed, so the bar after the close, not the next one. But if you wish to have other criteria, you could enter it after another if statement.
if self.data.MACD_hist[0]>0:
if self.position.size<0 and self.data.MACD_hist[-1]<0 :
self.close()
self.log('CLOSE SHORT POSITION, %.2f' % self.dataclose[0])
#### change here ####
if SOME OTHER CONDITION:
self.order=self.buy()
self.log('OPEN LONG POSITION, %.2f' % self.dataclose[0])
...
Once you close the position, it is not really necessary to check if the position goes to 0 units. You can safely assume it will.

Stop Python script running on a nao robot touching his head

My problem is the following. I have a script in Python that is executed so that the robot does perform various actions without stopping until the execution stops, however for security reasons (in case the robot goes crazy and wants to kill us all) I need to add this instruction to stop it using the touch sensor of his head in case this is pressed.
I read a little about the ALTouch module with which the TouchChanged() module can be generated, but it acts on all the sensors (including movements) and not only with the touch sensor on the head.
Any ideas or related documentation will be welcomed.
Here´s some of my code:
class SoundProcessingModule(object):
def __init__( self, app):
ttsProxy.say("Touch my head at any moment to stop me")
super(SoundProcessingModule, self).__init__()
app.start()
session = app.session
self.audio_service = session.service("ALAudioDevice")
self.isProcessingDone = False
self.nbOfFramesToProcess = 100
self.framesCount=0
self.micFront = []
self.module_name = "SoundProcessingModule"
def startProcessing(self):
self.audio_service.setClientPreferences(self.module_name, 16000, 3, 0)
self.audio_service.subscribe(self.module_name)
while self.isProcessingDone == False:
time.sleep(1)
self.audio_service.unsubscribe(self.module_name)
def processRemote(self, nbOfChannels, nbOfSamplesByChannel, timeStamp, inputBuffer):
#ReadyToDance
postureProxy.goToPosture("StandInit", 0.5)
self.framesCount = self.framesCount + 1
if (self.framesCount <= self.nbOfFramesToProcess):
print(self.framesCount)
self.micFront=self.convertStr2SignedInt(inputBuffer)
rmsMicFront = self.calcRMSLevel(self.micFront)
print ("Nivel RMS del microfono frontal = " + str(rmsMicFront))
rmsArray.insert(self.framesCount-1,rmsMicFront)
#-40 y -30
if promedio >= -40 and promedio <= -30 :
#Some dance moves
#-29 y -20
elif promedio >= -29 and promedio <= -20:
#Some dance moves
#-19 y -11
elif promedio >= -19 and promedio <= -11:
#Some dance moves
else :
self.isProcessingDone=True
#Plot RMS signal
plt.plot(rmsArray)
plt.ylabel('RMS')
plt.xlabel('Frames')
plt.text(np.argmin(rmsArray), np.min(rmsArray) - 0.1, u'Mínimo', fontsize=10, horizontalalignment='center',
verticalalignment='center')
plt.text(np.argmax(rmsArray), np.max(rmsArray) + 0.1, u'Máximo', fontsize=10, horizontalalignment='center',
verticalalignment='center')
print("")
print ("El promedio total del sonido es: " + str(np.mean(rmsArray)))
print("El maximo total del sonido es: " + str(np.max(rmsArray)))
print("El minimo total del sonido es: " + str(np.min(rmsArray)))
plt.show()
postureProxy.goToPosture("Sit", 1.0)
def calcRMSLevel(self,data) :
rms = 20 * np.log10( np.sqrt( np.sum( np.power(data,2) / len(data) )))
return rms
def convertStr2SignedInt(self, data) :
signedData=[]
ind=0;
for i in range (0,len(data)/2) :
signedData.append(data[ind]+data[ind+1]*256)
ind=ind+2
for i in range (0,len(signedData)) :
if signedData[i]>=32768 :
signedData[i]=signedData[i]-65536
for i in range (0,len(signedData)) :
signedData[i]=signedData[i]/32768.0
return signedData
def StiffnessOn(proxy):
# We use the "Body" name to signify the collection of all joints
pNames = "Body"
pStiffnessLists = 1.0
pTimeLists = 1.0
proxy.stiffnessInterpolation(pNames, pStiffnessLists, pTimeLists)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
#Es necesario estar al pendiente de la IP del robot para moficarla
parser.add_argument("--ip", type=str, default="nao.local",
help="Robot IP address. On robot or Local Naoqi: use '127.0.0.1'.")
parser.add_argument("--port", type=int, default=9559,
help="Naoqi port number")
args = parser.parse_args()
# Inicializamos proxys.
try:
proxy = ALProxy("ALMotion", "nao.local", 9559)
except Exception, e:
print "Could not create proxy to ALMotion"
print "Error was: ", e
try:
postureProxy = ALProxy("ALRobotPosture", "nao.local", 9559)
except Exception, e:
print "Could not create proxy to ALRobotPosture"
print "Error was: ", e
try:
ttsProxy = ALProxy("ALTextToSpeech" , "nao.local", 9559)
except Exception, e:
print "Could not create proxy to ALTextToSpeech"
print "Error was: ", e
try:
memory = ALProxy("ALMemory" , "nao.local", 9559)
except Exception, e:
print "Could not create proxy to ALMemory"
print "Error was: ", e
try:
connection_url = "tcp://" + args.ip + ":" + str(args.port)
app = qi.Application(["SoundProcessingModule", "--qi-url=" + connection_url])
except RuntimeError:
print ("Can't connect to Naoqi at ip \"" + args.ip + "\" on port " + str(args.port) +".\n"
"Please check your script arguments. Run with -h option for help.")
sys.exit(1)
MySoundProcessingModule = SoundProcessingModule(app)
app.session.registerService("SoundProcessingModule", MySoundProcessingModule)
MySoundProcessingModule.startProcessing()
The robot dances according to the RMS level captured from the front mic, but I need to stop it at anytime when the head sensor (or any sensor) is touched.
Instead of subscribing to TouchChanged, you can subscribe to the head only with the 3 events (for the 3 tactile buttons):
FrontTactilTouched
MiddleTactilTouched
RearTactilTouched
They will be raised with value 1 when touch starts, and 0 when touch ends. So you will want to filter and only stop your dance when value is 1.

python myhdl package how to generate verilog initial block

From the code mostly from the sample of myhdl:
from myhdl import Signal, intbv, delay, always, now, Simulation, toVerilog
__debug = True
def ClkDriver(clk):
halfPeriod = delay(10)
#always(halfPeriod)
def driveClk():
clk.next = not clk
return driveClk
def HelloWorld(clk, outs):
counts = intbv(3)[32:]
#always(clk.posedge)
def sayHello():
outs.next = not outs
if counts >= 3 - 1:
counts.next = 0
else:
counts.next = counts + 1
if __debug__:
print "%s Hello World! outs %s %s" % (
now(), str(outs), str(outs.next))
return sayHello
clk = Signal(bool(0))
outs = Signal(intbv(0)[1:])
clkdriver_inst = ClkDriver(clk)
hello_inst = toVerilog(HelloWorld, clk, outs)
sim = Simulation(clkdriver_inst, hello_inst)
sim.run(150)
I expect it to generate a verilog program that contains an initial block, like something:
module HelloWorld(...)
reg [31:0] counts;
initial begin
counts = 32'h3
end
always #(...
How can you get the initial block generated?
Note that on the google cache for old.myhdl.org/doku.php/dev:initial_values it links to example https://bitbucket.org/cfelton/examples/src/tip/ramrom/ . So it looks the feature should be supported. However the rom sample generates static case statements. That's not what I'm looking for.
Three steps to resolve it:
Update to the latest myhdl on master or a version that contains the hash 87784ad which added the feature under issue #105 or #150. As an example for virtualenv, run a git clone, followed by pip install -e <path-to-myhdl-dir>.
Change the signal to a list.
Set toVerilog.initial_values=True before calling toVerilog.
Code snippet follows.
def HelloWorld(clk, outs):
counts = [Signal(intbv(3)[32:])]
#always(clk.posedge)
def sayHello():
outs.next = not outs
if counts[0] >= 3 - 1:
counts[0].next = 0
else:
counts[0].next = counts[0] + 1
if __debug__:
print "%s Hello World! outs %s %s %d" % (
now(), str(outs), str(outs.next), counts[0])
return sayHello
clk = Signal(bool(0))
outs = Signal(intbv(0)[1:])
clkdriver_inst = ClkDriver(clk)
toVerilog.initial_values=True
hello_inst = toVerilog(HelloWorld, clk, outs)
sim = Simulation(clkdriver_inst, hello_inst)
sim.run(150)

Adafruit ADC and Python AttributeErrors

I'm new to Python and currently working on a project on my Pi 3 mod b. I use an Adafruit ADC1015 to convert analogue signal. However, even if i have the code to get some volt measurments, i get an error of " AttributeError: 'int' object has no attribute 'readADCSingleEnded'".
To explain that, the python script i'm trying to run is the following:
#!/usr/bin/python
import time, signal, sys
from Adafruit_ADS1x15 import ADS1x15
def signal_handler(signal, frame):
print 'You pressed Ctrl+C!'
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
ADS1015 = 0x00
ADS1115 = 0x01
gain = 4096 # +/- 4.096V
sps = 250 # 250 samples per second
# Initialise the ADC using the default mode (use default I2C address)
# Set this to ADS1015 or ADS1115 depending on the ADC you are using!
adc = ADS1015(ic=ADS1015)
# Read channel 0 in single-ended mode using the settings above
volts=adc.readADCSingleEnded(0, gain, sps) / 1000
# To read channel 3 in single-ended mode, +/- 1.024V, 860 sps use:
# volts = adc.readADCSingleEnded(3, 1024, 860)
print "%.6f" % (volts)
The "ADS1x15" file we import contains the following code related to the error:
# Constructor
def __init__(self, address=0x48, ic=__IC_ADS1015, debug=False):
# Depending on if you have an old or a new Raspberry Pi, you
# may need to change the I2C bus. Older Pis use SMBus 0,
# whereas new Pis use SMBus 1. If you see an error like:
# 'Error accessing 0x48: Check your I2C address '
# change the SMBus number in the initializer below!
self.i2c = Adafruit_I2C(address)
self.address = address
self.debug = debug
# Make sure the IC specified is valid
if ((ic < self.__IC_ADS1015) | (ic > self.__IC_ADS1115)):
if (self.debug):
print "ADS1x15: Invalid IC specfied: %h" % ic
return -1
else:
self.ic = ic
# Set pga value, so that getLastConversionResult() can use it,
# any function that accepts a pga value must update this.
self.pga = 6144
def readADCSingleEnded(self, channel=0, pga=6144, sps=250):
"Gets a single-ended ADC reading from the specified channel in mV. \
The sample rate for this mode (single-shot) can be used to lower the noise \
(low sps) or to lower the power consumption (high sps) by duty cycling, \
see datasheet page 14 for more info. \
The pga must be given in mV, see page 13 for the supported values."
# With invalid channel return -1
if (channel > 3):
if (self.debug):
print "ADS1x15: Invalid channel specified: %d" % channel
return -1
# Disable comparator, Non-latching, Alert/Rdy active low
# traditional comparator, single-shot mode
config = self.__ADS1015_REG_CONFIG_CQUE_NONE | \
self.__ADS1015_REG_CONFIG_CLAT_NONLAT | \
self.__ADS1015_REG_CONFIG_CPOL_ACTVLOW | \
self.__ADS1015_REG_CONFIG_CMODE_TRAD | \
self.__ADS1015_REG_CONFIG_MODE_SINGLE
# Set sample per seconds, defaults to 250sps
# If sps is in the dictionary (defined in init) it returns the value of the constant
# othewise it returns the value for 250sps. This saves a lot of if/elif/else code!
if (self.ic == self.__IC_ADS1015):
config |= self.spsADS1015.setdefault(sps, self.__ADS1015_REG_CONFIG_DR_1600SPS)
else:
if ( (sps not in self.spsADS1115) & self.debug):
print "ADS1x15: Invalid pga specified: %d, using 6144mV" % sps
config |= self.spsADS1115.setdefault(sps, self.__ADS1115_REG_CONFIG_DR_250SPS)
# Set PGA/voltage range, defaults to +-6.144V
if ( (pga not in self.pgaADS1x15) & self.debug):
print "ADS1x15: Invalid pga specified: %d, using 6144mV" % sps
config |= self.pgaADS1x15.setdefault(pga, self.__ADS1015_REG_CONFIG_PGA_6_144V)
self.pga = pga
# Set the channel to be converted
if channel == 3:
config |= self.__ADS1015_REG_CONFIG_MUX_SINGLE_3
elif channel == 2:
config |= self.__ADS1015_REG_CONFIG_MUX_SINGLE_2
elif channel == 1:
config |= self.__ADS1015_REG_CONFIG_MUX_SINGLE_1
else:
config |= self.__ADS1015_REG_CONFIG_MUX_SINGLE_0
# Set 'start single-conversion' bit
config |= self.__ADS1015_REG_CONFIG_OS_SINGLE
# Write config register to the ADC
bytes = [(config >> 8) & 0xFF, config & 0xFF]
self.i2c.writeList(self.__ADS1015_REG_POINTER_CONFIG, bytes)
# Wait for the ADC conversion to complete
# The minimum delay depends on the sps: delay >= 1/sps
# We add 0.1ms to be sure
delay = 1.0/sps+0.0001
time.sleep(delay)
# Read the conversion results
result = self.i2c.readList(self.__ADS1015_REG_POINTER_CONVERT, 2)
if (self.ic == self.__IC_ADS1015):
# Shift right 4 bits for the 12-bit ADS1015 and convert to mV
return ( ((result[0] << 8) | (result[1] & 0xFF)) >> 4 )*pga/2048.0
else:
# Return a mV value for the ADS1115
# (Take signed values into account as well)
val = (result[0] << 8) | (result[1])
if val > 0x7FFF:
return (val - 0xFFFF)*pga/32768.0
else:
return ( (result[0] << 8) | (result[1]) )*pga/32768.0
I believed this would run smmothly, as it is a part something that is related to the ADC, but i haven't managed to solve this problem, even if i tried a lot.
Found it. Line
adc = ADS1015(ic=ADS1015)
Should be
adc = ADS1x15(ic=ADS1015)

Categories

Resources