22import logging
33import sys
44import zipfile
5+ import json
56from typing import List , Optional
67from collections import namedtuple
78from pathlib import Path
1920PioEnv = namedtuple ('FWVersion' , ['nice_name' , 'raw_name' ])
2021
2122
23+ def get_pio_environments (fw_dir : str ) -> List [PioEnv ]:
24+ ini_path = Path (fw_dir , 'platformio.ini' )
25+ with open (ini_path .resolve (), 'r' ) as fp :
26+ ini_lines = fp .readlines ()
27+ environment_lines = [ini_line for ini_line in ini_lines if ini_line .startswith ('[env:' )]
28+ raw_pio_envs = []
29+ for environment_line in environment_lines :
30+ match = re .search (r'\[env:(.+)\]' , environment_line )
31+ if match :
32+ raw_pio_envs .append (match .group (1 ))
33+ log .info (f'Found pio environments: { raw_pio_envs } ' )
34+
35+ # we don't want to build native
36+ if 'native' in raw_pio_envs :
37+ raw_pio_envs .remove ('native' )
38+ nice_name_lookup = {
39+ 'ramps' : 'RAMPS' ,
40+ 'esp32' : 'ESP32' ,
41+ 'mksgenlv21' : 'MKS Gen L v2.1' ,
42+ 'mksgenlv2' : 'MKS Gen L v2' ,
43+ 'mksgenlv1' : 'MKS Gen L v1' ,
44+ }
45+ pio_environments = []
46+ for raw_env in raw_pio_envs :
47+ if raw_env in nice_name_lookup :
48+ pio_env = PioEnv (nice_name_lookup [raw_env ], raw_env )
49+ else :
50+ pio_env = PioEnv (raw_env , raw_env )
51+ pio_environments .append (pio_env )
52+ return pio_environments
53+
54+
55+ def download_fw (zip_url : str ) -> str :
56+ log .info (f'Downloading OAT FW from: { zip_url } ' )
57+ resp = requests .get (zip_url )
58+ zipfile_name = 'OATFW.zip'
59+ with open (zipfile_name , 'wb' ) as fd :
60+ fd .write (resp .content )
61+ fd .close ()
62+ return zipfile_name
63+
64+
65+ def extract_fw (zipfile_name : str ) -> str :
66+ log .info (f'Extracting FW from { zipfile_name } ' )
67+ with zipfile .ZipFile (zipfile_name , 'r' ) as zip_ref :
68+ zip_infolist = zip_ref .infolist ()
69+ if len (zip_infolist ) > 0 and zip_infolist [0 ].is_dir ():
70+ fw_dir = zip_infolist [0 ].filename
71+ else :
72+ log .fatal (f'Could not find FW top level directory in { zip_infolist } !' )
73+ sys .exit (1 )
74+ zip_ref .extractall ()
75+ log .info (f'Extracted FW to { fw_dir } ' )
76+ return fw_dir
77+
78+
2279class LogicState :
2380 release_list : Optional [List [FWVersion ]] = None
2481 release_idx : Optional [int ] = None
2582 fw_dir : Optional [str ] = None
26- pio_envs : Optional [ List [PioEnv ]] = None
83+ pio_envs : List [PioEnv ] = []
2784 pio_env : Optional [str ] = None
2885 config_file_path : Optional [str ] = None
86+ build_success : bool = False
87+ serial_ports : List [str ] = []
88+ upload_port : Optional [str ] = None
2989
3090 def __setattr__ (self , key , val ):
3191 log .debug (f'LogicState updated: { key } { getattr (self , key )} -> { val } ' )
@@ -38,18 +98,22 @@ def __init__(self, main_app: 'MainWidget'):
3898 self .pio_process = None
3999
40100 self .main_app = main_app
41- main_app .wBtn_download_fw .setDisabled ( False )
101+ main_app .wBtn_download_fw .setEnabled ( True )
42102 main_app .wBtn_download_fw .clicked .connect (self .spawn_worker_thread (self .download_and_extract_fw ))
43103 main_app .wBtn_select_local_config .clicked .connect (self .open_local_config_file )
44- main_app .wCombo_pio_env .currentIndexChanged .connect (self .pio_combo_box_changed )
104+ main_app .wCombo_pio_env .currentIndexChanged .connect (self .pio_env_combo_box_changed )
45105 main_app .wBtn_build_fw .clicked .connect (self .spawn_worker_thread (self .build_fw ))
106+ main_app .wBtn_refresh_ports .clicked .connect (self .spawn_worker_thread (self .refresh_ports ))
107+ main_app .wCombo_serial_port .currentIndexChanged .connect (self .serial_port_combo_box_changed )
46108 main_app .wBtn_upload_fw .clicked .connect (self .spawn_worker_thread (self .upload_fw ))
47109
48110 self .threadpool = QThreadPool ()
49111 self .threadpool .setMaxThreadCount (1 ) # Only one worker
50112
51113 # Manually spawn a worker to grab tags from GitHub
52114 self .spawn_worker_thread (self .get_fw_versions )()
115+ # Manually spawn a worker to refresh serial ports
116+ self .spawn_worker_thread (self .refresh_ports )()
53117
54118 def spawn_worker_thread (self , fn ):
55119 @Slot ()
@@ -82,14 +146,18 @@ def worker_finished(self, worker_name: Optional[str] = None):
82146 self .main_app .wMsg_config_path .setText (f'Local configuration file:\n { self .logic_state .config_file_path } ' )
83147
84148 # check requirements to unlock the build button
85- build_reqs = [
86- self .logic_state .config_file_path ,
87- self .logic_state .pio_env ,
88- ]
89- if all (r is not None for r in build_reqs ):
90- self .main_app .wBtn_build_fw .setDisabled (False )
91- else :
92- self .main_app .wBtn_build_fw .setDisabled (True )
149+ build_reqs_ok = all ([
150+ self .logic_state .config_file_path is not None ,
151+ self .logic_state .pio_env is not None ,
152+ ])
153+ self .main_app .wBtn_build_fw .setEnabled (build_reqs_ok )
154+
155+ # check requirements to unlock the upload button
156+ upload_reqs_ok = all ([
157+ self .logic_state .build_success == True ,
158+ self .logic_state .upload_port is not None ,
159+ ])
160+ self .main_app .wBtn_upload_fw .setEnabled (upload_reqs_ok )
93161
94162 def get_fw_versions (self ) -> str :
95163 fw_api_url = 'https://api.github.com/repos/OpenAstroTech/OpenAstroTracker-Firmware/releases'
@@ -112,16 +180,16 @@ def get_fw_versions_result(main_app: 'MainWidget', fw_versions_list: List[FWVers
112180 for fw_version in fw_versions_list :
113181 main_app .wCombo_fw_version .addItem (fw_version .nice_name )
114182 main_app .wCombo_fw_version .setCurrentIndex (0 )
115- main_app .wBtn_download_fw .setDisabled ( False )
183+ main_app .wBtn_download_fw .setEnabled ( True )
116184
117185 def download_and_extract_fw (self ) -> str :
118186 self .main_app .wSpn_download .setState (BusyIndicatorState .BUSY )
119187 fw_idx = self .main_app .wCombo_fw_version .currentIndex ()
120188 zip_url = self .logic_state .release_list [fw_idx ].url
121- zipfile_name = self . download_fw (zip_url )
189+ zipfile_name = download_fw (zip_url )
122190
123- self .logic_state .fw_dir = self . extract_fw (zipfile_name )
124- self .logic_state .pio_envs = self . get_pio_environments (self .logic_state .fw_dir )
191+ self .logic_state .fw_dir = extract_fw (zipfile_name )
192+ self .logic_state .pio_envs = get_pio_environments (self .logic_state .fw_dir )
125193 return self .download_and_extract_fw .__name__
126194
127195 @staticmethod
@@ -133,68 +201,14 @@ def download_and_extract_fw_result(main_app: 'MainWidget', pio_environments: Lis
133201 main_app .wCombo_pio_env .addItem (pio_env_name .nice_name )
134202 main_app .wCombo_pio_env .setPlaceholderText ('Select Board' )
135203
136- @staticmethod
137- def download_fw (zip_url : str ) -> str :
138- log .info (f'Downloading OAT FW from: { zip_url } ' )
139- resp = requests .get (zip_url )
140- zipfile_name = 'OATFW.zip'
141- with open (zipfile_name , 'wb' ) as fd :
142- fd .write (resp .content )
143- fd .close ()
144- return zipfile_name
145-
146- @staticmethod
147- def extract_fw (zipfile_name : str ) -> str :
148- log .info (f'Extracting FW from { zipfile_name } ' )
149- with zipfile .ZipFile (zipfile_name , 'r' ) as zip_ref :
150- zip_infolist = zip_ref .infolist ()
151- if len (zip_infolist ) > 0 and zip_infolist [0 ].is_dir ():
152- fw_dir = zip_infolist [0 ].filename
153- else :
154- log .fatal (f'Could not find FW top level directory in { zip_infolist } !' )
155- sys .exit (1 )
156- zip_ref .extractall ()
157- log .info (f'Extracted FW to { fw_dir } ' )
158- return fw_dir
159-
160- @staticmethod
161- def get_pio_environments (fw_dir : str ) -> List [PioEnv ]:
162- ini_path = Path (fw_dir , 'platformio.ini' )
163- with open (ini_path .resolve (), 'r' ) as fp :
164- ini_lines = fp .readlines ()
165- environment_lines = [ini_line for ini_line in ini_lines if ini_line .startswith ('[env:' )]
166- raw_pio_envs = []
167- for environment_line in environment_lines :
168- match = re .search (r'\[env:(.+)\]' , environment_line )
169- if match :
170- raw_pio_envs .append (match .group (1 ))
171- log .info (f'Found pio environments: { raw_pio_envs } ' )
172-
173- # we don't want to build native
174- if 'native' in raw_pio_envs :
175- raw_pio_envs .remove ('native' )
176- nice_name_lookup = {
177- 'ramps' : 'RAMPS' ,
178- 'esp32' : 'ESP32' ,
179- 'mksgenlv21' : 'MKS Gen L v2.1' ,
180- 'mksgenlv2' : 'MKS Gen L v2' ,
181- 'mksgenlv1' : 'MKS Gen L v1' ,
182- }
183- pio_environments = []
184- for raw_env in raw_pio_envs :
185- if raw_env in nice_name_lookup :
186- pio_env = PioEnv (nice_name_lookup [raw_env ], raw_env )
187- else :
188- pio_env = PioEnv (raw_env , raw_env )
189- pio_environments .append (pio_env )
190- return pio_environments
191-
192204 @Slot ()
193- def pio_combo_box_changed (self , idx : int ):
194- if self .logic_state .pio_envs is not None and idx != - 1 :
205+ def pio_env_combo_box_changed (self , idx : int ):
206+ if self .logic_state .pio_envs and idx != - 1 :
195207 self .logic_state .pio_env = self .logic_state .pio_envs [idx ].raw_name
196208 # manually update GUI
197209 self .worker_finished ()
210+ else :
211+ self .logic_state .pio_env = None
198212
199213 @Slot ()
200214 def open_local_config_file (self ):
@@ -241,36 +255,79 @@ def build_fw(self):
241255 )
242256 self .pio_process .start ()
243257
244- def upload_fw (self ):
245- self .main_app .wSpn_upload .setState (BusyIndicatorState .BUSY )
258+ @Slot ()
259+ def pio_build_finished (self ):
260+ log .info (f'platformio build finished' )
261+ exit_state = self .pio_process .qproc .exitCode ()
262+ if exit_state == QProcess .NormalExit :
263+ log .info ('Normal exit' )
264+ self .main_app .wSpn_build .setState (BusyIndicatorState .GOOD )
265+ self .logic_state .build_success = True
266+ else :
267+ log .error ('Did not exit normally' )
268+ self .main_app .wSpn_build .setState (BusyIndicatorState .BAD )
269+ self .pio_process = None
270+
271+ def refresh_ports (self ):
246272 if self .pio_process is not None :
247273 log .error (f'platformio already running! { self .pio_process } ' )
248274 return
249275
250276 self .pio_process = ExternalProcess (
251277 'platformio' ,
252- ['run' ,
253- '--environment' , self .logic_state .pio_env ,
254- '--project-dir' , self .logic_state .fw_dir ,
255- '--verbose' ,
256- '--target' , 'upload'
257- ],
258- self .pio_upload_finished ,
278+ ['device' , 'list' , '--serial' , '--json-output' ],
279+ self .pio_refresh_ports_finished ,
259280 )
260281 self .pio_process .start ()
261282
262283 @Slot ()
263- def pio_build_finished (self ):
264- log .info (f'platformio build finished' )
284+ def pio_refresh_ports_finished (self ):
285+ log .info (f'platformio refresh ports finished' )
265286 exit_state = self .pio_process .qproc .exitCode ()
266287 if exit_state == QProcess .NormalExit :
267288 log .info ('Normal exit' )
268- self .main_app .wSpn_build .setState (BusyIndicatorState .GOOD )
269- self .main_app .wBtn_upload_fw .setDisabled (False )
270289 else :
271290 log .error ('Did not exit normally' )
272- self . main_app . wSpn_build . setState ( BusyIndicatorState . BAD )
291+ all_port_data = json . loads ( self . pio_process . stdout_text )
273292 self .pio_process = None
293+ self .logic_state .serial_ports = [port_data ['port' ] for port_data in all_port_data ]
294+
295+ self .main_app .wCombo_serial_port .clear ()
296+ for serial_port in self .logic_state .serial_ports :
297+ self .main_app .wCombo_serial_port .addItem (serial_port )
298+ if len (self .logic_state .serial_ports ) > 0 :
299+ self .main_app .wCombo_serial_port .setCurrentIndex (0 )
300+ else :
301+ self .logic_state .upload_port = None
302+ self .main_app .wCombo_serial_port .setCurrentIndex (- 1 )
303+
304+ @Slot ()
305+ def serial_port_combo_box_changed (self , idx : int ):
306+ if self .logic_state .serial_ports and idx != - 1 :
307+ self .logic_state .upload_port = self .logic_state .serial_ports [idx ]
308+ # manually update GUI
309+ self .worker_finished ()
310+ else :
311+ self .logic_state .upload_port = None
312+
313+ def upload_fw (self ):
314+ self .main_app .wSpn_upload .setState (BusyIndicatorState .BUSY )
315+ if self .pio_process is not None :
316+ log .error (f'platformio already running! { self .pio_process } ' )
317+ return
318+
319+ self .pio_process = ExternalProcess (
320+ 'platformio' ,
321+ ['run' ,
322+ '--environment' , self .logic_state .pio_env ,
323+ '--project-dir' , self .logic_state .fw_dir ,
324+ '--verbose' ,
325+ '--target' , 'upload' ,
326+ '--upload-port' , self .logic_state .upload_port ,
327+ ],
328+ self .pio_upload_finished ,
329+ )
330+ self .pio_process .start ()
274331
275332 @Slot ()
276333 def pio_upload_finished (self ):
@@ -294,20 +351,23 @@ def __init__(self, log_object: LogObject):
294351 self .wCombo_fw_version = QComboBox ()
295352 self .wCombo_fw_version .setPlaceholderText ('Grabbing FW Versions...' )
296353 self .wBtn_download_fw = QPushButton ('Download' )
297- self .wBtn_download_fw .setDisabled ( True )
354+ self .wBtn_download_fw .setEnabled ( False )
298355 self .wSpn_download = QBusyIndicatorGoodBad (fixed_size = (50 , 50 ))
299356
300357 self .wMsg_pio_env = QLabel ('Select board:' )
301358 self .wCombo_pio_env = QComboBox ()
302359 self .wCombo_pio_env .setPlaceholderText ('No FW downloaded yet...' )
303360 self .wBtn_select_local_config = QPushButton ('Select local config file' )
304361 self .wBtn_build_fw = QPushButton ('Build FW' )
305- self .wBtn_build_fw .setDisabled ( True )
362+ self .wBtn_build_fw .setEnabled ( False )
306363 self .wMsg_config_path = QLabel ('No config file selected' )
307364 self .wSpn_build = QBusyIndicatorGoodBad (fixed_size = (50 , 50 ))
308365
366+ self .wBtn_refresh_ports = QPushButton ('Refresh ports' )
367+ self .wCombo_serial_port = QComboBox ()
368+ self .wCombo_serial_port .setPlaceholderText ('No port selected' )
309369 self .wBtn_upload_fw = QPushButton ('Upload FW' )
310- self .wBtn_upload_fw .setDisabled ( True )
370+ self .wBtn_upload_fw .setEnabled ( False )
311371 self .wSpn_upload = QBusyIndicatorGoodBad (fixed_size = (50 , 50 ))
312372
313373 self .logText = QPlainTextEdit ()
@@ -321,7 +381,7 @@ def __init__(self, log_object: LogObject):
321381 [self .wMsg_fw_version , self .wCombo_fw_version , self .wBtn_download_fw , self .wSpn_download ],
322382 [self .wMsg_pio_env , self .wCombo_pio_env , self .wBtn_select_local_config , self .wBtn_build_fw ],
323383 [self .wMsg_config_path , None , None , self .wSpn_build ],
324- [None , None , self .wBtn_upload_fw , self .wSpn_upload ]
384+ [self . wBtn_refresh_ports , self . wCombo_serial_port , self .wBtn_upload_fw , self .wSpn_upload ]
325385 ]
326386 for y , row_arr in enumerate (layout_arr ):
327387 for x , widget in enumerate (row_arr ):
0 commit comments