Skip to content

Commit 972bc1e

Browse files
committed
initial code from original github.com/sanworks/pulsepal repo [develop branch] added for integration with pybpod-api com arcom implementation
1 parent beacfbb commit 972bc1e

File tree

3 files changed

+250
-9
lines changed

3 files changed

+250
-9
lines changed
File renamed without changes.

pypulsepal/pulsepal.py

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
from ArCOM import ArCOMObject
2+
import math
3+
4+
class PulsePalObject(object):
5+
def __init__(self, PortName):
6+
self.OpMenuByte = 213
7+
self.model = 0
8+
self.dac_bitMax = 0;
9+
self.Port = ArCOMObject(PortName, 115200)
10+
# Handshake to confirm connectivity
11+
self.Port.write((self.OpMenuByte,72), 'uint8')
12+
Handshake = self.Port.read(1, 'uint8')
13+
if Handshake != 75:
14+
raise PulsePalError('Error: incorrect handshake returned.')
15+
FirmwareVersion = self.Port.read(1, 'uint32')
16+
if FirmwareVersion < 20:
17+
self.model = 1;
18+
self.dac_bitMax = 255;
19+
else:
20+
self.model = 2;
21+
self.dac_bitMax = 65535;
22+
self.firmwareVersion = FirmwareVersion
23+
if self.firmwareVersion == 20:
24+
print("Notice: NOTE: A firmware update is available. It fixes a bug in Pulse Gated trigger mode when used with multiple inputs.")
25+
print("To update, follow the instructions at https://sites.google.com/site/pulsepalwiki/updating-firmware")
26+
self.Port.write((self.OpMenuByte,89,80,89,84,72,79,78), 'uint8') # Client name op + 'PYTHON' in ASCII
27+
self.cycleFrequency = 20000;
28+
self.isBiphasic = [float('nan'),0, 0, 0, 0];
29+
self.phase1Voltage = [float('nan'),5, 5, 5, 5];
30+
self.phase2Voltage = [float('nan'),-5, -5, -5, -5];
31+
self.restingVoltage = [float('nan'),0, 0, 0, 0];
32+
self.phase1Duration = [float('nan'),0.001, 0.001, 0.001, 0.001];
33+
self.interPhaseInterval = [float('nan'),0.001, 0.001, 0.001, 0.001];
34+
self.phase2Duration = [float('nan'),0.001, 0.001, 0.001, 0.001];
35+
self.interPulseInterval = [float('nan'),0.01, 0.01, 0.01, 0.01];
36+
self.burstDuration = [float('nan'),0, 0, 0, 0];
37+
self.interBurstInterval = [float('nan'),0, 0, 0, 0];
38+
self.pulseTrainDuration = [float('nan'),1, 1, 1, 1];
39+
self.pulseTrainDelay = [float('nan'),0, 0, 0, 0];
40+
self.linkTriggerChannel1 = [float('nan'),1, 1, 1, 1];
41+
self.linkTriggerChannel2 = [float('nan'),0, 0, 0, 0];
42+
self.customTrainID = [float('nan'),0, 0, 0, 0];
43+
self.customTrainTarget = [float('nan'),0, 0, 0, 0];
44+
self.customTrainLoop = [float('nan'),0, 0, 0, 0];
45+
self.triggerMode = [float('nan'),0, 0];
46+
self.outputParameterNames = ['isBiphasic', 'phase1Voltage','phase2Voltage', 'phase1Duration', 'interPhaseInterval',
47+
'phase2Duration','interPulseInterval', 'burstDuration', 'interBurstInterval', 'pulseTrainDuration',
48+
'pulseTrainDelay', 'linkTriggerChannel1', 'linkTriggerChannel2', 'customTrainID',
49+
'customTrainTarget', 'customTrainLoop', 'restingVoltage']
50+
self.triggerParameterNames = ['triggerMode']
51+
def setFixedVoltage(self, channel, voltage):
52+
voltage = math.ceil(((voltage+10)/float(20))*self.dac_bitMax) # Convert volts to bytes
53+
if self.model == 1:
54+
self.Port.write((self.OpMenuByte, 79, channel, voltage), 'uint8')
55+
else:
56+
self.Port.write((self.OpMenuByte, 79, channel), 'uint8', voltage, 'uint16')
57+
# Receive acknowledgement
58+
ok = self.Port.read(1, 'uint8')
59+
if len(ok) == 0:
60+
raise PulsePalError('Error: Pulse Pal did not return an acknowledgement byte after a call to setFixedVoltage.')
61+
def programOutputChannelParam(self, paramName, channel, value):
62+
originalValue = value
63+
if isinstance(paramName, str):
64+
paramCode = self.outputParameterNames.index(paramName)+1
65+
else:
66+
paramCode = paramName
67+
68+
if 2 <= paramCode <= 3 or paramCode == 17:
69+
value = math.ceil(((value+10)/float(20))*self.dac_bitMax) # Convert volts to bits
70+
if self.model == 1:
71+
self.Port.write((self.OpMenuByte,74,paramCode,channel,value),'uint8')
72+
else:
73+
self.Port.write((self.OpMenuByte,74,paramCode,channel),'uint8',value,'uint16')
74+
elif 4 <= paramCode <= 11:
75+
self.Port.write((self.OpMenuByte,74,paramCode,channel),'uint8',value*self.cycleFrequency,'uint32')
76+
else:
77+
self.Port.write((self.OpMenuByte,74,paramCode,channel,value),'uint8')
78+
# Receive acknowledgement
79+
ok = self.Port.read(1, 'uint8')
80+
if len(ok) == 0:
81+
raise PulsePalError('Error: Pulse Pal did not return an acknowledgement byte after a call to programOutputChannelParam.')
82+
# Update the PulsePal object's parameter fields
83+
if paramCode == 1:
84+
self.isBiphasic[channel] = originalValue
85+
elif paramCode == 2:
86+
self.phase1Voltage[channel] = originalValue
87+
elif paramCode == 3:
88+
self.phase2Voltage[channel] = originalValue
89+
elif paramCode == 4:
90+
self.phase1Duration[channel] = originalValue
91+
elif paramCode == 5:
92+
self.interPhaseInterval[channel] = originalValue
93+
elif paramCode == 6:
94+
self.phase2Duration[channel] = originalValue
95+
elif paramCode == 7:
96+
self.interPulseInterval[channel] = originalValue
97+
elif paramCode == 8:
98+
self.burstDuration[channel] = originalValue
99+
elif paramCode == 9:
100+
self.interBurstInterval[channel] = originalValue
101+
elif paramCode == 10:
102+
self.pulseTrainDuration[channel] = originalValue
103+
elif paramCode == 11:
104+
self.pulseTrainDelay[channel] = originalValue
105+
elif paramCode == 12:
106+
self.linkTriggerChannel1[channel] = originalValue
107+
elif paramCode == 13:
108+
self.linkTriggerChannel2[channel] = originalValue
109+
elif paramCode == 14:
110+
self.customTrainID[channel] = originalValue
111+
elif paramCode == 15:
112+
self.customTrainTarget[channel] = originalValue
113+
elif paramCode == 16:
114+
self.customTrainLoop[channel] = originalValue
115+
elif paramCode == 17:
116+
self.restingVoltage[channel] = originalValue
117+
def programTriggerChannelParam(self, paramName, channel, value):
118+
originalValue = value
119+
if isinstance(paramName, str):
120+
paramCode = self.triggerParameterNames.index(paramName)+128
121+
else:
122+
paramCode = paramName
123+
self.Port.write((self.OpMenuByte,74,paramCode,channel,value),'uint8')
124+
# Receive acknowledgement
125+
ok = self.Port.read(1, 'uint8')
126+
if len(ok) == 0:
127+
raise PulsePalError('Error: Pulse Pal did not return an acknowledgement byte after a call to programTriggerChannelParam.')
128+
if paramCode == 1:
129+
self.triggerMode[channel] = originalValue
130+
def syncAllParams(self):
131+
# Prepare 32-bit time params
132+
programValues32 = [0]*32; pos = 0
133+
for i in range(1,5):
134+
programValues32[pos] = self.phase1Duration[i]*self.cycleFrequency; pos+=1
135+
programValues32[pos] = self.interPhaseInterval[i]*self.cycleFrequency; pos+=1
136+
programValues32[pos] = self.phase2Duration[i]*self.cycleFrequency; pos+=1
137+
programValues32[pos] = self.interPulseInterval[i]*self.cycleFrequency; pos+=1
138+
programValues32[pos] = self.burstDuration[i]*self.cycleFrequency; pos+=1
139+
programValues32[pos] = self.interBurstInterval[i]*self.cycleFrequency; pos+=1
140+
programValues32[pos] = self.pulseTrainDuration[i]*self.cycleFrequency; pos+=1
141+
programValues32[pos] = self.pulseTrainDelay[i]*self.cycleFrequency; pos+=1
142+
143+
# Prepare 16-bit voltages
144+
if self.model == 2:
145+
programValues16 = [0]*12;
146+
pos = 0
147+
for i in range(1,5):
148+
value = math.ceil(((self.phase1Voltage[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bits
149+
programValues16[pos] = value; pos+=1
150+
value = math.ceil(((self.phase2Voltage[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bits
151+
programValues16[pos] = value; pos+=1
152+
value = math.ceil(((self.restingVoltage[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bits
153+
programValues16[pos] = value; pos+=1
154+
# Prepare 8-bit params
155+
if self.model == 1:
156+
programValues8 = [0]*28;
157+
else:
158+
programValues8 = [0]*16;
159+
pos = 0
160+
for i in range(1,5):
161+
programValues8[pos] = self.isBiphasic[i]; pos+=1
162+
if self.model == 1:
163+
value = math.ceil(((self.phase1Voltage[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bits
164+
programValues8[pos] = value; pos+=1
165+
value = math.ceil(((self.phase2Voltage[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bits
166+
programValues8[pos] = value; pos+=1
167+
programValues8[pos] = self.customTrainID[i]; pos+=1
168+
programValues8[pos] = self.customTrainTarget[i]; pos+=1
169+
programValues8[pos] = self.customTrainLoop[i]; pos+=1
170+
if self.model == 1:
171+
value = math.ceil(((self.restingVoltage[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bits
172+
programValues8[pos] = value; pos+=1
173+
174+
# Prepare trigger channel link params
175+
programValuesTL = [0]*8; pos = 0
176+
for i in range(1,5):
177+
programValuesTL[pos] = self.linkTriggerChannel1[i]; pos+=1
178+
for i in range(1,5):
179+
programValuesTL[pos] = self.linkTriggerChannel2[i]; pos+=1
180+
181+
# Send all params to device
182+
if self.model == 1:
183+
self.Port.write(
184+
(self.OpMenuByte, 73), 'uint8',
185+
programValues32, 'uint32',
186+
programValues8, 'uint8',
187+
programValuesTL, 'uint8',
188+
self.triggerMode[1:3], 'uint8')
189+
if self.model == 2:
190+
self.Port.write(
191+
(self.OpMenuByte, 73), 'uint8',
192+
programValues32, 'uint32',
193+
programValues16, 'uint16',
194+
programValues8, 'uint8',
195+
programValuesTL, 'uint8',
196+
self.triggerMode[1:3], 'uint8')
197+
# Receive acknowledgement
198+
ok = self.Port.read(1, 'uint8')
199+
if len(ok) == 0:
200+
raise PulsePalError('Error: Pulse Pal did not return an acknowledgement byte after a call to syncAllParams.')
201+
def sendCustomPulseTrain(self, customTrainID, pulseTimes, pulseVoltages):
202+
nPulses = len(pulseTimes)
203+
for i in range(0,nPulses):
204+
pulseTimes[i] = pulseTimes[i]*self.cycleFrequency # Convert seconds to multiples of minimum cycle (100us)
205+
pulseVoltages[i] = math.ceil(((pulseVoltages[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bytes
206+
207+
opCode = customTrainID + 74 # 75 if custom train 1, 76 if 2
208+
if self.model == 1:
209+
self.Port.write((self.OpMenuByte , opCode, 0), 'uint8', nPulses, 'uint32', pulseTimes, 'uint32', pulseVoltages, 'uint8')
210+
else:
211+
self.Port.write((self.OpMenuByte , opCode), 'uint8', nPulses, 'uint32', pulseTimes, 'uint32', pulseVoltages, 'uint16')
212+
# Receive acknowledgement
213+
ok = self.Port.read(1, 'uint8')
214+
if len(ok) == 0:
215+
raise PulsePalError('Error: Pulse Pal did not return an acknowledgement byte after a call to sendCustomPulseTrain.')
216+
def sendCustomWaveform(self, customTrainID, pulseWidth, pulseVoltages): # For custom pulse trains with pulse times = pulse width
217+
nPulses = len(pulseVoltages)
218+
pulseTimes = [0]*nPulses
219+
pulseWidth = pulseWidth*self.cycleFrequency # Convert seconds to to multiples of minimum cycle (100us)
220+
for i in range(0,nPulses):
221+
pulseTimes[i] = pulseWidth*i # Add consecutive pulse
222+
pulseVoltages[i] = math.ceil(((pulseVoltages[i]+10)/float(20))*self.dac_bitMax) # Convert volts to bytes
223+
opCode = customTrainID + 74 # 75 if custom train 1, 76 if 2
224+
if self.model == 1:
225+
self.Port.write((self.OpMenuByte , opCode, 0), 'uint8', nPulses, 'uint32', pulseTimes, 'uint32', pulseVoltages, 'uint8')
226+
else:
227+
self.Port.write((self.OpMenuByte , opCode), 'uint8', nPulses, 'uint32', pulseTimes, 'uint32', pulseVoltages, 'uint16')
228+
# Receive acknowledgement
229+
ok = self.Port.read(1, 'uint8')
230+
if len(ok) == 0:
231+
raise PulsePalError('Error: Pulse Pal did not return an acknowledgement byte after a call to sendCustomWaveform.')
232+
def setContinuousLoop(self, channel, state):
233+
self.Port.write((self.OpMenuByte, 82, channel, state), 'uint8')
234+
def triggerOutputChannels(self, channel1, channel2, channel3, channel4):
235+
triggerByte = (1*channel1) + (2*channel2) + (4*channel3) + (8*channel4)
236+
self.Port.write((self.OpMenuByte, 77, triggerByte), 'uint8')
237+
def abortPulseTrains(self):
238+
self.Port.write((self.OpMenuByte, 80), 'uint8')
239+
def __del__(self):
240+
self.Port.write((self.OpMenuByte, 81), 'uint8')
241+
self.Port.close()
242+
243+
class PulsePalError(Exception):
244+
pass

setup.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@
1313

1414

1515
setup(
16-
name="templatepy",
16+
name="pypulsepal",
1717
version="0.0.1.dev0",
18-
description="templatepy",
18+
description="pypulsepal",
1919
long_description=long_description,
2020
long_description_content_type="text/markdown",
2121
python_requires=">=3.6",
2222
packages=find_packages(),
23-
url="https://github.com/larsrollik/templatepy",
23+
url="https://github.com/larsrollik/pypulsepal",
2424
author="Lars B. Rollik",
2525
author_email="L.B.Rollik@protonmail.com",
2626
license=license_text,
27-
install_requires=[],
27+
install_requires=[
28+
"pybpod-api", # for Arcom
29+
],
2830
extras_require={
2931
"dev": [
3032
"black",
@@ -39,9 +41,4 @@
3941
},
4042
zip_safe=False,
4143
include_package_data=True,
42-
entry_points={
43-
"console_scripts": [
44-
"console_script_name = module.path.to.function:function_name",
45-
],
46-
},
4744
)

0 commit comments

Comments
 (0)