Skip to content

Commit 7c647c9

Browse files
authored
V1.2 Release (#10)
* Add support for multiple SPI buttons. * Add support for SPIButtons CS pin selection. * Additional ShifterAnalog configurability to support G27. * Added working DFU uploader * Parser update: Replies have the command echoed * Using async comms for terminal * queue flood prevention * No more send queue buffering * Fixed bin to dfu script * Moved save button handling to system ui * using full echo of the sent command in the reply * moved main class selector to serial ui * Save and restore flash dumps * ram usage display
1 parent 9a080f5 commit 7c647c9

27 files changed

+1591
-441
lines changed

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# OpenFFBoard-configurator
22
A simple GUI to configure the [Open FFBoard](https://github.com/Ultrawipf/OpenFFBoard) written in Python 3 with Qt.
33

4-
Requires the latest firmware version most of the time.
4+
This allows complete configuration of all settings in the Open FFBoard firmware at runtime.
5+
6+
Requires the latest firmware version most of the time from a matching branch.
57

68

79
Be very careful when changing motor types, drivers or the power value.
@@ -11,12 +13,13 @@ Incorrect settings may cause unintended movements or damage hardware.
1113

1214
On older windows versions CDC drivers may not load automatically.
1315

14-
Then you need to manually install for example the STM VCP driver for the device. (We will provide a inf installer later)
16+
Then you need to manually install for example the STM VCP driver for the device. (We will provide an installer later)
1517

1618

1719
![FFB Window](screenshots/FFBwheel.png?raw=true)
1820

1921
Dependencies:
2022

2123
PyQt5
22-
pyqtgraph (For TMC graph)
24+
pyqtgraph (For TMC graph)
25+
pyusb and libusb-1.0.dll for DFU

build/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/*
2+
dist/*
3+
build/*

build/OpenFFBoard.spec

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ delete = ["opengl32sw.dll","d3dcompiler_47.dll","libGLESv2.dll","Qt5Quick.dll","
1010

1111
a = Analysis([os.path.join(folder,'main.py')],
1212
binaries=[],
13-
datas=[(os.path.join(folder,'res'), 'res')],
13+
datas=[(os.path.join(folder,'res'), 'res'),(os.path.join(folder,"libusb-1.0.dll"),".")],
1414
hiddenimports=[],
1515
hookspath=[],
1616
runtime_hooks=[],

build/bin_to_dfu.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import sys,struct,zlib,os
2+
import argparse
3+
4+
address = 0x8000000
5+
DEFAULT_DEVICE="0x0483:0xdf11"
6+
7+
# (Start,length) to skip eeprom emulation sectors
8+
flash_sectors_f4 = [
9+
(0x8000000,0x3fff),(0x800C000,-1) # multiple sectors combined. -1 size for rest
10+
]
11+
12+
def compute_crc(data):
13+
return 0xFFFFFFFF & -zlib.crc32(data) -1
14+
def build(file,targets,device=DEFAULT_DEVICE):
15+
data = b''
16+
for t,target in enumerate(targets):
17+
tdata = b''
18+
for image in target:
19+
tdata += struct.pack('<2I',image['address'],len(image['data']))+image['data']
20+
tdata = struct.pack('<6sBI255s2I',b'Target',0,1, b'ST...',len(tdata),len(target)) + tdata
21+
data += tdata
22+
data = struct.pack('<5sBIB',b'DfuSe',1,len(data)+11,len(targets)) + data
23+
v,d=map(lambda x: int(x,0) & 0xFFFF, device.split(':',1))
24+
data += struct.pack('<4H3sB',0,d,v,0x011a,b'UFD',16)
25+
crc = compute_crc(data)
26+
data += struct.pack('<I',crc)
27+
open(file,'wb').write(data)
28+
29+
if __name__ == "__main__":
30+
parser = argparse.ArgumentParser(description="Convert bin to dfu")
31+
parser.add_argument('binary', type=str, help="bin file input")
32+
parser.add_argument('--address', type=str, help="flash address. Default 0x8000000",default = "0x8000000")
33+
#parser.add_argument('--addroffset', type=str, help="flash address offset after eeprom emulation. Default 0xc000",default = "0xc000")
34+
parser.add_argument('--out', nargs='?', type=str,help = ".dfu file output",default = None)
35+
36+
args = parser.parse_args()
37+
binfile = args.binary
38+
outfile = args.out
39+
40+
if not outfile:
41+
outfile = binfile.replace(".bin","") + ".dfu"
42+
if(args.address):
43+
address = int(args.address,0)
44+
# if(args.addroffset):
45+
# addroffset = int(args.addroffset,0)
46+
47+
if not os.path.isfile(binfile):
48+
print("Invalid file '%s'." % binfile)
49+
data = bytes(open(binfile,'rb').read())
50+
51+
## F4 targets
52+
target = []
53+
for page in flash_sectors_f4:
54+
start_offset = page[0]-address
55+
if start_offset > len(data):
56+
break
57+
print("Appending %x - %x" % (page[0],page[0]+page[1]))
58+
stop = min(start_offset+page[1]+1,len(data))
59+
if page[1] == -1:
60+
stop = len(data)
61+
print("%x - %x" % (start_offset,stop))
62+
target.append({'address': page[0], 'data': data[start_offset:stop]})
63+
64+
print("Saving to", outfile)
65+
build(outfile,[target],DEFAULT_DEVICE)

build/build_nuitka.bat

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cd ..
2+
python -m nuitka --show-progress --standalone --windows-dependency-tool=pefile --plugin-enable=qt-plugins --plugin-enable=numpy --follow-imports --windows-icon-from-ico=build\app.ico main.py

buttonconf_ui.py

+145-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from PyQt5.QtWidgets import QMainWindow
1+
from collections import namedtuple
2+
from PyQt5.QtCore import QTimer
3+
from PyQt5.QtWidgets import QGridLayout, QHBoxLayout, QLineEdit, QMainWindow
24
from PyQt5.QtWidgets import QDialog
35
from PyQt5.QtWidgets import QWidget,QGroupBox
46
from PyQt5.QtWidgets import QMessageBox,QVBoxLayout,QCheckBox,QButtonGroup,QPushButton,QLabel,QSpinBox,QComboBox
@@ -16,8 +18,10 @@ def __init__(self,name,id, main):
1618
if(id == 0): # local buttons
1719
self.dialog = (LocalButtonsConf(name,self.main))
1820
elif(id == 1):
19-
self.dialog = (SPIButtonsConf(name,self.main))
21+
self.dialog = (SPIButtonsConf(name,self.main,1))
2022
elif(id == 2):
23+
self.dialog = (SPIButtonsConf(name,self.main,2))
24+
elif(id == 3):
2125
self.dialog = (ShifterButtonsConf(name,self.main))
2226

2327
OptionsDialog.__init__(self, self.dialog,main)
@@ -84,9 +88,13 @@ def readValues(self):
8488

8589
class SPIButtonsConf(OptionsDialogGroupBox):
8690

87-
def __init__(self,name,main):
91+
def __init__(self,name,main,id):
8892
self.main = main
93+
self.id = id
8994
OptionsDialogGroupBox.__init__(self,name,main)
95+
96+
def getPrefix(self):
97+
return f"spi{self.id}_"
9098

9199
def initUI(self):
92100
vbox = QVBoxLayout()
@@ -104,46 +112,170 @@ def initUI(self):
104112
vbox.addWidget(self.polBox)
105113
self.setLayout(vbox)
106114

115+
vbox.addWidget(QLabel("CS #"))
116+
self.csBox = QSpinBox()
117+
self.csBox.setMinimum(1)
118+
self.csBox.setMaximum(3)
119+
vbox.addWidget(self.csBox)
120+
107121
def apply(self):
108-
self.main.comms.serialWrite("spibtn_mode="+str(self.modeBox.currentData()))
109-
self.main.comms.serialWrite("spi_btnnum="+str(self.numBtnBox.value()))
110-
self.main.comms.serialWrite("spi_btnpol="+("1" if self.polBox.isChecked() else "0"))
122+
self.main.comms.serialWrite(f"{self.getPrefix()}btn_mode="+str(self.modeBox.currentData()))
123+
self.main.comms.serialWrite(f"{self.getPrefix()}btnnum="+str(self.numBtnBox.value()))
124+
self.main.comms.serialWrite(f"{self.getPrefix()}btnpol="+("1" if self.polBox.isChecked() else "0"))
125+
self.main.comms.serialWrite(f"{self.getPrefix()}btn_cs={self.csBox.value()}")
111126

112127
def readValues(self):
113-
self.main.comms.serialGetAsync("spi_btnnum?",self.numBtnBox.setValue,int)
128+
self.main.comms.serialGetAsync(f"{self.getPrefix()}btnnum?",self.numBtnBox.setValue,int)
114129
self.modeBox.clear()
115130
def modecb(mode):
116131
modes = mode.split("\n")
117132
modes = [m.split(":") for m in modes if m]
118133
for m in modes:
119134
self.modeBox.addItem(m[0],m[1])
120-
self.main.comms.serialGetAsync("spibtn_mode?",self.modeBox.setCurrentIndex,int)
121-
self.main.comms.serialGetAsync("spibtn_mode!",modecb)
122-
self.main.comms.serialGetAsync("spi_btnpol?",self.polBox.setChecked,int)
135+
self.main.comms.serialGetAsync(f"{self.getPrefix()}btn_mode?",self.modeBox.setCurrentIndex,int)
136+
self.main.comms.serialGetAsync(f"{self.getPrefix()}btn_mode!",modecb)
137+
self.main.comms.serialGetAsync(f"{self.getPrefix()}btnpol?",self.polBox.setChecked,int)
138+
self.main.comms.serialGetAsync(f"{self.getPrefix()}btn_cs?", self.csBox.setValue, int)
123139

124140
class ShifterButtonsConf(OptionsDialogGroupBox):
141+
class Mode(namedtuple('Mode', ['index', 'name', 'uses_spi', 'uses_local_reverse'])):
142+
pass
125143

126144
def __init__(self,name,main):
127145
self.main = main
128146
OptionsDialogGroupBox.__init__(self,name,main)
129147

130148
def initUI(self):
149+
def addThreshold(name):
150+
vbox.addWidget(QLabel(name))
151+
numBtnBox = QSpinBox()
152+
numBtnBox.setMinimum(0)
153+
numBtnBox.setMaximum(4096)
154+
vbox.addWidget(numBtnBox)
155+
return numBtnBox
156+
131157
vbox = QVBoxLayout()
132158
vbox.addWidget(QLabel("Mode"))
133159
self.modeBox = QComboBox()
160+
self.modeBox.currentIndexChanged.connect(self.modeBoxChanged)
134161
vbox.addWidget(self.modeBox)
162+
163+
self.xPos = QLineEdit()
164+
self.xPos.setReadOnly(True)
165+
self.yPos = QLineEdit()
166+
self.yPos.setReadOnly(True)
167+
self.gear = QLineEdit()
168+
self.gear.setReadOnly(True)
169+
170+
posGroup = QGridLayout()
171+
posGroup.addWidget(QLabel("X"), 1, 1)
172+
posGroup.addWidget(self.xPos, 1, 2)
173+
posGroup.addWidget(QLabel("Y"), 1, 3)
174+
posGroup.addWidget(self.yPos, 1, 4)
175+
posGroup.addWidget(QLabel("Calculated Gear"), 2, 1, 1, 2)
176+
posGroup.addWidget(self.gear, 2, 3, 1, 2)
177+
posGroupBox = QGroupBox()
178+
posGroupBox.setTitle("Current")
179+
posGroupBox.setLayout(posGroup)
180+
vbox.addWidget(posGroupBox)
181+
182+
vbox.addWidget(QLabel("X Channel"))
183+
self.xChannel = QSpinBox()
184+
self.xChannel.setMinimum(1)
185+
self.xChannel.setMaximum(6)
186+
vbox.addWidget(self.xChannel)
187+
188+
vbox.addWidget(QLabel("Y Channel"))
189+
self.yChannel = QSpinBox()
190+
self.yChannel.setMinimum(1)
191+
self.yChannel.setMaximum(6)
192+
vbox.addWidget(self.yChannel)
193+
194+
self.x12 = addThreshold("X 1,2 Threshold")
195+
self.x56 = addThreshold("X 5,6 Threshold")
196+
self.y135 = addThreshold("Y 1,3,5 Threshold")
197+
self.y246 = addThreshold("Y 2,4,6 Threshold")
198+
199+
self.revBtnLabel = QLabel("Reverse Button Digital Input")
200+
vbox.addWidget(self.revBtnLabel)
201+
self.revBtnBox = QSpinBox()
202+
self.revBtnBox.setMinimum(1)
203+
self.revBtnBox.setMaximum(8)
204+
vbox.addWidget(self.revBtnBox)
205+
206+
self.csPinLabel = QLabel("SPI CS Pin Number")
207+
vbox.addWidget(self.csPinLabel)
208+
self.csPinBox = QSpinBox()
209+
self.csPinBox.setMinimum(1)
210+
self.csPinBox.setMaximum(3)
211+
vbox.addWidget(self.csPinBox)
212+
135213
self.setLayout(vbox)
136-
214+
215+
self.timer = QTimer()
216+
self.timer.timeout.connect(self.readXYPosition)
217+
218+
def onshown(self):
219+
self.timer.start(500)
220+
221+
def onclose(self):
222+
self.timer.stop()
223+
224+
def modeBoxChanged(self, _):
225+
mode = self.modeBox.currentData()
226+
227+
if mode is not None:
228+
self.revBtnLabel.setVisible(mode.uses_local_reverse)
229+
self.revBtnBox.setVisible(mode.uses_local_reverse)
230+
self.csPinLabel.setVisible(mode.uses_spi)
231+
self.csPinBox.setVisible(mode.uses_spi)
137232

138233
def apply(self):
139-
self.main.comms.serialWrite("shifter_mode="+str(self.modeBox.currentData()))
234+
self.main.comms.serialWrite(f"shifter_mode={self.modeBox.currentData().index}")
235+
self.main.comms.serialWrite(f"shifter_x_chan={self.xChannel.value()}")
236+
self.main.comms.serialWrite(f"shifter_y_chan={self.yChannel.value()}")
237+
self.main.comms.serialWrite(f"shifter_x_12={self.x12.value()}")
238+
self.main.comms.serialWrite(f"shifter_x_56={self.x56.value()}")
239+
self.main.comms.serialWrite(f"shifter_y_135={self.y135.value()}")
240+
self.main.comms.serialWrite(f"shifter_y_246={self.y246.value()}")
241+
self.main.comms.serialWrite(f"shifter_rev_btn={self.revBtnBox.value()}")
242+
self.main.comms.serialWrite(f"shifter_cs_pin={self.csPinBox.value()}")
243+
244+
def readXYPosition(self):
245+
def updatePosition(valueStr: str):
246+
x,y = valueStr.strip().split(",")
247+
self.xPos.setText(x)
248+
self.yPos.setText(y)
249+
250+
def updateGear(value: str):
251+
value = value.strip()
252+
if value == "0":
253+
value = "N"
254+
elif value == "7":
255+
value = "R"
256+
257+
self.gear.setText(value)
258+
259+
self.main.comms.serialGetAsync("shifter_vals?",updatePosition, str)
260+
self.main.comms.serialGetAsync("shifter_gear?", updateGear, str)
140261

141262
def readValues(self):
142263
self.modeBox.clear()
143264
def modecb(mode):
144265
modes = mode.split("\n")
145266
modes = [m.split(":") for m in modes if m]
146267
for m in modes:
147-
self.modeBox.addItem(m[0],m[1])
268+
index, uses_spi, uses_local_reverse = m[1].split(',')
269+
self.modeBox.addItem(m[0], ShifterButtonsConf.Mode(int(index), m[0], uses_spi == "1", uses_local_reverse == "1"))
148270
self.main.comms.serialGetAsync("shifter_mode?",self.modeBox.setCurrentIndex,int)
149271
self.main.comms.serialGetAsync("shifter_mode!",modecb)
272+
self.main.comms.serialGetAsync("shifter_x_chan?",self.xChannel.setValue,int)
273+
self.main.comms.serialGetAsync("shifter_y_chan?",self.yChannel.setValue,int)
274+
self.main.comms.serialGetAsync("shifter_x_12?",self.x12.setValue,int)
275+
self.main.comms.serialGetAsync("shifter_x_56?",self.x56.setValue,int)
276+
self.main.comms.serialGetAsync("shifter_y_135?",self.y135.setValue,int)
277+
self.main.comms.serialGetAsync("shifter_y_246?",self.y246.setValue,int)
278+
self.main.comms.serialGetAsync("shifter_rev_btn?",self.revBtnBox.setValue,int)
279+
self.main.comms.serialGetAsync("shifter_cs_pin?",self.csPinBox.setValue, int)
280+
self.readXYPosition()
281+

config.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
from PyQt5.QtWidgets import QFileDialog,QMessageBox
3+
4+
def saveDump(buf):
5+
dump = {"flash":[]}
6+
for l in buf.split("\n"):
7+
if not l:
8+
break
9+
addr,val = l.split(":")
10+
dump["flash"].append({"addr":addr,"val":val})
11+
12+
dlg = QFileDialog()
13+
dlg.setFileMode(QFileDialog.AnyFile)
14+
dlg.setNameFilters(["Json files (*.json)"])
15+
if dlg.exec_():
16+
filenames = dlg.selectedFiles()
17+
try:
18+
with open(filenames[0],"w") as f:
19+
json.dump(dump,f)
20+
msg = QMessageBox(QMessageBox.Information,"Save flash dump","Saved successfully.")
21+
msg.exec_()
22+
except Exception as e:
23+
msg = QMessageBox(QMessageBox.Warning,"Save flash dump","Error while saving flash dump:\n"+str(e))
24+
msg.exec_()
25+
else:
26+
return
27+
28+
def loadDump():
29+
dlg = QFileDialog()
30+
dlg.setFileMode(QFileDialog.ExistingFile)
31+
dlg.setNameFilters(["Json files (*.json)"])
32+
if dlg.exec_():
33+
dump = {}
34+
filenames = dlg.selectedFiles()
35+
with open(filenames[0],"r") as f:
36+
dump = json.load(f)
37+
return dump
38+
39+
else:
40+
return None

0 commit comments

Comments
 (0)