├── LICENSE ├── README.md ├── compile.sh ├── firmware.cmd └── firmware.sh /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3rd party firmware selector for the Meshtastic project 2 | The CMD and Bash script automates the process of selecting, downloading, and applying firmware updates from the [meshtastic/firmware](https://github.com/meshtastic/firmware) GitHub repository via the USB port. 3 | 4 | # Windows Quick start 5 | [Download firmware.cmd (right click save)](https://github.com/mikecarper/meshfirmware/blob/main/firmware.cmd?raw=true) 6 | Make sure file is named firmware.cmd and not firmware.cmd.txt 7 | double click and run the file firmware.cmd 8 | 9 | Windows Video 10 | ----- 11 | 12 | https://github.com/user-attachments/assets/ab68cb5e-63d5-4c73-ac4a-fdb76702fb20 13 | 14 | 15 | 16 | 17 | # Linux Quick start 18 | Copy and run this in your linux terminal 19 | ```bash 20 | cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && chmod +x firmware.sh && ./firmware.sh 21 | ``` 22 |
23 | Readable Code 24 | 25 | ```bash 26 | cd ~ 27 | git clone https://github.com/mikecarper/meshfirmware.git 28 | cd meshfirmware 29 | chmod +x firmware.sh 30 | ./firmware.sh 31 | ``` 32 | 33 |
34 | 35 | 36 | Linux Video 37 | ----- 38 | 39 | https://github.com/user-attachments/assets/06fc7b59-ed03-44d7-a4d1-a0492dec5d16 40 | 41 | 42 | 43 | # Linux Compile the firmware 44 | Copy and run this in your linux terminal 45 | ```bash 46 | cd ~ && git clone https://github.com/mikecarper/meshfirmware.git && cd meshfirmware && chmod +x compile.sh && ./compile.sh 47 | ``` 48 |
49 | Readable Code 50 | 51 | ```bash 52 | cd ~ 53 | git clone https://github.com/mikecarper/meshfirmware.git 54 | cd meshfirmware 55 | chmod +x compile.sh 56 | ./compile.sh 57 | ``` 58 | 59 |
60 | 61 | Linux Video 62 | ----- 63 | 64 | https://github.com/user-attachments/assets/20117724-6e62-4c17-8879-aebb1ef48456 65 | 66 | 67 | 68 | 69 | Overview 70 | -------- 71 | 72 | The [script](https://github.com/mikecarper/meshfirmware/blob/main/firmware.sh) does the following: 73 | 74 | * Updates a local cache file with GitHub release data if it is older than 6 hours. 75 | 76 | * Falls back to using the cached data if no internet connection is detected. 77 | 78 | * Parses the JSON release data to build a list of firmware release versions. 79 | 80 | * Appends labels (such as _(alpha)_, _(beta)_, _(rc)_, or _(pre-release)_) based on the release tag. 81 | 82 | * Prepends the ! label if the release has known issues. 83 | 84 | * Uses lsusb to detect connected USB devices. 85 | 86 | * If more than one matching USB device exists, the user is prompted to choose the correct one. 87 | 88 | * Matches the detected device against available firmware files. 89 | 90 | * If more than one matching firmware file exists, the user is prompted to choose the correct one. 91 | 92 | * For ESP32 devices, the script adjusts the update script (e.g., changes baud rate from 115200 to 1200) as required. 93 | Also allows the user to choose between an update or an install operation 94 | 95 | * Stops any systemd service locking the device before proceeding and restarts it afterward. 96 | 97 | 98 | 99 | Usage 100 | ----- 101 | 102 | Run the script with the following syntax: 103 | 104 | ```bash 105 | ./firmware.sh [OPTIONS] 106 | ``` 107 | 108 | ### Options 109 | 110 | * \--version VERSION 111 | Specify a firmware release version to auto-select (searches for tags containing the provided string). 112 | 113 | * \--install 114 | Set the operation mode to **install** (used instead of update). 115 | 116 | * \--update 117 | Set the operation mode to **update** (this is the default if not otherwise specified). 118 | 119 | * \--run 120 | Automatically update firmware without prompting the user. 121 | 122 | * \-h, --help 123 | Display the help message and exit. 124 | 125 | -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Directory we started in. 5 | ORIG_DIR="$(pwd)" 6 | 7 | # search for platformio.ini containing meshtastic 8 | found=false 9 | count=0 10 | while [ $count -lt 2 ]; do 11 | parent="$(dirname "$PWD")" 12 | file=$(find "$PWD" -type f -name platformio.ini -exec grep -q "github.com/meshtastic" {} \; -print | sort -r | tail -n 1) 13 | 14 | if [[ -n "$file" ]]; then 15 | parent="$(dirname "$file")" 16 | found=true 17 | cd "$parent" 18 | break 19 | fi 20 | count=$((count + 1)) 21 | # determine parent directory 22 | # if we’re at / (or somehow can’t go up), break out 23 | if [[ "$parent" == "$PWD" ]]; then 24 | break 25 | fi 26 | cd "$parent" 27 | done 28 | 29 | if ! $found; then 30 | echo " platformio.ini with meshtastic not found in any parent directories." 31 | read -rp "Would you like to clone https://github.com/meshtastic/firmware here $parent? [y/N] " ans 32 | case "$ans" in 33 | [Yy]* ) 34 | cd "$parent" 35 | git clone https://github.com/meshtastic/firmware && cd firmware 36 | # after cloning, you probably want to repeat the search or just proceed 37 | if [[ -f "platformio.ini" ]] && grep -q "github.com/meshtastic" platformio.ini; then 38 | echo "Now in $(pwd), and platformio.ini is present." 39 | else 40 | echo "Cloned, but platformio.ini still missing or wrong." 41 | fi 42 | ;; 43 | * ) 44 | echo "Exiting without cloning." 45 | exit 1 46 | ;; 47 | esac 48 | fi 49 | echo "$PWD" 50 | 51 | # Cleanup function that returns back. 52 | cleanup() { 53 | cd "$ORIG_DIR" || exit 54 | } 55 | # arrange for cleanup() to run on EXIT (this covers normal exit, errors, and Ctrl‑C) 56 | trap cleanup EXIT 57 | 58 | # Create small package (no debugging symbols) 59 | # Add `argp` for musl 60 | # -Os: Optimize for size; enables most -O2 optimizations. 61 | #-ffunction-sections -fdata-sections: Place individual functions and data in their own sections. 62 | # Allows the linker to later remove any sections that aren’t referenced in the final executable. 63 | # -Wl,--gc-sections: Linker garbage collection of unused sections. 64 | # -largp: Link against the argp library; provides command-line argument parsing. 65 | PLATFORMIO_BUILD_FLAGS="-Os -ffunction-sections -fdata-sections -Wl,--gc-sections -largp" 66 | 67 | VPN_INFO="$HOME/.vpnServerInfo" 68 | # Number of attempts for each file 69 | MAX_ATTEMPTS=60 70 | # Timeout in seconds for scp (adjust if needed) 71 | SCP_TIMEOUT=5 72 | 73 | # Optionally pass the desired environment name as the first argument. 74 | env_arg="${1:-}" 75 | 76 | # Update git 77 | git reset --hard 78 | git fetch origin 79 | git switch master 2>/dev/null || git checkout master 80 | git reset --hard origin/master 81 | git fetch origin 82 | git pull 83 | git pull --recurse-submodules 84 | 85 | 86 | # Get environment names from platformio.ini files. 87 | # This finds all lines that start with [env: and then strips off the prefix and trailing ]. 88 | mapfile -t envs < <( 89 | find . -type f -name "platformio.ini" -exec grep -h "^\[env:" {} \; \ 90 | | sort -u \ 91 | | sed -n 's/^\[env:\([^]]*\)].*/\1/p' 92 | ) 93 | 94 | # Check if any environments were found. 95 | if [ ${#envs[@]} -eq 0 ]; then 96 | echo "No environments found in platformio.ini files." 97 | exit 1 98 | fi 99 | 100 | selected_env="" 101 | if [ -n "$env_arg" ]; then 102 | # Try to auto-select an environment that matches the provided argument (case-insensitive). 103 | for env in "${envs[@]}"; do 104 | if [[ "${env,,}" == "${env_arg,,}" ]]; then 105 | selected_env="$env" 106 | break 107 | fi 108 | done 109 | 110 | if [ -z "$selected_env" ]; then 111 | echo "Environment '$env_arg' not found in the list." 112 | else 113 | echo "Auto-selected environment: $selected_env" 114 | fi 115 | fi 116 | 117 | if [ -z "$selected_env" ]; then 118 | # Display a numbered menu for the user to choose an environment. 119 | echo "Select an environment:" 120 | for i in "${!envs[@]}"; do 121 | printf "%d) %s\n" $((i+1)) "${envs[$i]}" 122 | done 123 | 124 | # If .pio/libdeps exists, show the short list of already built environments—but only if there is at least one. 125 | if [ -d ".pio/libdeps" ]; then 126 | # Enable nullglob so that the array is empty if no match is found. 127 | shopt -s nullglob 128 | built_dirs=(.pio/libdeps/*/) 129 | if [ ${#built_dirs[@]} -gt 0 ]; then 130 | # Create an associative array of built environment names. 131 | declare -A built_envs 132 | for d in "${built_dirs[@]}"; do 133 | if [ -d "$d" ]; then 134 | built_name=$(basename "$d") 135 | built_envs["$built_name"]=1 136 | fi 137 | done 138 | # Only print the section if at least one environment matches. 139 | if [ ${#built_envs[@]} -gt 0 ]; then 140 | echo "" 141 | echo "Already built environments:" 142 | 143 | # Loop through the global env list. When the env name is found in built_envs, print its number, name, and last build version. 144 | for i in "${!envs[@]}"; do 145 | env_name="${envs[$i]}" 146 | if [ -n "${built_envs[$env_name]:-}" ]; then 147 | 148 | # Capture the newest version folder for this env: 149 | versionLastBuild=$( 150 | find release -type f -path "release/*/$env_name/*" \ 151 | | sed -n "s|release/\([^/]*\)/$env_name/.*|\1|p" \ 152 | | sort -V \ 153 | | tail -n1 154 | ) 155 | 156 | # Print index, env name, and last build version 157 | printf "%d) %s (last build: %s)\n" \ 158 | $((i+1)) "$env_name" "$versionLastBuild" 159 | fi 160 | done 161 | fi 162 | fi 163 | shopt -u nullglob 164 | fi 165 | 166 | read -rp "Enter number: " selection 167 | if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#envs[@]} ]; then 168 | echo "Invalid selection." 169 | exit 1 170 | fi 171 | 172 | selected_env="${envs[$((selection-1))]}" 173 | fi 174 | 175 | # Now you have the selected environment in $selected_env. 176 | # You can use it further in your script. 177 | echo "Final environment: $selected_env" 178 | 179 | VERSION=$(bin/buildinfo.py long) 180 | 181 | # Get the last 20 tags (sorted by creation date descending) 182 | mapfile -t tags < <(git tag --sort=-creatordate | head -n20 | tac) 183 | 184 | if [ ${#tags[@]} -eq 0 ]; then 185 | echo "No tags found in this repository." 186 | exit 1 187 | fi 188 | 189 | echo "Select a release to check out:" 190 | n=1 191 | declare -A tagmap 192 | for tag in "${tags[@]}"; do 193 | echo "$n) $tag" 194 | tagmap[$n]="$tag" 195 | ((n++)) 196 | done 197 | # Add an extra option for "current" 198 | echo "$n) v${VERSION}+ current " 199 | read -rp "Enter selection [1-$n]: " choice 200 | 201 | if [[ "$choice" =~ ^[0-9]+$ ]]; then 202 | if [ "$choice" -ge 1 ] && [ "$choice" -lt "$n" ]; then 203 | selected="${tagmap[$choice]}" 204 | echo "You selected tag: $selected" 205 | git reset --hard 206 | git config advice.detachedHead false 207 | git checkout "$selected" 208 | elif [ "$choice" -eq "$n" ]; then 209 | echo "You selected: $VERSION current" 210 | else 211 | echo "Invalid selection: number not in range." 212 | exit 1 213 | fi 214 | else 215 | echo "Invalid input; please enter a number." 216 | exit 1 217 | fi 218 | 219 | # Determine which extra patch exists, prefer extra.bbs.patch if available. 220 | if [ -f extra.bbs.patch ]; then 221 | extraPatchFile="extra.bbs.patch" 222 | elif [ -f extra.patch ]; then 223 | extraPatchFile="extra.patch" 224 | else 225 | extraPatchFile="" 226 | fi 227 | 228 | # Check for an environment-specific patch. 229 | if [ -f "${selected_env}.patch" ]; then 230 | envPatchFile="${selected_env}.patch" 231 | else 232 | envPatchFile="" 233 | fi 234 | 235 | # Prepare an array of menu options and corresponding actions. 236 | options=() 237 | actions=() 238 | 239 | # --- Option 1: No modifications --- 240 | options+=("No modifications") 241 | actions+=("echo 'No modifications selected.'") 242 | 243 | enableRHAction="$(cat <<'EOF' 244 | find . -type f -name "platformio.ini" | while read -r file; do 245 | if grep -q -- "-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1" "$file"; then 246 | echo "Processing: $file" 247 | sed -i 's/-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1/-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=0/g' "$file" 248 | fi 249 | done 250 | echo "All platformio.ini files have been updated." 251 | EOF 252 | )" 253 | 254 | # --- Option 2: Enable Remote Hardware --- 255 | options+=("Enable Remote Hardware") 256 | actions+=("$enableRHAction") 257 | 258 | # --- Option 3: Apply extra patch only --- 259 | if [ -n "$extraPatchFile" ]; then 260 | options+=("Apply $extraPatchFile") 261 | actions+=("echo 'Applying $extraPatchFile...' && git apply $extraPatchFile") 262 | fi 263 | 264 | # --- Option 4: Enable Remote Hardware + apply extra patch --- 265 | # Only add if extra patch exists and no env patch exists, 266 | # OR if both exist we’ll add a dedicated option later. 267 | if [ -n "$extraPatchFile" ] && [ -z "$envPatchFile" ]; then 268 | options+=("Enable Remote Hardware + apply $extraPatchFile") 269 | actions+=("$enableRHAction 270 | echo 'Applying $extraPatchFile...' 271 | git apply $extraPatchFile") 272 | fi 273 | 274 | # --- Option 5: Enable Remote Hardware + apply extra patch + apply env patch --- 275 | if [ -n "$extraPatchFile" ] && [ -n "$envPatchFile" ]; then 276 | options+=("Enable Remote Hardware + apply $extraPatchFile + apply ${envPatchFile}") 277 | actions+=("$enableRHAction 278 | echo 'Applying $extraPatchFile...' 279 | git apply $extraPatchFile 280 | echo 'Applying ${envPatchFile}...' 281 | git apply ${envPatchFile}") 282 | fi 283 | 284 | # --- Option 6: Apply env patch only --- 285 | # Two cases: when extra patch is absent OR as an additional option if both exist. 286 | if [ -n "$envPatchFile" ]; then 287 | # If extra patch is absent, this will be the only env option. 288 | options+=("Apply ${envPatchFile}") 289 | actions+=("echo 'Applying ${envPatchFile}...' 290 | git apply ${envPatchFile}") 291 | fi 292 | 293 | # Display the dynamic menu. 294 | echo "Select an option:" 295 | for i in "${!options[@]}"; do 296 | printf "%d) %s\n" $((i+1)) "${options[$i]}" 297 | done 298 | 299 | read -rp "Enter your choice (1-${#options[@]}): " choice 300 | 301 | # Validate the choice. 302 | if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#options[@]}" ]; then 303 | echo "Invalid choice. Exiting." 304 | exit 1 305 | fi 306 | 307 | selected_index=$((choice - 1)) 308 | echo "Executing: ${options[$selected_index]}" 309 | eval "${actions[$selected_index]}" 310 | 311 | 312 | 313 | VERSION=$(bin/buildinfo.py long) 314 | if grep -q "DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=0" platformio.ini; then 315 | VERSION="${VERSION::-4}GPIO" 316 | fi 317 | 318 | # The shell vars the build tool expects to find 319 | export APP_VERSION=$VERSION 320 | OUTDIR="release/$VERSION/$selected_env" 321 | 322 | rm -f "${OUTDIR:?}"/firmware* > /dev/null 2>&1 || true 323 | if [ -d "${OUTDIR:?}" ]; then 324 | rm -r "${OUTDIR:?}"/* > /dev/null 2>&1 || true 325 | fi 326 | mkdir -p "$OUTDIR" 327 | 328 | basename=firmware-$selected_env-$VERSION 329 | 330 | 331 | rm -f .pio/build/"$selected_env"/firmware.* 332 | 333 | if [ -z "$env_arg" ]; then 334 | read -rp "Target: $basename. Press Enter to continue..." 335 | fi 336 | 337 | newpath=0 338 | if ! command -v platformio &>/dev/null; then 339 | pipx install "platformio" 340 | newpath=1 341 | fi 342 | if [ $newpath -eq 1 ]; then 343 | pipx ensurepath 344 | # shellcheck disable=SC1091 345 | source "$HOME/.bashrc" 346 | fi 347 | 348 | platformio pkg update -e "$selected_env" 349 | echo "Building for $selected_env with PLATFORMIO_BUILD_FLAGS: > $PLATFORMIO_BUILD_FLAGS <" 350 | 351 | pio run --environment "$selected_env" 352 | SRCELF=.pio/build/"$selected_env"/firmware.elf 353 | cp "$SRCELF" "$OUTDIR"/"$basename".elf 354 | 355 | if [ -f .pio/build/"$selected_env"/firmware.factory.bin ]; then 356 | echo "Copying ESP32 bin file" 357 | SRCBIN=.pio/build/"$selected_env"/firmware.factory.bin 358 | cp "$SRCBIN" "$OUTDIR"/"$basename".bin 359 | fi 360 | 361 | if [ -f .pio/build/"$selected_env"/firmware.bin ]; then 362 | echo "Copying ESP32 update bin file" 363 | SRCBIN=.pio/build/"$selected_env"/firmware.bin 364 | cp "$SRCBIN" "$OUTDIR"/"$basename"-update.bin 365 | fi 366 | 367 | if [ -f .pio/build/"$selected_env"/firmware.zip ]; then 368 | echo "Generating NRF52 dfu file" 369 | DFUPKG=.pio/build/"$selected_env"/firmware.zip 370 | cp "$DFUPKG" "$OUTDIR/$basename-ota.zip" 371 | fi 372 | 373 | if [ -f .pio/build/"$selected_env"/firmware.hex ]; then 374 | echo "Generating NRF52 uf2 file" 375 | SRCHEX=.pio/build/"$selected_env"/firmware.hex 376 | fi 377 | 378 | if [ -n "${SRCHEX:-}" ]; then 379 | bin/uf2conv.py "$SRCHEX" -c -o "$OUTDIR/$basename.uf2" -f 0xADA52840 380 | cp bin/*.uf2 "$OUTDIR" 381 | else 382 | echo "Building Filesystem with web server for ESP32 targets" 383 | pio run --environment "$selected_env" -t buildfs || true 384 | cp .pio/build/"$selected_env"/littlefs.bin "$OUTDIR"/littlefswebui-"$selected_env"-"$VERSION".bin 385 | 386 | echo "Building Filesystem only for ESP32 targets" 387 | # Remove webserver files from the filesystem and rebuild 388 | 389 | if [ -d data/static ]; then 390 | ls -l data/static # Diagnostic list of files 391 | rm -rf data/static 392 | else 393 | echo "data/static not found, skipping cleanup" 394 | fi 395 | 396 | pio run --environment "$selected_env" -t buildfs || true 397 | cp .pio/build/"$selected_env"/littlefs.bin "$OUTDIR"/littlefs-"$selected_env"-"$VERSION".bin 398 | fi 399 | cp bin/device-install.* "$OUTDIR"/ 400 | cp bin/device-update.* "$OUTDIR"/ 401 | 402 | rm "$OUTDIR"/"$basename".elf 403 | 404 | find "$OUTDIR" -maxdepth 1 -type f -exec du -h {} \; \ 405 | | sed 's|^\./||' \ 406 | | awk '{print $1, $2}' \ 407 | | sort -k2 \ 408 | | column -t 409 | 410 | if [ -f "$VPN_INFO" ]; then 411 | # Trap SIGINT (Ctrl-C) to kill all child processes and exit. 412 | trap 'echo "Interrupted by Ctrl-C. Exiting."; kill 0; exit 1' SIGINT 413 | 414 | while IFS= read -r line || [ -n "$line" ]; do 415 | # Skip empty lines or comments 416 | [[ -z "$line" || "$line" =~ ^# ]] && continue 417 | 418 | echo "" 419 | echo "=== Processing: $line ===" 420 | 421 | # Parse optional embedded password: user:pass@host vs user@host 422 | if [[ "$line" =~ ^([^:]+):([^@]+)@(.+)$ ]]; then 423 | user="${BASH_REMATCH[1]}" 424 | PASSWORD="${BASH_REMATCH[2]}" 425 | host="${BASH_REMATCH[3]}" 426 | connection="$user@$host" 427 | echo "[DEBUG] Using embedded password for $connection" >&2 428 | else 429 | connection="$line" 430 | # Prompt once per connection 431 | read -rp "Enter password for $connection (or 'skip'): " PASSWORD < /dev/tty 432 | # erase prompt 433 | printf "\r"; tput cuu1; tput el 434 | fi 435 | 436 | # If they typed 'skip' or left it blank, we skip this connection 437 | if [ "$PASSWORD" = "skip" ] || [ -z "$PASSWORD" ]; then 438 | echo "Skipped $connection." 439 | continue 440 | fi 441 | 442 | # Now do the SCP loop 443 | for file in "$OUTDIR"/*; do 444 | [ -f "$file" ] || continue 445 | basefile=$(basename "$file") 446 | local_md5=$(md5sum "$file" | awk '{print $1}') 447 | attempt=1 448 | success=0 449 | 450 | while [ $attempt -le $MAX_ATTEMPTS ]; do 451 | echo -n "$attempt: $basefile -> $connection ..." 452 | printf "\r" 453 | 454 | # ensure remote dir 455 | sshpass -p "$PASSWORD" ssh -n -o StrictHostKeyChecking=no \ 456 | "$connection" "mkdir -p ~/meshfirmware/meshtastic_firmware/${VERSION}/" 457 | 458 | timeout --foreground $SCP_TIMEOUT \ 459 | sshpass -p "$PASSWORD" scp -o StrictHostKeyChecking=no \ 460 | "$file" "${connection}:~/meshfirmware/meshtastic_firmware/${VERSION}/" < /dev/null 461 | scp_status=$? 462 | 463 | [ $scp_status -ne 0 ] && { ((attempt++)); continue; } 464 | 465 | remote_md5=$(sshpass -p "$PASSWORD" ssh -n -o StrictHostKeyChecking=no \ 466 | "$connection" "md5sum ~/meshfirmware/meshtastic_firmware/${VERSION}/${basefile} 2>/dev/null" \ 467 | | awk '{print $1}') 468 | 469 | if [ "$local_md5" = "$remote_md5" ]; then 470 | echo "$basefile copied to $connection (MD5 matched)." 471 | success=1 472 | break 473 | else 474 | echo "MD5 mismatch for $basefile on $connection. Retrying..." 475 | ((attempt++)) 476 | fi 477 | done 478 | 479 | if [ $success -ne 1 ]; then 480 | echo "Failed to copy $basefile to $connection after $MAX_ATTEMPTS attempts." 481 | fi 482 | done 483 | 484 | echo "Finished processing $connection." 485 | done < "$VPN_INFO" 486 | fi 487 | 488 | echo "${ORIG_DIR:?}/firmware.sh" 489 | if [[ -f "${ORIG_DIR:?}/firmware.sh" ]]; then 490 | # ensure the target directory exists 491 | echo "Making dir ${ORIG_DIR:?}/meshtastic_firmware/${VERSION}" 492 | mkdir -p "${ORIG_DIR:?}/meshtastic_firmware/${VERSION}" 493 | 494 | for file in "$OUTDIR"/*; do 495 | [[ -f "$file" ]] || continue 496 | basefile=${file##*/} # same as basename 497 | cp -- "$file" "${ORIG_DIR:?}/meshtastic_firmware/${VERSION}/$basefile" 498 | done 499 | fi 500 | -------------------------------------------------------------------------------- /firmware.cmd: -------------------------------------------------------------------------------- 1 | # 2>NUL & @powershell -nop -ep bypass "(gc '%~f0')-join[Environment]::NewLine|iex" & goto :eof 2 | 3 | #Example execute: 4 | # powershell -ExecutionPolicy ByPass -File c:\git\meshfirmware\firmware.ps1 5 | 6 | # Typical defaults: gray text on a dark-blue (or black) background 7 | $Host.UI.RawUI.ForegroundColor = 'Gray' 8 | $Host.UI.RawUI.BackgroundColor = 'Black' # or 'DarkBlue' if you prefer 9 | 10 | 11 | Write-Host "" 12 | Write-Host "" 13 | Write-Host "" 14 | Write-Host "" 15 | 16 | # Flag to track if Ctrl-C has been pressed 17 | $scriptOver = $false 18 | 19 | # Register a Ctrl-C handler: 20 | $null = Register-ObjectEvent -InputObject ([System.Console]) -EventName CancelKeyPress -Action { 21 | # $EventArgs is in scope here: 22 | $EventArgs.Cancel = $true 23 | 24 | if (-not $scriptOver) { 25 | # First Ctrl-C press: prompt user 26 | Write-Host "`nCaught Ctrl-C." -ForegroundColor Yellow 27 | $scriptOver = $true 28 | Read-Host "Press Enter to exit (via Ctrl-C)" 29 | } else { 30 | # Second Ctrl-C press: exit without prompt 31 | Write-Host "`nExiting script..." -ForegroundColor Red 32 | exit 33 | } 34 | } 35 | 36 | $ScriptPath = $PSScriptRoot 37 | if ([string]::IsNullOrEmpty($ScriptPath)) { 38 | $ScriptPath = (Get-Location).Path 39 | } 40 | 41 | $pythonCommand = "" 42 | $timeoutMeshtastic = 10 # Timeout duration in seconds 43 | $baud = 1200 # 115200 44 | $CACHE_TIMEOUT_SECONDS=6 * 3600 # 6 hours 45 | 46 | $GITHUB_API_URL="https://api.github.com/repos/meshtastic/firmware/releases" 47 | $REPO_API_URL="https://api.github.com/repos/meshtastic/meshtastic.github.io/contents" 48 | $WEB_HARDWARE_LIST_URL="https://raw.githubusercontent.com/meshtastic/web-flasher/refs/heads/main/public/data/hardware-list.json" 49 | $PORTABLE_PYTHON_URL="https://api.github.com/repos/winpython/winpython/releases/latest" 50 | 51 | $FIRMWARE_ROOT="${ScriptPath}\meshtastic_firmware" 52 | $PORTABLE_PYTHON_DIR="${ScriptPath}\meshtastic_firmware\winpython" 53 | $DOWNLOAD_DIR="${ScriptPath}\meshtastic_firmware\downloads" 54 | $RELEASES_FILE="${ScriptPath}\meshtastic_firmware\releases.json" 55 | $HARDWARE_LIST="${ScriptPath}\meshtastic_firmware\hardware-list.json" 56 | $BLEOTA_FILE="${ScriptPath}\meshtastic_firmware\bleota.json" 57 | 58 | $VERSIONS_TAGS_FILE="${ScriptPath}\meshtastic_firmware\01versions_tags.txt" 59 | $VERSIONS_LABELS_FILE="${ScriptPath}\meshtastic_firmware\02versions_labels.txt" 60 | $CHOSEN_TAG_FILE="${ScriptPath}\meshtastic_firmware\03chosen_tag.txt" 61 | $MATCHING_FILES_FILE="${ScriptPath}\meshtastic_firmware\07matching_files.txt" 62 | $ARCHITECTURE_FILE="${ScriptPath}\meshtastic_firmware\11architecture.txt" 63 | 64 | 65 | 66 | $cleanupFiles = @( 67 | $VERSIONS_TAGS_FILE, 68 | $VERSIONS_LABELS_FILE, 69 | $CHOSEN_TAG_FILE, 70 | $MATCHING_FILES_FILE, 71 | $ARCHITECTURE_FILE 72 | ) 73 | 74 | # delete any that exist 75 | foreach ($f in $cleanupFiles) { 76 | if (Test-Path $f) { 77 | Remove-Item $f -Force -ErrorAction Ignore | Out-Null 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | function NormalizeString { 85 | [CmdletBinding()] 86 | param( 87 | [Parameter(Mandatory, ValueFromPipeline = $true)] 88 | [string] $InputString 89 | ) 90 | process { 91 | # remove -, _, and any whitespace, then lowercase 92 | ($InputString -replace '[-_\s]', '').ToLower() 93 | } 94 | } 95 | 96 | # helper to convert a byte count into KB/MB/GB/TB automatically 97 | function FormatSize { 98 | param([ulong]$Bytes) 99 | $x = switch ($Bytes) { 100 | { $_ -ge 1TB } { "{0:N2} TB" -f ($Bytes/1TB); break } 101 | { $_ -ge 1GB } { "{0:N2} GB" -f ($Bytes/1GB); break } 102 | { $_ -ge 1MB } { "{0:N2} MB" -f ($Bytes/1MB); break } 103 | { $_ -ge 1KB } { "{0:N2} KB" -f ($Bytes/1KB); break } 104 | default { "$Bytes bytes" } 105 | } 106 | return $x 107 | } 108 | 109 | 110 | function GetPortablePython { 111 | $rel = Invoke-RestMethod -Uri $PORTABLE_PYTHON_URL -Headers @{ 'User-Agent' = 'PowerShell' } -ErrorAction Stop 112 | 113 | # pick the newest 64-bit portable ZIP (e.g. Winpython64-3.13.3.0dot.zip) 114 | $asset = $rel.assets | 115 | Where-Object { $_.name -match 'Winpython64-.*dot\.zip$' } | 116 | Sort-Object -Property name -Descending | 117 | Select-Object -First 1 118 | 119 | if (-not $asset) { 120 | throw "No matching zip found in the latest release of $repo." 121 | } 122 | 123 | $target = Join-Path $FIRMWARE_ROOT $asset.name 124 | # make sure the directory exists 125 | if (-not (Test-Path $FIRMWARE_ROOT)) { 126 | New-Item -ItemType Directory -Path $FIRMWARE_ROOT -Force | Out-Null 127 | } 128 | 129 | if (-not (Test-Path $target) -or ((Get-Item $target).Length -eq 0)) { 130 | Write-Host "Downloading $($asset.name) $($asset.browser_download_url) $target" 131 | Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $target -UseBasicParsing -Headers @{ 'User-Agent' = 'PowerShell' } -ErrorAction Stop 132 | } 133 | 134 | # 1) unzip to a temp workspace 135 | $tempDir = Join-Path $env:TEMP ("wpytmp_" + [guid]::NewGuid()) 136 | Expand-Archive -LiteralPath $target -DestinationPath $tempDir -Force 137 | 138 | # 2) locate license.txt ***inside the extracted tree*** 139 | $license = Get-ChildItem -Path $tempDir -Recurse -Filter license.txt ` 140 | | Select-Object -First 1 141 | if (-not $license) { 142 | Remove-Item $tempDir -Recurse -Force 143 | throw "license.txt not found in the WinPython zip aborting." 144 | } 145 | 146 | $zipRootDir = $license.Directory.FullName # 'level where license.txt is found' 147 | 148 | # 3) make final destination & move contents 149 | New-Item -ItemType Directory -Path $PORTABLE_PYTHON_DIR -Force | Out-Null 150 | Move-Item -Path (Join-Path $zipRootDir '*') -Destination $PORTABLE_PYTHON_DIR -Force 151 | 152 | # 4) clean up the temp workspace 153 | Remove-Item $tempDir -Recurse -Force 154 | } 155 | 156 | # Function to fetch the latest stable Python version from GitHub 157 | function Get-LatestPythonVersion { 158 | $url = "https://api.github.com/repos/actions/python-versions/releases/latest" 159 | $release = Invoke-RestMethod -Uri $url -Headers @{Accept = "application/vnd.github.v3+json"} 160 | $latestVersion = $release.tag_name 161 | return $latestVersion 162 | } 163 | 164 | function get_esptool_cmd() { 165 | $esptoolPath = Get-Command esptool -ErrorAction SilentlyContinue 166 | if ($esptoolPath) { 167 | # If esptool is found, set the ESPTOOL command 168 | $ESPTOOL_CMD = "esptool" # Set esptool command 169 | } else { 170 | try { 171 | # Check if Python is installed and get the version 172 | $pythonVersion = & $pythonCommand --version 173 | Write-Progress -Status "Checking Versions" -Activity "Python interpreter found: $pythonVersion" 174 | # Set the ESPTOOL command to use Python 175 | $ESPTOOL_CMD = "$pythonCommand -m esptool" # Construct as a single string 176 | } 177 | 178 | catch { 179 | $ESPTOOL_CMD = "python -m esptool" # Fallback to Python esptool 180 | } 181 | } 182 | 183 | $run = run_cmd "$ESPTOOL_CMD version" 184 | $esptoolVersion = $run | Select-Object -Last 1 185 | if ($pythonVersion) { 186 | Write-Progress -Status "Checking Versions" -Activity "Python interpreter found: $pythonVersion esptool version: $esptoolVersion" 187 | } 188 | else { 189 | Write-Progress -Status "Checking Versions" -Activity "esptool version: $esptoolVersion" 190 | } 191 | 192 | 193 | return $ESPTOOL_CMD 194 | } 195 | 196 | if (-not ([type]::GetType('NativeMethods', $false))) { 197 | Add-Type -TypeDefinition @' 198 | using System; 199 | using System.Runtime.InteropServices; 200 | public static class NativeMethods { 201 | [DllImport("shell32.dll", SetLastError = true)] 202 | public static extern IntPtr CommandLineToArgvW( 203 | [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, 204 | out int pNumArgs); 205 | 206 | [DllImport("kernel32.dll", SetLastError = true)] 207 | public static extern IntPtr LocalFree(IntPtr hMem); 208 | } 209 | '@ 210 | } 211 | 212 | function Split-CommandLine { 213 | param([string]$cmd) 214 | 215 | [int] $argc = 0 216 | [IntPtr]$argv = [NativeMethods]::CommandLineToArgvW($cmd, [ref]$argc) 217 | if ($argv -eq [IntPtr]::Zero) { throw "Cannot parse: $cmd" } 218 | 219 | try { 220 | # Copy the unmanaged pointer array into a managed IntPtr[] 221 | $ptrArr = New-Object IntPtr[] $argc 222 | [Runtime.InteropServices.Marshal]::Copy($argv, $ptrArr, 0, $argc) 223 | 224 | # Convert each pointer to a managed string 225 | $ptrArr | ForEach-Object { 226 | [Runtime.InteropServices.Marshal]::PtrToStringUni($_) 227 | } 228 | } 229 | finally { 230 | [NativeMethods]::LocalFree($argv) | Out-Null 231 | } 232 | } 233 | 234 | function run_cmd { 235 | param( 236 | [Parameter(Mandatory)][string] $CommandLine, 237 | [switch] $Stream # -Stream → live to console 238 | ) 239 | 240 | 241 | # --- split exe + args ----------------------------------------------- 242 | $parts = Split-CommandLine $CommandLine 243 | $exe = $parts[0] 244 | $args = if ($parts.Count -gt 1) { $parts[1..($parts.Count-1)] } else { @() } 245 | Write-Progress -Activity "$exe" -Status "$args" 246 | 247 | # --- stream or capture ---------------------------------------------- 248 | if ($Stream) { 249 | & $exe @args 2>&1 | Write-Host 250 | Write-Progress -Activity " " -Status " " -Completed 251 | return 252 | } 253 | 254 | $output = & $exe @args 2>&1 | Out-String # capture as ONE string 255 | Write-Progress -Activity " " -Status " " -Completed 256 | return $output.TrimEnd() 257 | } 258 | 259 | 260 | function check_requirements() { 261 | # Check if Python is installed 262 | $null = & python --version 2>$null 263 | if ($LASTEXITCODE -eq 0) { 264 | $global:pythonCommand = "python" 265 | } 266 | else { 267 | $testPythonCommand = "$PORTABLE_PYTHON_DIR\python\python.exe" 268 | if (Test-Path -Path $testPythonCommand -PathType Leaf) { 269 | $null = & $testPythonCommand --version 2>$null 270 | if ($LASTEXITCODE -eq 0) { 271 | $global:pythonCommand = $testPythonCommand 272 | } 273 | } 274 | if ([string]::IsNullOrWhiteSpace($global:pythonCommand)) { 275 | GetPortablePython 276 | 277 | $testPythonCommand = "$PORTABLE_PYTHON_DIR\python\python.exe" 278 | 279 | $null = & $testPythonCommand --version 2>$null 280 | if ($LASTEXITCODE -eq 0) { 281 | $global:pythonCommand = $testPythonCommand 282 | } 283 | } 284 | } 285 | Write-Progress -Activity "Update pip command line tool" 286 | & $pythonCommand -m ensurepip --upgrade *> $null 287 | & $pythonCommand -m pip install --upgrade pip *> $null 288 | 289 | # Check if meshtastic is installed 290 | & $pythonCommand -m pip show meshtastic *> $null 291 | $meshtasticInstalled = ($LASTEXITCODE -eq 0) 292 | if (-not $meshtasticInstalled) { 293 | Write-Host "Meshtastic is not installed. Installing..." 294 | 295 | # Install or upgrade meshtastic using pip3 296 | & $pythonCommand -m pip install --upgrade --no-warn-script-location "meshtastic[cli]" 297 | } 298 | else { 299 | Write-Progress -Activity "Update meshtastic command line tool" 300 | & $pythonCommand -m pip install --upgrade --no-warn-script-location "meshtastic[cli]" | out-null 301 | } 302 | 303 | # Check if esptool is installed 304 | & $pythonCommand -m pip show esptool *> $null 305 | $meshtasticInstalled = ($LASTEXITCODE -eq 0) 306 | if (-not $meshtasticInstalled) { 307 | Write-Host "esptool is not installed. Installing..." 308 | 309 | # Install or upgrade esptool using pip3 310 | & $pythonCommand -m pip install --upgrade --no-warn-script-location "esptool" 311 | } 312 | else { 313 | Write-Progress -Activity "Update esptool command line tool" 314 | & $pythonCommand -m pip install --upgrade --no-warn-script-location "esptool" | out-null 315 | } 316 | 317 | Write-Progress -Activity " " -Status " " -Completed 318 | } 319 | 320 | 321 | function getallUSBCom($output) { 322 | # Get all Serial Ports and filter for USB serial devices by checking Description and DeviceID 323 | #$comDevices = Get-WmiObject Win32_SerialPort 324 | $comDevices = Get-WmiObject -Class Win32_PnPEntity | Where-Object { $_.DeviceID -like "*USB*" -and $_.Name -like "*(com*" } 325 | 326 | # Initialize the array for storing the results 327 | $usbComDevices = @() 328 | 329 | foreach ($device in $comDevices) { 330 | #$device 331 | # Extract COM port from the Name property 332 | if ($device.Name -match 'COM(\d+)') { 333 | $comPort = $matches[0] # The full COM port string like COM3 or COM5 334 | } 335 | 336 | # Split the string by "\" and get the last part 337 | $HardwareID = $device.HardwareID.Split("\")[-1] 338 | 339 | # Add the device information to $usbComDevices 340 | $usbComDevices += [PSCustomObject]@{ 341 | drive_letter = $comPort 342 | device_name = $HardwareID 343 | friendly_name = $device.Name 344 | firmware_revision = "--" 345 | } 346 | } 347 | 348 | return $usbComDevices 349 | } 350 | 351 | function runMeshtasticCommand($selectedComPort, $command) { 352 | # Define a temporary file to capture the output 353 | $tempOutputFile = Join-Path -Path $ScriptPath -ChildPath "meshtastic_output$selectedComPort.txt" 354 | $tempErrorFile = Join-Path -Path $ScriptPath -ChildPath "meshtastic_error$selectedComPort.txt" 355 | 356 | # Start the meshtastic process with a hidden window and capture the process ID 357 | #Write-Host "Running meshtastic command on port $selectedComPort" 358 | Write-Progress -Activity "running $pythonCommand -m meshtastic --port $selectedComPort $command" 359 | $process = Start-Process "$pythonCommand" -ArgumentList " -m meshtastic --port $selectedComPort $command" -PassThru -WindowStyle Hidden -RedirectStandardOutput $tempOutputFile -RedirectStandardError $tempErrorFile 360 | $processWait = $process.WaitForExit($timeoutMeshtastic * 1000) # Timeout is in milliseconds 361 | 362 | 363 | $meshtasticOutput = "" 364 | $meshtasticError = "" 365 | # Check if the process exited within the timeout 366 | if ($processWait) { 367 | # If the process exits within the timeout, capture the output 368 | $meshtasticOutput = Get-Content $tempOutputFile -Raw 369 | $meshtasticError = Get-Content $tempErrorFile -Raw 370 | $process.Dispose() 371 | } else { 372 | # If the process did not exit within the timeout, forcefully kill it 373 | $meshtasticError = "Timed Out" 374 | $process.Kill() 375 | } 376 | Start-Sleep -Seconds 1 377 | 378 | # Clean up: remove temporary files 379 | try { 380 | Remove-Item $tempOutputFile -Force | out-null 381 | Remove-Item $tempErrorFile -Force | out-null 382 | } catch { 383 | Write-Warning "ERROR: Could not delete temporary files. Make sure no other process is using them." 384 | } 385 | return ,$meshtasticOutput, $meshtasticError 386 | } 387 | 388 | function getMeshtasticNodeInfo($selectedComPort) { 389 | $result = runMeshtasticCommand $selectedComPort "--info --no-nodes" 390 | $meshtasticOutput = $result[0] 391 | $meshtasticError = $result[1] 392 | 393 | if ($meshtasticError) { 394 | Write-Host "$selectedComPort error: $meshtasticError" 395 | if ($meshtasticError -eq "Timed Out") { 396 | return "Timed Out" 397 | } 398 | } 399 | 400 | $meshtasticOutput = $meshtasticOutput -replace '(\{|\}|\,)', "$1`n" 401 | 402 | $deviceInfo = New-Object PSObject -property @{ 403 | Name = "" 404 | HWName = "" 405 | HWNameShort = "" 406 | FWVersion = "" 407 | } 408 | 409 | $splitted = $meshtasticOutput -split "`n" 410 | $splitted | ForEach-Object { 411 | # Split each line into key-value pairs 412 | $i = $_ -split ":", 2 413 | if ($i.Count -eq 2) { 414 | # Ensure that the line contains both key and value 415 | $key = $i[0].Trim() -replace '"', "" 416 | $value = $i[1].Trim() 417 | 418 | # Matching keys and storing values 419 | if ($key -like "*Owner*") { 420 | $deviceInfo.Name = $value 421 | } 422 | if ($key -like "*pioEnv*") { 423 | $deviceInfo.HWName = $value -replace '"', "" # Removing any quotes in the value 424 | } 425 | if ($key -like "*hwModel*") { 426 | $deviceInfo.HWNameShort = $value -replace '"', "" # Removing any quotes in the value 427 | } 428 | if ($key -like "*firmwareVersion*") { 429 | $deviceInfo.FWVersion = $value -replace '"', "" # Removing any quotes in the value 430 | } 431 | } 432 | } 433 | if ([string]::IsNullOrWhiteSpace($deviceInfo.Name)) { 434 | return "Timed Out" 435 | } 436 | 437 | return $deviceInfo 438 | } 439 | 440 | function selectUSBCom() { 441 | param ( 442 | [Parameter(Mandatory=$true)] 443 | $availableComPorts 444 | ) 445 | # Display a menu with available COM ports 446 | $validPort = $false 447 | while (-not $validPort) { 448 | # Ask the user to enter the COM port to operate on 449 | $selectedComPort = Read-Host "Enter the COM port to operate on" 450 | 451 | # Normalize the input to ensure both 'COM7' and '7' are valid 452 | if ($selectedComPort -match '^\d+$') { 453 | # If it's just a number, prepend "COM" to it 454 | $selectedComPort = "COM$selectedComPort" 455 | } 456 | 457 | # Check if the selected COM port exists in the list of USB COM devices 458 | if ($availableComPorts -contains $selectedComPort) { 459 | Write-Host "Selected COM port is valid: $selectedComPort" 460 | # Proceed with further operations on the selected COM port 461 | $validPort = $true 462 | } else { 463 | Write-Host "Invalid COM port: $selectedComPort Please select a valid COM port." 464 | } 465 | } 466 | 467 | Write-Progress -Activity " " -Status " " -Completed 468 | return $selectedComPort 469 | } 470 | 471 | function USBDeview() { 472 | 473 | if (-not (Test-Path $usbDeviewPath)) { 474 | Write-Host "USBDeview.exe not found. Downloading and extracting..." 475 | 476 | # Download the zip file 477 | Invoke-WebRequest -Uri $downloadUrl -OutFile $zipFilePath 478 | 479 | # Extract the zip file 480 | Expand-Archive -Path $zipFilePath -DestinationPath $extractFolderPath -Force 481 | 482 | # Check if the extraction was successful and move the exe to the desired location 483 | $extractedExePath = Join-Path -Path $extractFolderPath -ChildPath "USBDeview.exe" 484 | 485 | if (Test-Path $extractedExePath) { 486 | # Move the USBDeview.exe to the ScriptPath 487 | Move-Item -Path $extractedExePath -Destination $usbDeviewPath -Force 488 | Write-Host "USBDeview.exe extracted and moved successfully." 489 | } else { 490 | Write-Host "ERROR: Failed to extract USBDeview.exe." 491 | } 492 | 493 | # Clean up: Remove the downloaded zip file and extracted folder 494 | Remove-Item -Path $zipFilePath -Force 495 | Remove-Item -Path $extractFolderPath -Recurse -Force 496 | } 497 | 498 | # Define a path for the usb_devices.xml file 499 | $usbDevicesOutputPath = Join-Path -Path $ScriptPath -ChildPath "usb_devices.xml" 500 | if (Test-Path $usbDevicesOutputPath) { 501 | Remove-Item -Path $usbDevicesOutputPath -Force 502 | } 503 | 504 | # Run USBDeview and output the connected devices in XML format 505 | Write-Progress -Status "Getting USB Devices" -Activity "Running $ScriptPath\USBDeview.exe /sort DriveLetter /TrayIcon 0 /DisplayDisconnected 0 /sxml $usbDevicesOutputPath" 506 | $usbDevices = Start-Process -FilePath "$ScriptPath\USBDeview.exe" -ArgumentList "/sort DriveLetter /TrayIcon 0 /DisplayDisconnected 0 /sxml $usbDevicesOutputPath" -PassThru -Wait -NoNewWindow 507 | 508 | # Check if the XML output file exists 509 | if (Test-Path $usbDevicesOutputPath) { 510 | # Load the XML file 511 | [xml]$xmlContent = Get-Content -Path $usbDevicesOutputPath 512 | 513 | # Extract and display device information (e.g., drive letter, description, etc.) 514 | $usbDevicesList = $xmlContent.usb_devices_list.item 515 | 516 | # Filter out devices with drive letters starting with 'COM' and display relevant details 517 | $comDevices = $usbDevicesList | Where-Object { $_.drive_letter -like "COM*" } 518 | 519 | return $comDevices 520 | } else { 521 | Write-Host "Error: usb_devices.xml not found. USBDeview was not ran successfully." 522 | exit 523 | } 524 | } 525 | 526 | # Function to get and display the USB devices 527 | function getUsbComDevices() { 528 | [CmdletBinding()] 529 | param( 530 | [switch] $SkipInfo = $false 531 | ) 532 | $usbComDevices = @() 533 | #$comDevices = USBDeview 534 | $comDevices = getallUSBCom 535 | 536 | # Process each device and store the relevant details in $usbComDevices 537 | $comDevices | ForEach-Object { 538 | if (-not $SkipInfo) { 539 | Write-Progress -Status "Checking USB Devices" -Activity "Checking for meshtastic on $($_.drive_letter)" 540 | $deviceInfo = getMeshtasticNodeInfo $_.drive_letter 541 | } 542 | else { 543 | $deviceInfo = "Timed Out" 544 | } 545 | 546 | #if ($deviceInfo -eq "Timed Out") { 547 | # $ESPTOOL_CMD = get_esptool_cmd 548 | # $output = run_cmd "$ESPTOOL_CMD --baud 115200 --port $($_.drive_letter) chip_id" 549 | # Write-Host $output 550 | # Write-Progress -Status "Checking USB Devices" -Activity "Checking for meshtastic on $($_.drive_letter)" 551 | # Start-Sleep -Seconds 5 552 | # $deviceInfo = getMeshtasticNodeInfo $_.drive_letter 553 | #} 554 | 555 | 556 | if ($deviceInfo -eq "Timed Out") { 557 | $usbComDevices += [PSCustomObject]@{ 558 | COMPort = $_.drive_letter 559 | DeviceName = $_.device_name 560 | FriendlyName = $_.friendly_name 561 | FirmwareVersion = $_.firmware_revision 562 | Meshtastic = $deviceInfo 563 | } 564 | } 565 | else { 566 | $usbComDevices += [PSCustomObject]@{ 567 | ComPort = $_.drive_letter 568 | DeviceName = $deviceInfo.HWName 569 | FriendlyName = $deviceInfo.Name 570 | FirmwareVersion = $deviceInfo.FWVersion 571 | Meshtastic = $deviceInfo.HWNameShort 572 | } 573 | } 574 | } 575 | return $usbComDevices 576 | } 577 | 578 | function getUSBComPort() { 579 | [CmdletBinding()] 580 | param( 581 | [switch] $SkipInfo = $false 582 | ) 583 | 584 | $selectedComPort = 0 585 | # Run in a loop until we get valid $comDevices 586 | do { 587 | if ($SkipInfo) { 588 | $usbComDevices = getUsbComDevices -SkipInfo 589 | } 590 | else { 591 | $usbComDevices = getUsbComDevices 592 | } 593 | 594 | # If there are no USB COM devices, display an error and loop again 595 | if ($usbComDevices.Count -eq 0) { 596 | Write-Host "No valid COM devices found. Please check the connection. Trying again in 5 seconds." -ForegroundColor Red 597 | Start-Sleep -Seconds 5 # Wait before trying again 598 | } else { 599 | $availableComPorts = $usbComDevices | Select-Object -ExpandProperty ComPort 600 | if ($availableComPorts.Count -eq 1) { 601 | $meshtasticVersion = $usbComDevices | Select-Object -ExpandProperty FirmwareVersion 602 | $hwModelSlug = $usbComDevices | Select-Object -ExpandProperty Meshtastic 603 | $selectedComPort = $usbComDevices | Select-Object -ExpandProperty ComPort 604 | #Write-Host "$selectedComPort. Version: $meshtasticVersion. Hardware: $hwModelSlug." 605 | } 606 | else { 607 | # If we found valid COM devices, let the user select one 608 | $tableOutput = $usbComDevices | Sort-Object -Property ComPort | Format-Table -Property ComPort, DeviceName, FriendlyName, FirmwareVersion, Meshtastic | Out-String 609 | # Remove lines that are empty or only contain spaces 610 | $tableOutput = $tableOutput -split "`n" | Where-Object { $_.Trim() -ne "" } | Out-String 611 | 612 | Write-Host "" 613 | Write-Host $tableOutput 614 | $selectedComPort = selectUSBCom -availableComPorts $availableComPorts 615 | 616 | # now filter out the single object whose ComPort matches 617 | $device = $usbComDevices | 618 | Where-Object { $_.ComPort -eq $selectedComPort } 619 | 620 | # and pull out the fields you care about 621 | $hwModelSlug = $device.Meshtastic 622 | $meshtasticVersion = $device.FirmwareVersion 623 | 624 | #Write-Host "$selectedComPort. Version: $meshtasticVersion. Hardware: $hwModelSlug." 625 | 626 | } 627 | } 628 | 629 | } while ($usbComDevices.Count -eq 0 -and $selectedComPort -eq 0) # Continue looping until we have at least one valid COM device 630 | 631 | return $selectedComPort, $hwModelSlug, $meshtasticVersion, $usbComDevices 632 | } 633 | 634 | 635 | 636 | 637 | 638 | 639 | # Check for an active internet connection. 640 | function CheckInternet { 641 | $domain = [uri]$GITHUB_API_URL 642 | $domain = $domain.Host 643 | try { 644 | # Ping the domain to check for internet connection. 645 | if (Test-Connection -ComputerName $domain -Count 1 -Quiet) { 646 | return $true 647 | } else { 648 | return $false 649 | } 650 | } catch { 651 | return $false 652 | } 653 | } 654 | 655 | # Update the GitHub release cache if needed. 656 | function UpdateReleases { 657 | if (-not (CheckInternet)) { 658 | Write-Progress -Activity "No internet connection; using cached release data if available." 659 | Return 660 | } 661 | if ((Test-Path $RELEASES_FILE) -and (Get-Date).AddSeconds(-$CACHE_TIMEOUT_SECONDS) -lt (Get-Item $RELEASES_FILE).LastWriteTime) { 662 | Write-Progress -Activity "Using cached release data (up to date within the last 6 hours)." 663 | return 664 | } 665 | 666 | # Create the firmware directory if it doesn't exist 667 | if (-not (Test-Path $FIRMWARE_ROOT)) { 668 | New-Item -ItemType Directory -Path $FIRMWARE_ROOT | Out-Null 669 | } 670 | 671 | # Ensure the directory for $RELEASES_FILE exists 672 | $releasesDir = [System.IO.Path]::GetDirectoryName($RELEASES_FILE) 673 | if (-not (Test-Path $releasesDir)) { 674 | New-Item -ItemType Directory -Path $releasesDir | Out-Null 675 | } 676 | 677 | Write-Progress -Activity "Updating release cache from GitHub..." 678 | # Download into a temp file first 679 | $tmpFile = [System.IO.Path]::GetTempFileName() 680 | try { 681 | Invoke-WebRequest -Uri $GITHUB_API_URL -OutFile $tmpFile -ErrorAction Stop 682 | } catch { 683 | Write-Host "Failed to download release data." 684 | Remove-Item $tmpFile 685 | return 686 | } 687 | 688 | # Check if the downloaded file is valid JSON 689 | try { 690 | $jsonContent = Get-Content $tmpFile | ConvertFrom-Json 691 | } catch { 692 | Write-Host "Downloaded file is not valid JSON. Aborting." 693 | Remove-Item $tmpFile 694 | return 695 | } 696 | 697 | # Filter out "download_count" keys from the JSON. 698 | $filteredTmp = [System.IO.Path]::GetTempFileName() 699 | $jsonContent | ConvertTo-Json -Depth 10 | ForEach-Object { 700 | $_ -replace '"download_count":\s*\d+,', '' 701 | } | Set-Content -Path $filteredTmp 702 | 703 | # Use the filtered JSON for further processing. 704 | if (-not (Test-Path $RELEASES_FILE)) { 705 | Move-Item $filteredTmp $RELEASES_FILE 706 | Remove-Item $tmpFile 707 | } else { 708 | # Compare the MD5 hashes of the cached file and the newly filtered file. 709 | $oldMd5 = Get-FileHash $RELEASES_FILE -Algorithm MD5 710 | $newMd5 = Get-FileHash $filteredTmp -Algorithm MD5 711 | if ($oldMd5.Hash -ne $newMd5.Hash) { 712 | Write-Progress -Activity "Release data changed. Updating cache and removing cached version lists. $($oldMd5.Hash) $($newMd5.Hash)" 713 | Remove-Item $RELEASES_FILE -ErrorAction Ignore | Out-Null 714 | Move-Item $filteredTmp $RELEASES_FILE 715 | Remove-Item $VERSIONS_TAGS_FILE, $VERSIONS_LABELS_FILE -ErrorAction Ignore | Out-Null 716 | } else { 717 | Write-Progress -Activity "Release data is unchanged. $($oldMd5.Hash) $($newMd5.Hash)" 718 | 719 | # Update the LastWriteTime of the RELEASES_FILE to the current time 720 | Set-ItemProperty -Path $RELEASES_FILE -Name LastWriteTime -Value (Get-Date) 721 | 722 | Remove-Item $filteredTmp 723 | } 724 | Remove-Item $tmpFile 725 | } 726 | } 727 | 728 | function UpdateHardwareList { 729 | # Check if the file exists and if it's older than 6 hours 730 | if (-not (Test-Path $HARDWARE_LIST) -or ((Get-Date) - (Get-Item $HARDWARE_LIST).LastWriteTime).TotalMinutes -gt 360) { 731 | Write-Progress -Activity "Downloading resources.ts from GitHub..." 732 | 733 | # Create the directory if it doesn't exist 734 | $directory = [System.IO.Path]::GetDirectoryName($HARDWARE_LIST) 735 | if (-not (Test-Path $directory)) { 736 | New-Item -Path $directory -ItemType Directory 737 | } 738 | 739 | # Download the file 740 | Invoke-WebRequest -Uri $WEB_HARDWARE_LIST_URL -OutFile $HARDWARE_LIST 741 | } 742 | Write-Progress -Activity " " -Status " " -Completed 743 | } 744 | 745 | 746 | 747 | # Function to build the release menu and save version tags and labels. 748 | function BuildReleaseMenuData { 749 | $tmpfile = New-TemporaryFile 750 | 751 | $ReleasesJson = Get-Content -Path "$RELEASES_FILE" -Raw 752 | $ReleasesJson = $ReleasesJson -replace '[^\x00-\x7F]', '' # Remove non-ASCII characters. 753 | 754 | # Parse the JSON manually 755 | $jsonData = $ReleasesJson | ConvertFrom-Json 756 | 757 | # Loop through each release to build the entries. 758 | foreach ($release in $jsonData) { 759 | $tag = $release.tag_name 760 | $prerelease = $release.prerelease 761 | $draft = $release.draft 762 | $body = $release.body 763 | $created_at = $release.created_at 764 | 765 | $suffix = "" 766 | $date = $created_at 767 | 768 | if ($tag -match "[Aa]lpha") { 769 | $suffix = "$suffix (alpha)" 770 | } elseif ($tag -match "[Bb]eta") { 771 | $suffix = "$suffix (beta)" 772 | } elseif ($tag -match "[Rr][Cc]") { 773 | $suffix = "$suffix (rc)" 774 | } 775 | 776 | if ($draft -eq $true) { 777 | $suffix = "$suffix (draft)" 778 | } elseif ($prerelease -eq $true) { 779 | $suffix = "$suffix (pre-release)" 780 | } 781 | 782 | $tag = $tag.Substring(1) # Remove the 'v' from the version tag 783 | $label = "{0,-14} {1}" -f $tag, $suffix 784 | 785 | if ($body -match '⚠️') { 786 | $label = "! $label" 787 | } elseif ($body -match 'Known issue') { 788 | $label = "! $label" 789 | } elseif ($body -match 'Revocation') { 790 | $label = "! $label" 791 | } 792 | else { 793 | $label = " $label" 794 | } 795 | 796 | # Write the entry to the temporary file. 797 | "$date`t$tag`t$label" | Out-File -Append -FilePath $tmpfile 798 | } 799 | 800 | # Check if any subdirectory name in FIRMWARE_ROOT (skip "downloads") is not in the tag_names from above. 801 | Get-ChildItem -Path $FIRMWARE_ROOT -Directory | ForEach-Object { 802 | $folder = $_ 803 | if ($folder.PSIsContainer -and $folder.Name -ne "downloads" -and $folder.Name -ne "winpython") { 804 | $folderName = $folder.Name.ToLower() 805 | 806 | if ($folderName -match "^v") { 807 | $folderName = $folderName.Substring(1) 808 | } 809 | 810 | $found = $false 811 | $content = Get-Content -Path $tmpfile 812 | foreach ($line in $content) { 813 | if ($line -match $folderName) { 814 | $found = $true 815 | break 816 | } 817 | } 818 | 819 | if (-not $found) { 820 | $firstFile = Get-ChildItem -Path $folder.FullName -File -Filter "firmware-*" | Select-Object -First 1 821 | if ($firstFile) { 822 | $mtime = (Get-Date (Get-Item $firstFile.FullName).LastWriteTime -UFormat "%Y-%m-%dT%H:%M:%SZ") 823 | } else { 824 | $mtime = (Get-Date (Get-Item $folder.FullName).LastWriteTime -UFormat "%Y-%m-%dT%H:%M:%SZ") 825 | } 826 | 827 | $label = "! $folderName $mtime (nightly)" 828 | "$mtime`t$folderName`t$label" | Out-File -Append -FilePath $tmpfile 829 | } 830 | } 831 | } 832 | 833 | # Sort all entries by date in descending order (newest first) 834 | $sortedEntries = Get-Content -Path $tmpfile | Sort-Object -Descending 835 | 836 | # Build arrays from the sorted entries. 837 | $versionsTags = @() 838 | $versionsLabels = @() 839 | 840 | foreach ($entry in $sortedEntries) { 841 | $fields = $entry -split "`t" 842 | $versionsTags += $fields[1] 843 | $versionsLabels += $fields[2] 844 | } 845 | 846 | # Save the arrays for later use. 847 | $versionsTags | Out-File -FilePath $VERSIONS_TAGS_FILE 848 | $versionsLabels | Out-File -FilePath $VERSIONS_LABELS_FILE 849 | } 850 | 851 | 852 | 853 | 854 | function SelectRelease { 855 | param([string] $VersionArg) 856 | 857 | Write-Progress -Activity " " -Status " " -Completed 858 | 859 | # load the cached lists 860 | $versionsTags = Get-Content $VERSIONS_TAGS_FILE 861 | $versionsLabels = Get-Content $VERSIONS_LABELS_FILE 862 | $count = $versionsLabels.Count 863 | if ($count -eq 0) { throw "No releases cached." } 864 | 865 | # find first stable index 866 | $latestStableIndex = 0 867 | for ($i = 0; $i -lt $count; $i++) { 868 | if ($versionsLabels[$i] -notlike '! *' -and $versionsLabels[$i] -notlike '* (pre-release)*') { 869 | $latestStableIndex = $i 870 | break 871 | } 872 | } 873 | 874 | if ($VersionArg) { 875 | $chosen = $versionsTags.FindIndex({ $_ -like "*$VersionArg*" }) 876 | if ($chosen -lt 0) { throw "No matching release for '$VersionArg'" } 877 | } 878 | else { 879 | # layout maths 880 | $termWidth = $Host.UI.RawUI.WindowSize.Width 881 | $maxLabelLength = ($versionsLabels | Measure-Object Length -Maximum).Maximum 882 | $indexWidth = $count.ToString().Length 883 | $colLabelWidth = $maxLabelLength + 2 884 | $colWidth = $indexWidth + 2 + $colLabelWidth + 8 885 | $numPerRow = [Math]::Max(1, [int]($termWidth / $colWidth)) 886 | 887 | # 1) BUILD the array of PSCustomObject{text, color} 888 | $formatted = for ($i = 0; $i -lt $count; $i++) { 889 | $label = $versionsLabels[$i].Trim() 890 | $text = "{0:D$indexWidth}) {1,-$colLabelWidth}" -f ($i+1), $label 891 | 892 | # pick a color 893 | if ($label -match '[Nn]ightly') { 894 | $color = 'Red' 895 | } 896 | elseif ($i -eq $latestStableIndex) { 897 | $color = 'Cyan' 898 | } 899 | elseif ($label -match '\(pre-release\)' -and -not $script:PreColored) { 900 | $script:PreColored = $true 901 | $color = 'Yellow' 902 | } 903 | elseif ($label -notmatch '\(pre-release\)' -and -not $script:StableColored) { 904 | $script:StableColored = $true 905 | $color = 'Green' 906 | } 907 | else { 908 | $color = 'White' 909 | } 910 | 911 | [PSCustomObject]@{ Text = $text; Color = $color } 912 | } 913 | 914 | # 2) reverse in-place (oldest → newest) 915 | [Array]::Reverse($formatted) 916 | 917 | # 3) print in rows unchanged 918 | $row = 0 919 | foreach ($item in $formatted) { 920 | Write-Host -NoNewline $item.Text -ForegroundColor $item.Color 921 | $row++ 922 | if ($row % $numPerRow -eq 0) { Write-Host } 923 | } 924 | if ($row % $numPerRow -ne 0) { Write-Host } 925 | 926 | # 4) prompt 927 | do { 928 | $sel = Read-Host -Prompt "Enter number of your selection (1-$count)" 929 | $sel = $sel.TrimStart('0') 930 | $sel = [int]$sel 931 | 932 | $tag = '' 933 | try { 934 | $tag = $versionsLabels[$sel-1].Trim().TrimStart('!').Trim() 935 | if ($tag -match '^\S+') { 936 | $tag = $matches[0].Trim() 937 | } 938 | } 939 | catch {$tag = ''} 940 | } until (-not ([string]::IsNullOrEmpty($tag))) 941 | } 942 | 943 | # save & return 944 | Write-Host "Picked $tag" 945 | $tag | Out-File -Encoding ascii -NoNewline $CHOSEN_TAG_FILE 946 | return $tag 947 | } 948 | 949 | 950 | 951 | 952 | function DownloadAssets { 953 | <# 954 | .SYNOPSIS 955 | Download all firmware-* assets for the chosen release (skipping any “debug” builds). 956 | #> 957 | 958 | # 1) Read & parse the cached release JSON 959 | $jsonRaw = Get-Content -Path $RELEASES_FILE -Raw 960 | $jsonRaw = $jsonRaw -replace '[^\x00-\x7F]', '' 961 | try { 962 | $allReleases = $jsonRaw | ConvertFrom-Json 963 | } catch { 964 | Throw "Failed to parse JSON from '$RELEASES_FILE': $_" 965 | } 966 | 967 | # 2) Determine the chosen tag 968 | $chosenTag = (Get-Content -Path $CHOSEN_TAG_FILE -Raw).Trim() 969 | if (-not $chosenTag) { 970 | Throw "No chosen tag found in '$CHOSEN_TAG_FILE'." 971 | } 972 | $downloadPattern = "-$chosenTag" 973 | 974 | # 3) Find the release object 975 | $release = $allReleases | 976 | Where-Object { $_.tag_name.TrimStart('v') -eq $chosenTag } | 977 | Select-Object -First 1 978 | if (-not $release) { 979 | Throw "Release with tag '$chosenTag' not found." 980 | } 981 | 982 | # 4) Filter its assets 983 | $assets = $release.assets | 984 | Where-Object { $_.name -match '^firmware-' -and $_.name -notmatch 'debug' } 985 | if (-not $assets) { 986 | Throw "No firmware assets found for release '$chosenTag'." 987 | } 988 | 989 | # 5) Prepare download dir & remove stale temps 990 | if (-not (Test-Path $DOWNLOAD_DIR)) { 991 | New-Item -Path $DOWNLOAD_DIR -ItemType Directory | Out-Null 992 | } 993 | Get-ChildItem -Path $DOWNLOAD_DIR -Filter '*.tmp*' -File | 994 | Remove-Item -Force 995 | 996 | # 6) Download loop 997 | $hadExisting = $false 998 | foreach ($asset in $assets) { 999 | $name = $asset.name 1000 | $url = $asset.browser_download_url 1001 | $dest = Join-Path $DOWNLOAD_DIR $name 1002 | 1003 | if (Test-Path $dest) { 1004 | Write-Progress -Activity "Already have $name" 1005 | $hadExisting = $true 1006 | continue 1007 | } 1008 | 1009 | if ($hadExisting) { Write-Host ""; $hadExisting = $false } 1010 | 1011 | $tmpFile = Join-Path $DOWNLOAD_DIR ("{0}.tmp" -f ([Guid]::NewGuid())) 1012 | try { 1013 | Invoke-WebRequest -Uri $url -OutFile $tmpFile -UseBasicParsing -ErrorAction Stop 1014 | Move-Item -Path $tmpFile -Destination $dest -Force 1015 | } catch { 1016 | Write-Host "Failed to download $name" -ForegroundColor Red 1017 | Remove-Item -Path $tmpFile -ErrorAction SilentlyContinue 1018 | } 1019 | } 1020 | 1021 | # 7) Save the pattern marker 1022 | Write-Progress -Activity " " -Status " " -Completed 1023 | } 1024 | 1025 | 1026 | 1027 | function UnzipAssets { 1028 | param( 1029 | [string] $ReleasesFile = $RELEASES_FILE, 1030 | [string] $ChosenTagFile = $CHOSEN_TAG_FILE, 1031 | [string] $DownloadDir = $DOWNLOAD_DIR, 1032 | [string] $FirmwareRoot = $FIRMWARE_ROOT 1033 | ) 1034 | 1035 | # 1) Read chosen tag 1036 | $chosenTag = (Get-Content -Path $ChosenTagFile -Raw).Trim() 1037 | if (-not $chosenTag) { 1038 | Throw "No chosen tag found in '$ChosenTagFile'." 1039 | } 1040 | 1041 | # 2) Load & parse all releases 1042 | $jsonRaw = Get-Content -Path $ReleasesFile -Raw 1043 | $jsonRaw = $jsonRaw -replace '[^\x00-\x7F]', '' 1044 | try { 1045 | $allReleases = $jsonRaw | ConvertFrom-Json 1046 | } catch { 1047 | Throw "Failed to parse JSON from '$ReleasesFile': $_" 1048 | } 1049 | 1050 | # 3) Locate the release object by tag (strip leading 'v') 1051 | $release = $allReleases | 1052 | Where-Object { $_.tag_name.TrimStart('v') -eq $chosenTag } | 1053 | Select-Object -First 1 1054 | if (-not $release) { 1055 | Throw "Release '$chosenTag' not found in JSON." 1056 | } 1057 | 1058 | # 4) Filter for firmware-… zip assets (exclude debug) 1059 | $assets = $release.assets | 1060 | Where-Object { 1061 | $_.name -match '^firmware-' -and 1062 | $_.name -notmatch 'debug' 1063 | } 1064 | if (-not $assets) { 1065 | Throw "No matching firmware assets found for release '$chosenTag'." 1066 | } 1067 | 1068 | # 5) Unzip each asset to \\\… 1069 | foreach ($asset in $assets) { 1070 | $name = $asset.name 1071 | $localFile = Join-Path $DownloadDir $name 1072 | 1073 | if ($name -match '^firmware-([^-\s]+)-.+\.zip$') { 1074 | $product = $Matches[1] 1075 | $targetDir = Join-Path $FirmwareRoot "$chosenTag\$product" 1076 | 1077 | # ensure folder exists 1078 | New-Item -Path $targetDir -ItemType Directory -Force | Out-Null 1079 | 1080 | # if empty, unzip 1081 | $hasFiles = Get-ChildItem -Path $targetDir -File -Recurse -ErrorAction SilentlyContinue 1082 | if (-not $hasFiles) { 1083 | Expand-Archive -Path $localFile -DestinationPath $targetDir -Force 1084 | } 1085 | else { 1086 | Write-Progress -Activity "Skipping $name - target folder already populated." 1087 | } 1088 | } 1089 | else { 1090 | Write-Host "Asset '$name' does not match expected naming convention; skipping." 1091 | } 1092 | } 1093 | Write-Progress -Activity " " -Status " " -Completed 1094 | } 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | function GetHardwareInfo { 1102 | [CmdletBinding()] 1103 | param( 1104 | [Parameter(Mandatory)] 1105 | [string] $Slug, 1106 | [Parameter(Mandatory)] 1107 | [string] $ListPath, 1108 | [string] $SelectedFirmwareFile, 1109 | [string] $selectedComPort, 1110 | [string] $HWNameFile, 1111 | [string] $Version 1112 | ) 1113 | 1114 | if (-not (Test-Path $ListPath)) { 1115 | Throw "Hardware list not found at path: $ListPath" 1116 | } 1117 | 1118 | # normalize the lookup slug 1119 | $normSlug = NormalizeString $Slug 1120 | 1121 | # Load & parse JSON 1122 | $hardwareList = Get-Content -Path $ListPath -Raw | ConvertFrom-Json 1123 | 1124 | # Find matching entry 1125 | foreach ($entry in $HardwareList) { 1126 | # build a list of normalized candidate keys for this entry 1127 | $candidates = @( 1128 | NormalizeString $entry.hwModelSlug 1129 | NormalizeString $entry.platformioTarget 1130 | NormalizeString $entry.displayName 1131 | ) 1132 | 1133 | if ($candidates -contains $normSlug) { 1134 | break 1135 | } 1136 | } 1137 | 1138 | if (-not $entry) { 1139 | Throw "No hardware entry found for slug '$Slug'" 1140 | } 1141 | 1142 | # Determine if those optional properties actually exist, otherwise default to $false 1143 | $requiresDfu = if ($entry.PSObject.Properties.Name -contains 'requiresDfu') { $entry.requiresDfu } else { $false } 1144 | $hasInkHud = if ($entry.PSObject.Properties.Name -contains 'hasInkHud') { $entry.hasInkHud } else { $false } 1145 | $hasMui = if ($entry.PSObject.Properties.Name -contains 'hasMui') { $entry.hasMui } else { $false } 1146 | $partitionScheme = if ($entry.PSObject.Properties.Name -contains 'partitionScheme') { $entry.partitionScheme } else { "4MB" } 1147 | 1148 | # Build and return a PSCustomObject 1149 | return [PSCustomObject]@{ 1150 | Slug = $entry.hwModelSlug 1151 | Architecture = $entry.architecture 1152 | DisplayName = $entry.displayName 1153 | RequiresDfu = $requiresDfu 1154 | HasInkHud = $hasInkHud 1155 | HasMui = $hasMui 1156 | FirmwareFile = $SelectedFirmwareFile 1157 | ComPort = $selectedComPort 1158 | HWNameFile = $HWNameFile 1159 | Version = $Version 1160 | FlashSize = $partitionScheme 1161 | } 1162 | } 1163 | 1164 | 1165 | 1166 | function MakeConfigBackup { 1167 | [CmdletBinding()] 1168 | param( 1169 | [Parameter(Mandatory)] 1170 | [string] $HWNameShort, 1171 | 1172 | [Parameter(Mandatory)] 1173 | $selectedComPort 1174 | ) 1175 | 1176 | Write-Host "Making a config backup" 1177 | 1178 | # Generate the backup config name 1179 | $backupConfigName = "$ScriptPath\config_backup.${HWNameShort}.${selectedComPort}.$([System.DateTime]::Now.ToString('yyyyMMddHHmmss')).yaml" 1180 | 1181 | # Start the loop for backup process 1182 | while ($true) { 1183 | try { 1184 | # Run the meshtastic command and redirect output to the backup config file 1185 | Write-Host "Running -m meshtastic meshtastic --port $selectedComPort --export-config > $backupConfigName" 1186 | # Start the Meshtastic process and redirect both stdout and stderr to the backup config file 1187 | $process = Start-Process -FilePath "$pythonCommand" -ArgumentList " -m meshtastic --port $selectedComPort --export-config" -PassThru -Wait -NoNewWindow -RedirectStandardOutput "$backupConfigName" -RedirectStandardError "$backupConfigName.error" 1188 | 1189 | # Check if the file has been created and output the file size 1190 | if (Test-Path "$backupConfigName") { 1191 | $fileSize = (Get-Item "$backupConfigName").Length 1192 | if ("$fileSize" -gt 0) { 1193 | if (-not (Test-Path "$backupConfigName.error") -or ((Get-Item "$backupConfigName.error").length -eq 0)) { 1194 | Write-Host "Backup configuration created: $backupConfigName. $fileSize bytes" 1195 | if (Test-Path "$backupConfigName.error") { 1196 | Remove-Item "$backupConfigName.error" -Force | out-null 1197 | } 1198 | break 1199 | } 1200 | else { 1201 | $content = Get-Content "$backupConfigName.error" -Raw 1202 | Write-Host "Error from meshtastic:" 1203 | Write-Host $content 1204 | } 1205 | } 1206 | } 1207 | Write-Host "Failed to create backup configuration." 1208 | $response = Read-Host "Press Enter to try again or type 'skip' to skip the creation" 1209 | if ($response -eq "skip") { 1210 | Write-Host "Skipping config backup." 1211 | break 1212 | } 1213 | Start-Sleep -Seconds 1 1214 | } catch { 1215 | # If there's an error, print the warning message 1216 | Write-Host "Error caught: $($_.Exception.Message)" 1217 | Write-Host "Warning: Timed out waiting for connection completion. Config backup not done." -ForegroundColor Red 1218 | 1219 | # Prompt the user for input to either try again or skip 1220 | $response = Read-Host "Press Enter to try again or type 'skip' to skip the creation" 1221 | 1222 | if ($response -eq "skip") { 1223 | Write-Host "Skipping config backup." 1224 | break 1225 | } 1226 | 1227 | # Wait for 1 second before retrying 1228 | Start-Sleep -Seconds 1 1229 | } 1230 | } 1231 | 1232 | } 1233 | 1234 | function GetFirmwareFiles($HWNameShort) { 1235 | $ChosenTagFile = $CHOSEN_TAG_FILE 1236 | $FirmwareRoot = $FIRMWARE_ROOT 1237 | $HWNameShortNorm = $HWNameShort | NormalizeString 1238 | 1239 | $chosenTag = (Get-Content -Path $CHOSEN_TAG_FILE -Raw).Trim() 1240 | $FolderPath = "$FirmwareRoot\$chosenTag" 1241 | 1242 | if (-not (Test-Path $FolderPath)) { 1243 | throw "Folder not found: $FolderPath" 1244 | } 1245 | 1246 | $matching = Get-ChildItem -Path $FolderPath -File -Recurse | Where-Object { 1247 | # must start with firmware-, then some name, then - (the version), and end in .bin/.uf2/.zip 1248 | $_.Name -match '^firmware-.+?.(?:bin|uf2|zip)$' -and $_.Name -notmatch '-ota\.' 1249 | } | 1250 | ForEach-Object { 1251 | # capture everything after firmware- up to the dash before the version 1252 | if ($_.Name -match "^firmware-(.+?)-$chosenTag.*") { 1253 | $match = $matches[1] 1254 | $normalized = $match | NormalizeString 1255 | if ($HWNameShortNorm -like "*$normalized*" -or $normalized -like "*$HWNameShortNorm*") { 1256 | [PSCustomObject]@{ 1257 | BaseName = $match 1258 | FullName = $_.FullName 1259 | NameLen = $_.Name.Length 1260 | } 1261 | } 1262 | } 1263 | } | Sort-Object NameLen 1264 | 1265 | $best = $matching | Select-Object -First 1 1266 | if ($best) { 1267 | $HWNameShortNorm = $best.BaseName | NormalizeString 1268 | $matchingDeep = Get-ChildItem -Path $FolderPath -File -Recurse | Where-Object { 1269 | # must start with firmware-, then some name, then - (the version), and end in .bin/.uf2/.zip 1270 | $_.Name -match '^firmware-.+?.(?:bin|uf2|zip)$' -and $_.Name -notmatch '-ota\.' 1271 | } | 1272 | ForEach-Object { 1273 | # capture everything after firmware- up to the dash before the version 1274 | if ($_.Name -match "^firmware-(.+?)-$chosenTag.*") { 1275 | $match = $matches[1] 1276 | $normalized = $match | NormalizeString 1277 | if ($HWNameShortNorm -like "*$normalized*" -or $normalized -like "*$HWNameShortNorm*") { 1278 | [PSCustomObject]@{ 1279 | BaseName = $match 1280 | FullName = $_.FullName 1281 | NameLen = $_.Name.Length 1282 | } 1283 | } 1284 | } 1285 | } | Sort-Object NameLen 1286 | } 1287 | 1288 | # write all the full paths to your output file 1289 | $matchingDeep | Select-Object -ExpandProperty FullName | Set-Content -Path $MATCHING_FILES_FILE 1290 | 1291 | $best = $matchingDeep | Sort-Object NameLen | Select-Object -First 1 1292 | if ($best) { 1293 | return $best.BaseName 1294 | } else { 1295 | if ($HWNameShortNorm -ne "timedout") { 1296 | Write-Warning "No matching firmware file found $HWNameShortNorm" 1297 | } 1298 | } 1299 | } 1300 | 1301 | function SelectMatchingFile { 1302 | param( 1303 | [string]$MatchingFilesFile = $MATCHING_FILES_FILE 1304 | ) 1305 | 1306 | # Read all candidate full paths 1307 | $fullPaths = Get-Content -Path $MatchingFilesFile | Where-Object { $_.Trim() -ne '' } 1308 | if ($fullPaths.Count -eq 0) { 1309 | Write-Host "No matching files found in '$MatchingFilesFile'." -ForegroundColor Red 1310 | return $null 1311 | } 1312 | 1313 | # If only one, auto-select it (display just the filename) 1314 | if ($fullPaths.Count -eq 1) { 1315 | $leaf = Split-Path -Path $fullPaths[0] -Leaf 1316 | Write-Host "Only one firmware file found: $fullPaths" -ForegroundColor Yellow 1317 | return $fullPaths 1318 | } 1319 | 1320 | $fullPaths = $fullPaths | Sort-Object 1321 | 1322 | # Otherwise, show menu of filenames 1323 | Write-Host "Select a firmware file to use:" -ForegroundColor Cyan 1324 | for ($i = 0; $i -lt $fullPaths.Count; $i++) { 1325 | $leaf = Split-Path -Path $fullPaths[$i] -Leaf 1326 | Write-Host ("{0,2}) {1}" -f ($i + 1), $leaf) 1327 | } 1328 | 1329 | # Prompt until valid selection 1330 | do { 1331 | $sel = Read-Host -Prompt ("Enter the number of your choice (1-{0})" -f $fullPaths.Count) 1332 | } until ( 1333 | ($sel -as [int]) -and 1334 | $sel -ge 1 -and 1335 | $sel -le $fullPaths.Count 1336 | ) 1337 | 1338 | # Return the full path corresponding to the chosen index 1339 | return $fullPaths[$sel - 1] 1340 | } 1341 | 1342 | function GetModelFromNode { 1343 | # Iterate all removable drives (DriveType=2) 1344 | foreach ($vol in Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=2") { 1345 | $drive = $vol.DeviceID # e.g. "E:" 1346 | $infoFile = Join-Path $drive 'INFO_UF2.TXT' 1347 | if (Test-Path $infoFile) { 1348 | # Read the file and look for a line starting with "Model:" 1349 | foreach ($line in Get-Content $infoFile -ErrorAction SilentlyContinue) { 1350 | if ($line -match '^\s*Model:\s*(.+)$') { 1351 | return ,$matches[1].Trim(), $drive 1352 | } 1353 | } 1354 | } 1355 | } 1356 | return ,$null, $null # no INFO_UF2.TXT found or no Model: line 1357 | } 1358 | 1359 | 1360 | 1361 | 1362 | function SelectHardware { 1363 | param( 1364 | [string] $HardwareListFile = $HARDWARE_LIST 1365 | ) 1366 | 1367 | if (-not (Test-Path $HardwareListFile)) { 1368 | Throw "Hardware list file not found: $HardwareListFile" 1369 | } 1370 | 1371 | # Load and parse the JSON 1372 | $hardware = Get-Content $HardwareListFile -Raw | ConvertFrom-Json 1373 | 1374 | # Sort by displayName 1375 | $sorted = $hardware | Sort-Object displayName 1376 | 1377 | # Show a numbered menu 1378 | for ($i = 0; $i -lt $sorted.Count; $i++) { 1379 | $num = $i + 1 1380 | Write-Host ("[{0}] {1}" -f $num, $sorted[$i].displayName) 1381 | } 1382 | 1383 | # Prompt until we get a valid selection 1384 | do { 1385 | $sel = Read-Host "Enter the number of your choice (1-$($sorted.Count))" 1386 | } until ($sel -as [int] -and $sel -ge 1 -and $sel -le $sorted.Count) 1387 | 1388 | $chosen = $sorted[$sel - 1] 1389 | 1390 | # Return the slug 1391 | return $chosen.hwModelSlug 1392 | } 1393 | 1394 | 1395 | function UpdateBleOta { 1396 | param( 1397 | [string] $selectedFile 1398 | ) 1399 | 1400 | $RepoApiUrl = $REPO_API_URL 1401 | $FirmwareRoot = $FIRMWARE_ROOT 1402 | $BleOtaFile = $BLEOTA_FILE 1403 | $CacheTimeoutSeconds = $CACHE_TIMEOUT_SECONDS 1404 | 1405 | 1406 | if (-not (CheckInternet)) { 1407 | Write-Host "Offline - using local BLE-OTA binaries if present." 1408 | return 1409 | } 1410 | 1411 | # Ensure firmware root exists 1412 | New-Item -Path $FirmwareRoot -ItemType Directory -Force | Out-Null 1413 | 1414 | $needUpdate = -not (Test-Path $BleOtaFile) -or 1415 | ((Get-Date) -gt ((Get-Item $BleOtaFile).LastWriteTime.AddSeconds($CacheTimeoutSeconds))) 1416 | 1417 | if ($needUpdate) { 1418 | Write-Host "Checking if Bluetooth-OTA bin files need updating…" 1419 | 1420 | $tmp = [IO.Path]::GetTempFileName() 1421 | try { 1422 | Invoke-RestMethod -Uri $RepoApiUrl -OutFile $tmp -ErrorAction Stop 1423 | } catch { 1424 | Write-Warning "Failed to download release data." 1425 | Remove-Item $tmp -ErrorAction SilentlyContinue 1426 | return 1427 | } 1428 | 1429 | # Validate JSON 1430 | try { 1431 | (Get-Content $tmp -Raw) | ConvertFrom-Json | Out-Null 1432 | } catch { 1433 | Write-Warning "Downloaded file is not valid JSON. Aborting." 1434 | Remove-Item $tmp -ErrorAction SilentlyContinue 1435 | return 1436 | } 1437 | 1438 | if (-not (Test-Path $BleOtaFile)) { 1439 | Move-Item $tmp $BleOtaFile 1440 | } else { 1441 | $oldHash = (Get-FileHash $BleOtaFile -Algorithm MD5).Hash 1442 | $newHash = (Get-FileHash $tmp -Algorithm MD5).Hash 1443 | if ($oldHash -ne $newHash) { 1444 | Write-Host "Release data changed. Updating cache." 1445 | Move-Item $tmp $BleOtaFile -Force 1446 | } else { 1447 | # just bump the timestamp 1448 | (Get-Item $BleOtaFile).LastWriteTime = Get-Date 1449 | Remove-Item $tmp -ErrorAction SilentlyContinue 1450 | } 1451 | } 1452 | } 1453 | 1454 | # Load the directory listing 1455 | $dirs = (Get-Content $BleOtaFile -Raw) | ConvertFrom-Json 1456 | 1457 | # find up to 3 firmware* dirs, newest first 1458 | $folders = $dirs | 1459 | Where-Object { $_.type -eq 'dir' -and $_.name.StartsWith('firmware') } | 1460 | Sort-Object name -Descending | 1461 | Select-Object -First 3 -ExpandProperty name 1462 | 1463 | $found = $null 1464 | $attempt = 0 1465 | foreach ($f in $folders) { 1466 | $attempt++ 1467 | $url = "$RepoApiUrl/$f" 1468 | Write-Host "Attempt $attempt - Checking folder '$f'…" 1469 | $contents = Invoke-RestMethod -Uri $url 1470 | $fileUrls = $contents | 1471 | Where-Object { $_.type -eq 'file' -and $_.name.StartsWith('bleota') } | 1472 | Select-Object -ExpandProperty download_url 1473 | if ($fileUrls) { 1474 | $found = $f 1475 | break 1476 | } 1477 | } 1478 | 1479 | if (-not $found) { 1480 | Throw "No 'bleota*' files found in the first 3 firmware folders." 1481 | } 1482 | 1483 | # Figure out where to put them (same folder as the selected firmware file) 1484 | $destFolder = Split-Path $selectedFile -Parent 1485 | 1486 | # Download any missing bleota files 1487 | $contents = Invoke-RestMethod -Uri "$RepoApiUrl/$found" 1488 | $fileUrls = $contents | 1489 | Where-Object { $_.type -eq 'file' -and $_.name.StartsWith('bleota') } | 1490 | Select-Object -ExpandProperty download_url 1491 | 1492 | foreach ($u in $fileUrls) { 1493 | $fn = Split-Path $u -Leaf 1494 | $dst = Join-Path $destFolder $fn 1495 | if (-not (Test-Path $dst)) { 1496 | Write-Host "Downloading $fn" 1497 | Invoke-RestMethod -Uri $u -OutFile $dst 1498 | } 1499 | } 1500 | 1501 | Write-Host "" 1502 | } 1503 | 1504 | 1505 | 1506 | 1507 | function ApplyPatch() { 1508 | param( 1509 | [string] $selectedFile 1510 | ) 1511 | 1512 | $destFolder = Split-Path $selectedFile -Parent 1513 | $patchPath = "$destFolder\fix.patch" 1514 | 1515 | @' 1516 | diff --git a/device-install.bat b/device-install.bat 1517 | index 3ffca0b..e80233a 100644 1518 | --- a/device-install.bat 1519 | +++ b/device-install.bat 1520 | @@ -170,12 +170,34 @@ IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected." 1521 | SET "BASENAME=!FILENAME:firmware-=!" 1522 | CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!" 1523 | 1524 | +REM Extract the folder containing that file 1525 | +for %%F in ("%FILENAME%") do set "FWFOLDER=%%~dpF" 1526 | + 1527 | +REM Trim off any trailing backslash 1528 | +if "!FWFOLDER:~-1!"=="\" set "FWFOLDER=!FWFOLDER:~0,-1!" 1529 | + 1530 | +REM Pull just the folder name (e.g. "esp32s3") 1531 | +for %%D in ("!FWFOLDER!") do set "PLATFORM=%%~nD" 1532 | +CALL :LOG_MESSAGE DEBUG "platform folder is !PLATFORM!" 1533 | + 1534 | +REM If PLATFORM ends in "s3", pick the s3 OTA image 1535 | +if /I "!PLATFORM:~-2!"=="s3" ( 1536 | + set "OTA_FILENAME=bleota-s3.bin" 1537 | + goto :OTA_DONE 1538 | +) 1539 | + 1540 | +REM If PLATFORM ends in "c3", pick the c3 OTA image 1541 | +if /I "!PLATFORM:~-2!"=="c3" ( 1542 | + set "OTA_FILENAME=bleota-c3.bin" 1543 | + goto :OTA_DONE 1544 | +) 1545 | + 1546 | @REM Account for S3 and C3 board's different OTA partition. 1547 | FOR %%a IN (%S3%) DO ( 1548 | IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( 1549 | @REM We are working with any of %S3%. 1550 | SET "OTA_FILENAME=bleota-s3.bin" 1551 | - GOTO :end_loop_s3 1552 | + GOTO :OTA_DONE 1553 | ) 1554 | ) 1555 | 1556 | @@ -183,14 +205,13 @@ FOR %%a IN (%C3%) DO ( 1557 | IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( 1558 | @REM We are working with any of %C3%. 1559 | SET "OTA_FILENAME=bleota-c3.bin" 1560 | - GOTO :end_loop_c3 1561 | + GOTO :OTA_DONE 1562 | ) 1563 | ) 1564 | 1565 | @REM Everything else 1566 | SET "OTA_FILENAME=bleota.bin" 1567 | -:end_loop_s3 1568 | -:end_loop_c3 1569 | +:OTA_DONE 1570 | CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" 1571 | 1572 | @REM Check if (--web) is enabled and prefix BASENAME with "littlefswebui-" else "littlefs-". 1573 | 1574 | '@ | Set-Content -Encoding utf8 -NoNewline $patchPath 1575 | 1576 | Push-Location $destFolder 1577 | run_cmd "python -m patch -p1 fix.patch" 1578 | Pop-Location 1579 | } 1580 | 1581 | 1582 | 1583 | function GetHW() { 1584 | # Find nodes 1585 | $result = GetModelFromNode 1586 | $DFU_node = $result[0] 1587 | $Drive = $result[1] 1588 | if ($DFU_node) { 1589 | $HWNameShort = $DFU_node 1590 | Write-Host "Found Device in DFU update state $HWNameShort" 1591 | $selectedComPort = "NA" 1592 | } 1593 | else { 1594 | # Get node info 1595 | UpdateHardwareList 1596 | $result = getUSBComPort 1597 | $selectedComPort = $result[0] 1598 | $HWNameShort = $result[1] 1599 | $OldVersion = $result[2] 1600 | $devicesBefore = $result[3] 1601 | } 1602 | 1603 | $HWNameFile = GetFirmwareFiles $HWNameShort 1604 | if (-not $HWNameFile -and $selectedComPort -eq "NA") { 1605 | # Get node info 1606 | UpdateHardwareList 1607 | $result = getUSBComPort 1608 | $selectedComPort = $result[0] 1609 | $HWNameShort = $result[1] 1610 | $OldVersion = $result[2] 1611 | $devicesBefore = $result[3] 1612 | $HWNameFile = GetFirmwareFiles $HWNameShort 1613 | } 1614 | if ($HWNameFile) { 1615 | $SelectedFirmwareFile = SelectMatchingFile 1616 | } 1617 | else { 1618 | $HWNameFile = SelectHardware 1619 | GetFirmwareFiles $HWNameFile 1620 | $SelectedFirmwareFile = SelectMatchingFile 1621 | } 1622 | 1623 | $hw = GetHardwareInfo -Slug $HWNameFile -ListPath $HARDWARE_LIST -SelectedFirmwareFile $SelectedFirmwareFile -selectedComPort $selectedComPort -HWNameFile $HWNameFile -Version $OldVersion 1624 | 1625 | Write-Progress -Activity " " -Status " " -Completed 1626 | return $hw 1627 | } 1628 | 1629 | function flashESP32() { 1630 | param( 1631 | [Parameter(Mandatory)][pscustomobject]$hw # must expose Architecture, SelectedFirmwareFile, selectedComPort/Drive 1632 | ) 1633 | 1634 | $fi = Get-Item $hw.FirmwareFile 1635 | $baseName = $fi.Name 1636 | if ($baseName -like '*-update*') { 1637 | updateFlashViaEspTool $hw 1638 | } else { 1639 | installFlashViaEspTool $hw 1640 | } 1641 | } 1642 | 1643 | function updateFlashViaEspTool { 1644 | param( 1645 | [Parameter(Mandatory)][pscustomobject]$hw 1646 | ) 1647 | 1648 | $SelectedFirmwareFile = $hw.FirmwareFile 1649 | $selectedComPort = $hw.ComPort 1650 | $fi = Get-Item $hw.FirmwareFile 1651 | $baseName = $fi.Name 1652 | $SelectedFirmwareBasename = $baseName -replace '^firmware-', '' 1653 | 1654 | 1655 | 1656 | $destFolder = Split-Path $SelectedFirmwareFile -Parent 1657 | Push-Location $destFolder 1658 | 1659 | 1660 | $ESPTOOL_CMD = get_esptool_cmd 1661 | 1662 | 1663 | Write-Host "" 1664 | Write-Host "" 1665 | Write-Host "" 1666 | 1667 | 1668 | $attempt = 0 # counter for Write-Progress 1669 | $delaySeconds = 3 # pause between retries 1670 | 1671 | # Wake up port 1672 | while ($true) { 1673 | $attempt++ 1674 | 1675 | if ($attempt -gt 5) { 1676 | Write-Progress -Status "Unplug and replug the device" -Activity "Waiting for $selectedComPort. Attempt: $attempt" 1677 | } 1678 | else { 1679 | Write-Progress -Status "Putting device into 1200 baud update mode" -Activity "Waiting for $selectedComPort. Attempt: $attempt" 1680 | } 1681 | 1682 | # run esptool and capture *all* output 1683 | $output = run_cmd "$ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1684 | 1685 | if ($output -match 'device attached to the system is not') { 1686 | if ($attempt -eq 5) { 1687 | Write-Host $output # echo the error so the user sees it 1688 | Write-Warning "Turn on the screen on the device" 1689 | Write-Warning "Unplug and repug the device" 1690 | 1691 | [console]::Beep() 1692 | Read-Host "Press enter to Continue" 1693 | 1694 | } 1695 | Start-Sleep -Seconds $delaySeconds 1696 | 1697 | $devicesAfter = getUSBComPort -SkipInfo 1698 | $selectedComPort = $devicesAfter[0] 1699 | 1700 | continue 1701 | } 1702 | 1703 | Write-Progress -Completed -Activity " " -Status "Port ready after $attempt attempt(s)" 1704 | #Write-Host $output 1705 | break 1706 | } 1707 | 1708 | 1709 | Start-Sleep -Seconds 12 1710 | $devicesAfter = getUSBComPort -SkipInfo 1711 | $selectedComPortPart2 = $devicesAfter[0] 1712 | Write-Host "Flashing $SelectedFirmwareFile at 0x10000. Write Meshtastic Firmware." 1713 | Write-Host "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash 0x10000 $SelectedFirmwareFile" 1714 | Write-Host "" 1715 | run_cmd "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash 0x10000 $SelectedFirmwareFile" -Stream 1716 | 1717 | 1718 | Write-Host "" 1719 | Pop-Location 1720 | } 1721 | 1722 | function installFlashViaEspTool { 1723 | param( 1724 | [Parameter(Mandatory)][pscustomobject]$hw 1725 | ) 1726 | 1727 | $SelectedFirmwareFile = $hw.FirmwareFile 1728 | $selectedComPort = $hw.ComPort 1729 | $fi = Get-Item $hw.FirmwareFile 1730 | $baseName = $fi.Name 1731 | $SelectedFirmwareBasename = $baseName -replace '^firmware-', '' 1732 | 1733 | $OTA_OFFSET = '0x260000' 1734 | $SPIFFS_OFFSET = '0x300000' 1735 | if ($hw.FlashSize -eq '8MB') { 1736 | $OTA_OFFSET = '0x340000' 1737 | $SPIFFS_OFFSET = '0x670000' 1738 | } 1739 | elseif ($hw.FlashSize -eq '16MB') { 1740 | $OTA_OFFSET = '0x650000' 1741 | $SPIFFS_OFFSET = '0xc90000' 1742 | } 1743 | 1744 | $OTA_FILENAME = "bleota.bin" 1745 | if ($hw.Architecture -like '*-s3') { 1746 | $OTA_FILENAME = "bleota-s3.bin" 1747 | } 1748 | if ($hw.Architecture -like '*-c3') { 1749 | $OTA_FILENAME = "bleota-c3.bin" 1750 | } 1751 | 1752 | $SPIFFS_FILENAME = "littlefs-$SelectedFirmwareBasename" 1753 | if ($baseName -notlike '*-update*' -and $SelectedFirmwareFile -notlike '*-tft-*') { 1754 | $choice = Read-Host "`nFlash the Web UI as well? [Y]es / [N]o (default N)" 1755 | 1756 | if ($choice -match '^[Yy]') { 1757 | $SPIFFS_FILENAME = "littlefswebui-$SelectedFirmwareBasename" 1758 | } 1759 | } 1760 | #Write-Host "OTA_OFFSET set to: $OTA_OFFSET" 1761 | #Write-Host "OTA_FILENAME set to: $OTA_FILENAME" 1762 | #Write-Host "SPIFFS_OFFSET set to: $SPIFFS_OFFSET" 1763 | #Write-Host "SPIFFS_FILENAME set to: $SPIFFS_FILENAME" 1764 | 1765 | $destFolder = Split-Path $SelectedFirmwareFile -Parent 1766 | Push-Location $destFolder 1767 | 1768 | foreach ($file in @($SelectedFirmwareFile, $OTA_FILENAME, $SPIFFS_FILENAME)) { 1769 | if (-not (Test-Path $file)) { 1770 | Write-Warning "File does not exist: $file" 1771 | Write-Warning "Terminating." 1772 | Return $false 1773 | } 1774 | } 1775 | 1776 | 1777 | $ESPTOOL_CMD = get_esptool_cmd 1778 | 1779 | 1780 | Write-Host "" 1781 | Write-Host "" 1782 | Write-Host "" 1783 | Write-Host "Setting baud to 1200 for firmware update mode. $ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1784 | $a = run_cmd "$ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1785 | Start-Sleep -Seconds 1 1786 | $devicesAfter = getUSBComPort -SkipInfo 1787 | $selectedComPortPart2 = $devicesAfter[0] 1788 | Write-Host "Erasing the flash." 1789 | Write-Host "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 erase_flash" 1790 | run_cmd "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 erase_flash" -Stream 1791 | Write-Host "" 1792 | Write-Host "Flashing $SelectedFirmwareFile at 0x00. Write Meshtastic Firmware." 1793 | Write-Host "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash 0x00 $SelectedFirmwareFile" 1794 | Write-Host "" 1795 | run_cmd "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash 0x00 $SelectedFirmwareFile" -Stream 1796 | 1797 | 1798 | Write-Host "" 1799 | Write-Host "" 1800 | Write-Host "" 1801 | Write-Host "Waiting 12 seconds" 1802 | Start-Sleep -Seconds 12 1803 | $devicesAfter = getUSBComPort -SkipInfo 1804 | $selectedComPort = $devicesAfter[0] 1805 | Write-Host "Setting baud to 1200 for firmware update mode. $ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1806 | $b = run_cmd "$ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1807 | Start-Sleep -Seconds 1 1808 | Write-Host "Flashing $OTA_FILENAME at $OTA_OFFSET. Write Bluetooth Over The Air Update firmware." 1809 | Write-Host "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash $OTA_OFFSET $OTA_FILENAME" 1810 | Write-Host "" 1811 | run_cmd "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash $OTA_OFFSET $OTA_FILENAME" -Stream 1812 | 1813 | 1814 | Write-Host "" 1815 | Write-Host "" 1816 | Write-Host "" 1817 | Write-Host "Waiting 12 seconds" 1818 | Start-Sleep -Seconds 12 1819 | Write-Host "Setting baud to 1200 for firmware update mode. $ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1820 | $c = run_cmd "$ESPTOOL_CMD --baud 1200 --port $selectedComPort chip_id" 1821 | Start-Sleep -Seconds 1 1822 | Write-Host "Flashing $SPIFFS_FILENAME at $SPIFFS_OFFSET. Write Filesystem firmware." 1823 | Write-Host "$ESPTOOL_CMD" "--baud" "115200" "--port" "$selectedComPortPart2" "write_flash" "$SPIFFS_OFFSET" "$SPIFFS_FILENAME" 1824 | Write-Host "" 1825 | run_cmd "$ESPTOOL_CMD --baud 115200 --port $selectedComPortPart2 write_flash $SPIFFS_OFFSET $SPIFFS_FILENAME" -Stream 1826 | 1827 | 1828 | Write-Host "" 1829 | Pop-Location 1830 | } 1831 | 1832 | 1833 | function flashNotESP32 { 1834 | param( 1835 | [string] $SelectedFirmwareFile, 1836 | [string] $selectedComPort 1837 | ) 1838 | 1839 | if ($selectedComPort -ne "NA") { 1840 | Read-Host "Press Enter to put node into Device Firmware Update (DFU) mode via $pythonCommand -m meshtastic --port $selectedComPort --enter-dfu" 1841 | 1842 | $before = Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Name 1843 | 1844 | $result = runMeshtasticCommand $selectedComPort "--enter-dfu" 1845 | Write-Progress -Activity " " -Status " " -Completed 1846 | $meshtasticOutput = $result[0] 1847 | $meshtasticError = $result[1] 1848 | Write-Host $meshtasticOutput 1849 | Write-Host $meshtasticError 1850 | 1851 | $endTime = (Get-Date).AddSeconds(15) 1852 | while ((Get-Date) -lt $endTime -and -not $newDrive) { 1853 | Start-Sleep -Seconds 1 1854 | $after = Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Name 1855 | # any name in $after that wasn't in $before? 1856 | $newDrive = $after | Where-Object { $_ -notin $before } 1857 | } 1858 | } 1859 | else { 1860 | $newDrive = $Drive 1861 | } 1862 | 1863 | if ($newDrive) { 1864 | if (-not $newDrive.ToString().EndsWith(':')) { 1865 | $newDrive += ':' 1866 | } 1867 | Write-Host "DFU mount is drive `"$newDrive`"" 1868 | $dest = Join-Path -Path $newDrive -ChildPath (Split-Path $SelectedFirmwareFile -Leaf) 1869 | # Read-Host "Press Enter Copy the Firmware to $dest" 1870 | Copy-Item -Path $SelectedFirmwareFile -Destination $dest -Force -ErrorAction Stop 1871 | 1872 | Write-Host "Done." -ForegroundColor Green 1873 | } else { 1874 | Write-Warning "Timed out waiting for DFU drive (no new PSDrive after $timeout seconds)" 1875 | } 1876 | } 1877 | 1878 | 1879 | function InvokeFlash { 1880 | param( 1881 | [Parameter(Mandatory)][pscustomobject]$hw # must expose Architecture, SelectedFirmwareFile, selectedComPort/Drive 1882 | ) 1883 | Write-Progress -Activity " " -Status " " -Completed 1884 | 1885 | try { 1886 | if ($hw.Architecture -like '*esp32*') { 1887 | flashESP32 -hw $hw 1888 | } 1889 | else { 1890 | flashNotESP32 -SelectedFirmwareFile $hw.FirmwareFile -selectedComPort $hw.ComPort 1891 | } 1892 | 1893 | Write-Host "Flash completed." 1894 | } 1895 | catch { 1896 | Write-Warning "Flash failed: $_" 1897 | } 1898 | Write-Host "" 1899 | return "" 1900 | } 1901 | 1902 | 1903 | # Get release info 1904 | check_requirements 1905 | UpdateReleases 1906 | BuildReleaseMenuData 1907 | $tag = SelectRelease 1908 | DownloadAssets 1909 | UnzipAssets 1910 | 1911 | $hw = GetHW 1912 | 1913 | Write-Host "Selected hardware: $($hw.DisplayName)" 1914 | Write-Host " Architecture: $($hw.Architecture)" 1915 | Write-Host " Requires DFU: $($hw.RequiresDfu)" 1916 | Write-Host " Has Ink HUD: $($hw.HasInkHud)" 1917 | Write-Host " Has Meshtastic UI: $($hw.HasMui)" 1918 | Write-Host " New Firmware: $($hw.FirmwareFile)" 1919 | Write-Host " COM Port: $($hw.ComPort)" 1920 | 1921 | if ($hw.ComPort -ne "NA" -and $hw.Version -ne "--") { 1922 | MakeConfigBackup $hw.HWNameFile $hw.ComPort 1923 | } 1924 | $again = $true 1925 | while ($again) { 1926 | $x = InvokeFlash $hw 1927 | $x 1928 | 1929 | if ($hw.Architecture -like 'esp32*') { 1930 | $choice = Read-Host "`nEverything OK? [Y]es / [R]etry / change [C]OM port / [E]xit" 1931 | switch ($choice.ToUpper()) { 1932 | 'Y' { $again = $false } 1933 | 'R' { } # loop again with same port 1934 | 'C' { 1935 | getallUSBCom | Write-Host 1936 | 1937 | $hw.ComPort = Read-Host 'Enter new COM port (e.g. COM7)'; 1938 | } 1939 | default { $again = $false } 1940 | } 1941 | } 1942 | else { 1943 | $choice = Read-Host "`nEverything OK? [Y]es / [R]etry / change drive [D]letter / [E]xit" 1944 | switch ($choice.ToUpper()) { 1945 | 'Y' { $again = $false } 1946 | 'R' { } # loop again with same drive 1947 | 'D' { 1948 | GetModelFromNode | Write-Host 1949 | 1950 | $hw.ComPort = Read-Host 'Enter new drive letter (e.g. E:)\'; 1951 | } 1952 | default { $again = $false } 1953 | } 1954 | } 1955 | } 1956 | 1957 | 1958 | # When the user finally hits Enter, the script will exit naturally. 1959 | $scriptOver = $true 1960 | Read-Host 'Press Enter to exit (via end of script)' 1961 | -------------------------------------------------------------------------------- /firmware.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | : <<'EOF' 4 | 5 | # To run this file, copy this line below and run it. 6 | cd ~ && wget -qO - https://raw.githubusercontent.com/mikecarper/meshfirmware/refs/heads/main/firmware.sh | bash 7 | 8 | # 9 | EOF 10 | 11 | # Strict errors. 12 | # Trap errors and output file and line number. 13 | set -euo pipefail 14 | 15 | # Ensure we always restore on exit 16 | cleanup() { 17 | USB_AUTOSUSPEND_END=$(cat /sys/module/usbcore/parameters/autosuspend) 18 | if [[ "$USB_AUTOSUSPEND_END" != "$USB_AUTOSUSPEND" ]]; then 19 | echo "$USB_AUTOSUSPEND" | sudo tee /sys/module/usbcore/parameters/autosuspend >/dev/null 20 | fi 21 | } 22 | error_handler() { 23 | local lineno=$1 24 | echo "FAILED at ${BASH_SOURCE[0]}:${lineno}" >&2 25 | cleanup 26 | exit 1 27 | } 28 | 29 | trap 'error_handler $LINENO' ERR # on any error 30 | trap cleanup EXIT # on any exit (error or normal) 31 | 32 | 33 | 34 | # If BASH_SOURCE[0] is not set, fall back to the current working directory. 35 | if [ -z "${BASH_SOURCE+x}" ] || [ -z "${BASH_SOURCE[0]+x}" ]; then 36 | # The script is likely being run via a pipe, so there's no script file path 37 | PWD_SCRIPT="$(pwd)" 38 | else 39 | PWD_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 40 | fi 41 | 42 | # Global argument variables. 43 | VERSION_ARG="" 44 | OPERATION_ARG="" 45 | RUN_UPDATE=false 46 | 47 | # Global variable to track the spinner index. 48 | spinner_index=0 49 | # Array holding the spinner characters. 50 | spinner_chars=("-" "\\" "|" "/") 51 | 52 | ######################### 53 | # Configuration Variables 54 | ######################### 55 | # Define the repo 56 | REPO_OWNER="meshtastic" 57 | REPO_NAME="firmware" 58 | REPO_NAME_ALT="meshtastic.github.io" 59 | CACHE_TIMEOUT_SECONDS=$((6 * 3600)) # 6 hours 60 | MOUNT_FOLDER="/mnt/meshDeviceSD" 61 | USB_AUTOSUSPEND=$(cat /sys/module/usbcore/parameters/autosuspend) 62 | if [[ "$USB_AUTOSUSPEND" -ne -1 ]]; then 63 | # Only disable (-1) if it isn’t already 64 | echo "sudo needed to disable USB autosuspend and keep all USB ports active." 65 | echo -1 | sudo tee /sys/module/usbcore/parameters/autosuspend >/dev/null 66 | fi 67 | 68 | # Settings for the repo 69 | GITHUB_API_URL="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases" 70 | REPO_API_URL="https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME_ALT}/contents" 71 | WEB_HARDWARE_LIST_URL="https://raw.githubusercontent.com/${REPO_OWNER}/web-flasher/refs/heads/main/public/data/hardware-list.json" 72 | # Set Folders 73 | FIRMWARE_ROOT="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}" 74 | DOWNLOAD_DIR="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/downloads" 75 | # Vars to get passed around and cached as files. 76 | RELEASES_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/releases.json" 77 | RESOURCES_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/hardware-list.json" 78 | BLEOTA_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/bleota.json" 79 | VERSIONS_TAGS_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/01versions_tags.txt" 80 | VERSIONS_LABELS_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/02versions_labels.txt" 81 | CHOSEN_TAG_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/03chosen_tag.txt" 82 | DOWNLOAD_PATTERN_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/04download_pattern.txt" 83 | DEVICE_INFO_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/05device_info.txt" 84 | DETECTED_PRODUCT_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/06detected_product.txt" 85 | MATCHING_FILES_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/07matching_files.txt" 86 | CMD_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/08cmd.txt" 87 | SELECTED_FILE_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/09selected_file.txt" 88 | OPERATION_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/10operation.txt" 89 | ARCHITECTURE_FILE="${PWD_SCRIPT}/${REPO_OWNER}_${REPO_NAME}/11architecture.txt" 90 | 91 | 92 | ######################### 93 | # Function Definitions 94 | ######################### 95 | 96 | spinner() { 97 | # Print the spinner character (using \r to overwrite the same line) 98 | printf "\r%s" "${spinner_chars[spinner_index]}" >/dev/tty 99 | # Update the index, wrapping around to 0 when reaching the end of the array. 100 | spinner_index=$(((spinner_index + 1) % ${#spinner_chars[@]})) 101 | } 102 | 103 | # Display help and usage. 104 | show_help() { 105 | echo "Usage: $(basename "$0") [OPTIONS]" 106 | echo "" 107 | echo "Options:" 108 | echo " --version VERSION Specify the version to use." 109 | echo " --install Set the operation to 'install'." 110 | echo " --update Set the operation to 'update'." 111 | echo " --run Automatically run the update script without prompting." 112 | echo " -h, --help Display this help message and exit." 113 | exit 0 114 | } 115 | 116 | # Parse command-line arguments. 117 | parse_args() { 118 | while [[ $# -gt 0 ]]; do 119 | case "$1" in 120 | --version) 121 | shift 122 | VERSION_ARG="$1" 123 | ;; 124 | --install) 125 | if [ -n "$OPERATION_ARG" ] && [ "$OPERATION_ARG" != "install" ]; then 126 | echo "Error: Conflicting options specified." 127 | exit 1 128 | fi 129 | OPERATION_ARG="install" 130 | ;; 131 | --update) 132 | if [ -n "$OPERATION_ARG" ] && [ "$OPERATION_ARG" != "update" ]; then 133 | echo "Error: Conflicting options specified." 134 | exit 1 135 | fi 136 | OPERATION_ARG="update" 137 | ;; 138 | --run) 139 | RUN_UPDATE=true 140 | ;; 141 | -h | --help) 142 | show_help 143 | ;; 144 | *) 145 | echo "Unknown option: $1" 146 | show_help 147 | ;; 148 | esac 149 | shift 150 | done 151 | } 152 | 153 | # Check for an active internet connection. 154 | check_internet() { 155 | local domain 156 | domain=$(echo "$GITHUB_API_URL" | sed -E 's|https?://([^/]+)/.*|\1|') 157 | if ping -c1 -W2 "$domain" >/dev/null 2>&1; then 158 | return 0 159 | else 160 | return 1 161 | fi 162 | } 163 | 164 | # Update the GitHub release cache if needed. 165 | update_releases() { 166 | if check_internet; then 167 | # Ensure jq is present 168 | if ! command -v jq >/dev/null 2>&1; then 169 | echo "jq not found – installing…" 170 | if ! sudo apt-get -y install jq; then # first try: install directly 171 | echo "Package lists may be stale; updating and retrying…" 172 | sudo apt-get update 173 | sudo apt-get -y install jq 174 | fi 175 | fi 176 | 177 | # If we don't have a cache file or it's older than our timeout, attempt an update. 178 | if [ ! -f "$RELEASES_FILE" ] || [ "$(date +%s)" -ge "$(($(stat -c %Y "$RELEASES_FILE") + CACHE_TIMEOUT_SECONDS))" ]; then 179 | mkdir -p "$FIRMWARE_ROOT" 180 | echo "Updating release cache from GitHub. $RELEASES_FILE $GITHUB_API_URL" 181 | 182 | # Download into a temp file first 183 | tmpfile=$(mktemp) 184 | curl -s "$GITHUB_API_URL" -o "$tmpfile" || { 185 | echo "Failed to download release data." 186 | rm -f "$tmpfile" 187 | return 188 | } 189 | 190 | # Check if the newly downloaded file is valid JSON 191 | if ! errmsg=$(jq -e . "$tmpfile" 2>&1 >/dev/null); then 192 | echo "Downloaded file is not valid JSON:" 193 | echo "$errmsg" 194 | rm -f "$tmpfile" 195 | return 1 196 | fi 197 | 198 | # Filter out "download_count" keys from the JSON. 199 | # This jq filter defines a recursive walk function. 200 | filtered_tmp=$(mktemp) 201 | jq 'def walk(f): 202 | . as $in 203 | | if type=="object" then 204 | reduce keys[] as $key ({}; . + { ($key): ($in[$key] | walk(f)) }) 205 | elif type=="array" then map(walk(f)) 206 | else . end; 207 | walk(if type=="object" then del(.download_count) else . end)' "$tmpfile" >"$filtered_tmp" || { 208 | echo "Failed to filter JSON." 209 | rm -f "$tmpfile" "$filtered_tmp" 210 | return 211 | } 212 | 213 | # Use the filtered JSON for further processing. 214 | if [ ! -f "$RELEASES_FILE" ]; then 215 | mv "$filtered_tmp" "$RELEASES_FILE" 216 | rm -f "$tmpfile" 217 | else 218 | # Compare the MD5 sums of the cached file and the newly filtered file. 219 | old_md5=$(md5sum "$RELEASES_FILE" | awk '{print $1}') 220 | new_md5=$(md5sum "$filtered_tmp" | awk '{print $1}') 221 | if [ "$old_md5" != "$new_md5" ]; then 222 | echo "Release data changed. Updating cache and removing cached version lists. $old_md5 $new_md5" 223 | mv "$filtered_tmp" "$RELEASES_FILE" 224 | rm -f "${VERSIONS_TAGS_FILE}" "${VERSIONS_LABELS_FILE}" 225 | else 226 | echo "Release data is unchanged. $old_md5 $new_md5" 227 | rm -f "$filtered_tmp" 228 | fi 229 | rm -f "$tmpfile" 230 | fi 231 | else 232 | echo "Using cached release data (updated within the last 6 hours)." 233 | fi 234 | else 235 | echo "No internet connection; using cached release data if available." 236 | fi 237 | } 238 | 239 | update_bleota() { 240 | if check_internet; then 241 | # If we don't have a cache file or it's older than our timeout, attempt an update. 242 | if [ ! -f "$BLEOTA_FILE" ] || [ "$(date +%s)" -ge "$(($(stat -c %Y "$BLEOTA_FILE") + CACHE_TIMEOUT_SECONDS))" ]; then 243 | mkdir -p "$FIRMWARE_ROOT" 244 | echo "Checking if bluetooth over the air bin files from GitHub needs to be updated. $BLEOTA_FILE $REPO_API_URL" 245 | 246 | # Download into a temp file first 247 | tmpfile=$(mktemp) 248 | curl -s "$REPO_API_URL" -o "$tmpfile" || { 249 | echo "Failed to download release data." 250 | rm -f "$tmpfile" 251 | return 252 | } 253 | 254 | # Check if the newly downloaded file is valid JSON 255 | if ! errmsg=$(jq -e . "$tmpfile" 2>&1 >/dev/null); then 256 | echo "Downloaded file is not valid JSON:" 257 | echo "$errmsg" 258 | rm -f "$tmpfile" 259 | return 1 260 | fi 261 | 262 | # Use the filtered JSON for further processing. 263 | if [ ! -f "$BLEOTA_FILE" ]; then 264 | mv "$tmpfile" "$BLEOTA_FILE" 265 | else 266 | # Compare the MD5 sums of the cached file and the newly filtered file. 267 | old_md5=$(md5sum "$BLEOTA_FILE" | awk '{print $1}') 268 | new_md5=$(md5sum "$tmpfile" | awk '{print $1}') 269 | if [ "$old_md5" != "$new_md5" ]; then 270 | echo "Release data changed. Updating cache and removing cached version lists. $old_md5 $new_md5" 271 | mv "$tmpfile" "$BLEOTA_FILE" 272 | else 273 | touch "$BLEOTA_FILE" 274 | fi 275 | fi 276 | fi 277 | firmware_dir_list=$(cat "${BLEOTA_FILE}") 278 | 279 | # Get a list of firmware directories sorted in reverse order (latest first). 280 | firmware_folders=$(echo "$firmware_dir_list" \ 281 | | jq -r '.[] | select(.type=="dir") | select(.name | startswith("firmware")) | .name' \ 282 | | sort -r) 283 | 284 | attempt=1 285 | found_folder="" 286 | 287 | # Loop over each folder in firmware_folders, but only try up to 3. 288 | for folder in $firmware_folders; do 289 | 290 | folder_url="${REPO_API_URL}/${folder}" 291 | folder_contents=$(curl -s "$folder_url") 292 | 293 | # Filter for files that start with "bleota" 294 | file_urls=$(echo "$folder_contents" \ 295 | | jq -r '.[] | select(.type=="file") | select(.name | startswith("bleota")) | .download_url') 296 | 297 | if [ -n "$file_urls" ]; then 298 | found_folder="$folder" 299 | break 300 | fi 301 | 302 | attempt=$((attempt+1)) 303 | if [ $attempt -gt 3 ]; then 304 | break 305 | fi 306 | echo "Attempt $attempt: Checking folder: $folder" 307 | done 308 | 309 | if [ -z "$found_folder" ]; then 310 | echo "No files starting with 'bleota' found in up to 3 firmware folders." 311 | exit 1 312 | fi 313 | 314 | # Proceed with processing of $found_folder: 315 | selected_file=$(cat "${SELECTED_FILE_FILE}") 316 | folder=$(dirname "$selected_file") 317 | folder_url="${REPO_API_URL}/${found_folder}" 318 | folder_contents=$(curl -s "$folder_url") 319 | file_urls=$(echo "$folder_contents" \ 320 | | jq -r '.[] | select(.type=="file") | select(.name | startswith("bleota")) | .download_url') 321 | 322 | # Download each matching file, but only if it doesn't exist already. 323 | for url in $file_urls; do 324 | filename=$(basename "$url") 325 | destination="$folder/$filename" 326 | if [ ! -f "$destination" ]; then 327 | echo "Downloading $filename from $url..." 328 | curl -s -L -o "$destination" "$url" 329 | fi 330 | done 331 | else 332 | echo "Use local versions" 333 | #bleota.bin 334 | #bleota-s3.bin 335 | #bleota-c3.bin 336 | fi 337 | echo "" 338 | } 339 | 340 | update_hardware_list() { 341 | # Check if RESOURCES_FILE exists and is newer than 6 hours; if not, download it. 342 | if [ ! -f "$RESOURCES_FILE" ] || [ "$(find "$RESOURCES_FILE" -mmin +360)" ]; then 343 | echo "Downloading resources.ts from GitHub. $RESOURCES_FILE $WEB_HARDWARE_LIST_URL" 344 | mkdir -p "$(dirname "$RESOURCES_FILE")" 345 | curl -s -L "$WEB_HARDWARE_LIST_URL" -o "$RESOURCES_FILE" 346 | fi 347 | } 348 | 349 | # Retrieve release JSON data from the cache. 350 | get_release_data() { 351 | if [ ! -f "$RELEASES_FILE" ]; then 352 | echo "No cached release data available. Exiting." 353 | exit 1 354 | fi 355 | cat "$RELEASES_FILE" 356 | } 357 | 358 | # Normalize strings (remove dashes, underscores, spaces, and convert to lowercase). 359 | normalize() { 360 | echo "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:blank:]' | tr -d '-' | tr -d '_' 361 | } 362 | 363 | # Build the release menu and save version tags and labels. 364 | build_release_menu() { 365 | local releases_json="$1" 366 | # We'll build a temporary list of entries in the format: datetaglabel 367 | local tmpfile 368 | tmpfile=$(mktemp) 369 | 370 | echo "Parsing JSON and adding built firmware entry if available." 371 | 372 | # Process JSON releases 373 | while IFS=$'\t' read -r tag prerelease draft body created_at; do 374 | spinner 375 | # Determine suffix based on the tag. 376 | suffix="" 377 | # Strip time from created_at date 378 | date="${created_at}" 379 | suffix="$date" 380 | 381 | if [[ "$tag" =~ [Aa]lpha ]]; then 382 | suffix="$suffix (alpha)" 383 | elif [[ "$tag" =~ [Bb]eta ]]; then 384 | suffix="$suffix (beta)" 385 | elif [[ "$tag" =~ [Rr][Cc] ]]; then 386 | suffix="$suffix (rc)" 387 | fi 388 | 389 | # Override suffix based on draft or prerelease flags. 390 | if [ "$draft" = "true" ]; then 391 | suffix="$suffix (draft)" 392 | elif [ "$prerelease" = "true" ]; then 393 | suffix="$suffix (pre-release)" 394 | fi 395 | 396 | tag="${tag#v}" 397 | 398 | label=$(printf "%-14s" "$tag") 399 | label="$label $suffix" 400 | 401 | # Check for the warning emoji in body. 402 | if echo "$body" | grep -q -- '⚠️'; then 403 | label="! $label" 404 | else 405 | label=" $label" 406 | fi 407 | 408 | # Write the entry to the temporary file. 409 | echo -e "${date}\t${tag}\t${label}" >> "$tmpfile" 410 | spinner 411 | done < <(echo "$releases_json" | jq -r '.[] | [.tag_name, .prerelease, .draft, .body, .created_at] | @tsv') 412 | 413 | # Check if any subdirectory name in FIRMWARE_ROOT (skip "downloads") is not in the tag_names from above. 414 | for folder in "$FIRMWARE_ROOT"/*; do 415 | # Skip if not a directory. 416 | [ -d "$folder" ] || continue 417 | folder_name=$(basename "$folder") 418 | 419 | # Skip the downloads folder. 420 | if [ "$folder_name" = "downloads" ]; then 421 | continue 422 | fi 423 | 424 | # Convert folder name to lowercase for matching. 425 | folder_lower=$(echo "$folder_name" | tr '[:upper:]' '[:lower:]') 426 | if [[ "$folder_lower" == v* ]]; then 427 | folder_lower="${folder_lower:1}" 428 | fi 429 | 430 | # Check if this folder name is present (case-insensitive) anywhere in $tmpfile. 431 | if ! grep -qi "$folder_lower" "$tmpfile"; then 432 | # Find the first firmware-* file in the folder. 433 | first_file=$(find "$folder" -maxdepth 1 -type f -iname "firmware-*" | head -n 1) 434 | if [ -n "$first_file" ]; then 435 | mtime=$(date -u -d "$(stat -c %y "$first_file")" +"%Y-%m-%dT%H:%M:%SZ") 436 | else 437 | # Fallback: if no firmware-* file is found, use the folder's modification time. 438 | mtime=$(date -u -d "$(stat -c %y "$folder")" +"%Y-%m-%dT%H:%M:%SZ") 439 | fi 440 | 441 | # Build the label: version tag, then date, then "(nightly)" 442 | label="! ${folder_name} ${mtime} (nightly)" 443 | 444 | # Write the entry to the temporary file. 445 | # Format: datetaglabel 446 | echo -e "${mtime}\t${folder_name}\t${label}" >> "$tmpfile" 447 | fi 448 | done 449 | 450 | # Sort all entries by date in descending order (newest first) 451 | local sorted_entries 452 | sorted_entries=$(sort -r "$tmpfile") 453 | rm "$tmpfile" 454 | 455 | # Build arrays from the sorted entries. 456 | declare -a versions_tags=() 457 | declare -a versions_labels=() 458 | 459 | while IFS=$'\t' read -r date tag label; do 460 | versions_tags+=("$tag") 461 | versions_labels+=("$label") 462 | done <<< "$sorted_entries" 463 | 464 | # Save the arrays for later use. 465 | printf "%s\n" "${versions_tags[@]}" >"${VERSIONS_TAGS_FILE}" 466 | printf "%s\n" "${versions_labels[@]}" >"${VERSIONS_LABELS_FILE}" 467 | printf "\r" 468 | } 469 | 470 | # Allow the user to select a firmware release version. 471 | select_release() { 472 | local versions_tags versions_labels chosen_index auto_selected i selection 473 | local term_width max_len col_label_width col_width num_per_row num_entries index_width 474 | local label formatted pre_colored stable_colored 475 | local yellow green cyan reset 476 | 477 | # Use tput to set color codes. 478 | red=$(tput setaf 1) # Red for unreleased versions. 479 | yellow=$(tput setaf 3) # Yellow for pre-releases. 480 | green=$(tput setaf 2) # Green for the first stable entry. 481 | cyan=$(tput setaf 6) # Cyan for the latest stable (without "!" or pre-release). 482 | reset=$(tput sgr0) # Reset. 483 | 484 | # Load cached arrays from file. 485 | readarray -t versions_tags <"$VERSIONS_TAGS_FILE" 486 | readarray -t versions_labels <"$VERSIONS_LABELS_FILE" 487 | 488 | # Determine the latest stable candidate: the first entry that does NOT start with "!" and does NOT contain "(pre-release)". 489 | local latest_stable_index=-1 490 | for i in "${!versions_labels[@]}"; do 491 | label="${versions_labels[$i]}" 492 | if [[ "$label" != "!"* ]] && [[ "$label" != *"(pre-release)"* ]]; then 493 | latest_stable_index=$i 494 | break 495 | fi 496 | done 497 | 498 | if [ -n "$VERSION_ARG" ]; then 499 | for i in "${!versions_tags[@]}"; do 500 | if [[ "${versions_tags[$i]}" == *${VERSION_ARG}* ]]; then 501 | auto_selected="${versions_labels[$i]}" 502 | chosen_index=$i 503 | break 504 | fi 505 | done 506 | if [ -z "$auto_selected" ]; then 507 | echo "No release version found matching --version $VERSION_ARG" 508 | exit 1 509 | fi 510 | else 511 | echo "Available firmware release versions:" 512 | 513 | # Determine the current terminal width. 514 | term_width=$(tput cols) 515 | 516 | # Find the maximum label length so we know how wide to make each label field. 517 | max_len=0 518 | for label in "${versions_labels[@]}"; do 519 | if ((${#label} > max_len)); then 520 | max_len=${#label} 521 | fi 522 | done 523 | 524 | # Figure out how many digits we need for the highest index (the total count). 525 | num_entries=${#versions_labels[@]} 526 | index_width=${#num_entries} # Number of digits in the total count. 527 | 528 | # Decide how wide we want the label portion itself (allow a little extra padding). 529 | col_label_width=$((max_len + 2)) 530 | 531 | # The total column width = index portion + ") " + label portion + space. 532 | col_width=$((index_width + 2 + col_label_width + 1)) 533 | 534 | # How many columns fit in our adjusted terminal width? 535 | num_per_row=$((term_width / col_width)) 536 | if [ $num_per_row -lt 1 ]; then 537 | num_per_row=1 538 | fi 539 | 540 | # Flags to track whether we've already colored a pre-release or a stable entry. 541 | pre_colored=0 542 | stable_colored=0 543 | 544 | # Print the list in dynamically determined columns. 545 | # --- 1) Collect all formatted entries into an array --- 546 | declare -a formatted_entries=() 547 | 548 | for i in "${!versions_labels[@]}"; do 549 | label="${versions_labels[$i]}" 550 | formatted=$(printf "%*d) %-*s " "$index_width" $((i + 1)) "$col_label_width" "$label") 551 | 552 | # If the label contains "nightly" (case-insensitive), color it red. 553 | if [[ "$label" =~ [Nn]ightly ]]; then 554 | formatted="${red}${formatted}${reset}" 555 | # If this entry is the latest stable candidate, color it cyan. 556 | elif [ "$i" -eq "$latest_stable_index" ]; then 557 | formatted="${cyan}${formatted}${reset}" 558 | # Otherwise, apply yellow to the first pre-release and green to the first stable entry. 559 | elif [[ "$label" == *"(pre-release)"* ]] && [ $pre_colored -eq 0 ]; then 560 | formatted="${yellow}${formatted}${reset}" 561 | pre_colored=1 562 | elif [[ "$label" != *"(pre-release)"* ]] && [ $stable_colored -eq 0 ]; then 563 | formatted="${green}${formatted}${reset}" 564 | stable_colored=1 565 | fi 566 | 567 | # Print the (possibly colored) entry. 568 | formatted_entries+=( "$formatted" ) 569 | done 570 | 571 | # --- Now print that array in reverse order --- 572 | total=${#formatted_entries[@]} 573 | rowcount=0 574 | #num_per_row=${num_per_row:-1} 575 | 576 | for (( idx=total-1; idx>=0; idx-- )); do 577 | # Print the (possibly colored) entry. 578 | printf "%s" "${formatted_entries[$idx]}" 579 | (( rowcount++ )) || true 580 | # Every time we hit 'num_per_row' entries in a row, insert a newline. 581 | if (( rowcount % num_per_row == 0 )); then 582 | echo "" 583 | fi 584 | done 585 | 586 | # If the last row wasn't full, make sure we end on a newline. 587 | if (( rowcount % num_per_row != 0 )); then 588 | echo "" 589 | fi 590 | 591 | # Prompt for the user's selection. 592 | echo "" 593 | read -r -p "Enter the number of your selection: " selection "${CHOSEN_TAG_FILE}" 604 | } 605 | 606 | # Download firmware assets for the chosen release. 607 | download_assets() { 608 | local releases_json chosen_tag download_pattern assets StreamOutput 609 | releases_json=$(get_release_data) 610 | chosen_tag=$(cat "${CHOSEN_TAG_FILE}") 611 | download_pattern="-${chosen_tag}" 612 | 613 | mapfile -t assets < <( 614 | echo "$releases_json" | jq -r --arg TAG "$chosen_tag" ' 615 | .[] | select((.tag_name | ltrimstr("v")) == $TAG) | .assets[] | 616 | select(.name | test("^firmware-"; "i")) | 617 | select(.name | test("debug"; "i") | not) | 618 | {name: .name, url: .browser_download_url} | @base64' 619 | ) 620 | mkdir -p "$DOWNLOAD_DIR" 621 | 622 | # Search for lingering temporary files in the DOWNLOAD_DIR 623 | tmp_files=$(find "$DOWNLOAD_DIR" -maxdepth 1 -type f -name '*.tmp*') 624 | if [ -n "$tmp_files" ]; then 625 | echo "Found temporary files in $DOWNLOAD_DIR:" 626 | echo "$tmp_files" 627 | echo "Cleaning them up..." 628 | find "$DOWNLOAD_DIR" -maxdepth 1 -type f -name '*.tmp*' -delete 629 | fi 630 | 631 | if [ ${#assets[@]} -eq 0 ]; then 632 | echo "No firmware assets found for release $chosen_tag matching criteria." 633 | exit 1 634 | fi 635 | 636 | StreamOutput=0 637 | for asset in "${assets[@]}"; do 638 | local decoded asset_name asset_url local_file 639 | decoded=$(echo "$asset" | base64 --decode) 640 | asset_name=$(echo "$decoded" | jq -r '.name') 641 | asset_url=$(echo "$decoded" | jq -r '.url') 642 | local_file="${DOWNLOAD_DIR}/${asset_name}" 643 | if [ -f "$local_file" ]; then 644 | echo "Already downloaded $asset_name " 645 | StreamOutput=1 646 | printf "\r" 647 | tput cuu1 648 | else 649 | if [ $StreamOutput -eq 1 ]; then 650 | echo "" 651 | StreamOutput=0 652 | fi 653 | tmp_file=$(mktemp --tmpdir="$DOWNLOAD_DIR" "${asset_name}.tmp.XXXXXX") 654 | echo "Downloading $asset_name $asset_url" 655 | if curl -SL --progress-bar -o "$tmp_file" "$asset_url"; then 656 | mv "$tmp_file" "$local_file" 657 | else 658 | echo "Download failed for $asset_name" 659 | rm -f "$tmp_file" 660 | fi 661 | printf "\r" 662 | tput cuu1 663 | tput cuu1 664 | tput el 665 | fi 666 | done 667 | if [ $StreamOutput -eq 1 ]; then 668 | echo "" 669 | fi 670 | echo "$download_pattern" >"${DOWNLOAD_PATTERN_FILE}" 671 | } 672 | 673 | # Unzip downloaded firmware assets into the appropriate folder structure. 674 | unzip_assets() { 675 | local chosen_tag download_pattern asset product target_dir releases_json StreamOutput 676 | chosen_tag=$(cat "${CHOSEN_TAG_FILE}") 677 | download_pattern=$(cat "${DOWNLOAD_PATTERN_FILE}") 678 | releases_json=$(get_release_data) 679 | 680 | mapfile -t assets < <( 681 | echo "$releases_json" | jq -r --arg TAG "$chosen_tag" ' 682 | .[] | select((.tag_name | sub("^v";"")) == $TAG) | .assets[] | 683 | select(.name | test("^firmware-"; "i")) | 684 | select(.name | test("debug"; "i") | not) | 685 | {name: .name} | @base64' 686 | ) 687 | 688 | StreamOutput=0 689 | for asset in "${assets[@]}"; do 690 | local decoded asset_name 691 | decoded=$(echo "$asset" | base64 --decode) 692 | asset_name=$(echo "$decoded" | jq -r '.name') 693 | local_file="${DOWNLOAD_DIR}/${asset_name}" 694 | if [[ "$asset_name" =~ ^firmware-([^-\ ]+)-(.+)\.zip$ ]]; then 695 | product="${BASH_REMATCH[1]}" 696 | target_dir="${FIRMWARE_ROOT}/${chosen_tag}/${product}" 697 | mkdir -p "$target_dir" 698 | if [ -z "$(ls -A "$target_dir" 2>/dev/null)" ]; then 699 | if [ $StreamOutput -eq 1 ]; then 700 | echo "" 701 | fi 702 | 703 | echo "Unzipping $asset_name into $target_dir..." 704 | unzip -o "$local_file" -d "$target_dir" 705 | StreamOutput=0 706 | else 707 | if [ $StreamOutput -eq 0 ]; then 708 | echo "Files already exist for " 709 | echo "$asset_name " 710 | StreamOutput=1 711 | else 712 | echo "$asset_name " 713 | fi 714 | printf "\r" 715 | tput cuu1 716 | fi 717 | else 718 | echo "Asset $asset_name does not match expected naming convention. Skipping unzip." 719 | fi 720 | done 721 | if [ $StreamOutput -eq 1 ]; then 722 | echo "" 723 | fi 724 | } 725 | 726 | # Detect the connected USB device. 727 | detect_device() { 728 | # /dev/ttyACM0 729 | local lsusb_output filtered_device_lines detected_raw detected_line detected_dev fallback newpath search_full 730 | lsusb_output=$(lsusb) 731 | mapfile -t all_device_lines < <(echo "$lsusb_output" | sed -n 's/.*ID [0-9a-fA-F]\{4\}:[0-9a-fA-F]\{4\} //p') 732 | filtered_device_lines=() 733 | for line in "${all_device_lines[@]}"; do 734 | if ! echo "$line" | grep -qiE "hub|ethernet|mouse|keyboard"; then 735 | filtered_device_lines+=("$line") 736 | fi 737 | done 738 | 739 | echo "" 740 | if [ "${#filtered_device_lines[@]}" -eq 0 ]; then 741 | # Prompt user to either re-scan or quit. 742 | echo "USB devices found:" 743 | echo "$lsusb_output" | sed -n 's/.*ID [0-9a-fA-F]\{4\}:[0-9a-fA-F]\{4\} //p' 744 | read -rp "Press Enter to look again or q to quit: " choice < /dev/tty 745 | if [[ "$choice" =~ ^[Qq]$ ]]; then 746 | echo "Exiting." 747 | exit 0 748 | else 749 | detect_device # Call itself again. 750 | return 751 | fi 752 | fi 753 | if [ "${#filtered_device_lines[@]}" -eq 1 ]; then 754 | detected_raw="${filtered_device_lines[0]}" 755 | # Determine detected_dev for the single device: 756 | search_full=$(echo "$detected_raw" | tr ' ' '_' | tr '(' '_' | tr ')' '_' | tr ',' '_') 757 | #echo "$search_full" > /dev/tty 758 | detected_dev="" 759 | for link in /dev/serial/by-id/*; do 760 | if [[ $(basename "$link") == *"$search_full"* ]]; then 761 | detected_dev=$(readlink -f "$link") 762 | break 763 | fi 764 | done 765 | 766 | if [ -z "$detected_dev" ]; then 767 | fallback=$(echo "$detected_raw" | cut -d' ' -f2- | tr ' ' '_' | tr '(' '_' | tr ')' '_' | tr ',' '_') 768 | #echo "$fallback" > /dev/tty 769 | for link in /dev/serial/by-id/*; do 770 | if [[ $(basename "$link") == *"$fallback"* ]]; then 771 | detected_dev=$(readlink -f "$link") 772 | break 773 | fi 774 | done 775 | fi 776 | 777 | if [ -z "$detected_dev" ]; then 778 | third_fallback=$(echo "$detected_raw" | tr ' ' '_' | tr '(' '_' | tr ')' '_' | tr '/' '_' | tr ',' '_' | sed 's/^/usb-/') 779 | #echo "$third_fallback" > /dev/tty 780 | for link in /dev/serial/by-id/*; do 781 | if [[ $(basename "$link") == *"$third_fallback"* ]]; then 782 | detected_dev=$(readlink -f "$link") 783 | break 784 | fi 785 | done 786 | fi 787 | else 788 | # Multiple devices detected; ensure meshtastic is available. 789 | newpath=0 790 | source "$HOME/.bashrc" 791 | if ! command -v pipx &>/dev/null; then 792 | echo "Installing pipx" 793 | sudo apt -y install pipx 794 | fi 795 | if ! command -v meshtastic &>/dev/null; then 796 | pipx install "meshtastic[cli]" 797 | newpath=1 798 | fi 799 | if [ $newpath -eq 1 ]; then 800 | pipx ensurepath 801 | # shellcheck disable=SC1091 802 | source "$HOME/.bashrc" 803 | fi 804 | 805 | 806 | declare -a detected_devs menu_options 807 | declare -gA seen_dev=() 808 | echo "Multiple USB devices detected:" 809 | for idx in "${!filtered_device_lines[@]}"; do 810 | local device_info search_full detected_dev version 811 | device_info="${filtered_device_lines[$idx]}" 812 | # Determine detected_dev for this device: 813 | search_full=$(echo "$device_info" | tr ' ' '_') 814 | detected_dev="" 815 | 816 | for link in /dev/serial/by-id/*; do 817 | if [[ $(basename "$link") == *"$search_full"* ]]; then 818 | detected_dev=$(readlink -f "$link") 819 | if [[ -z ${seen_dev[$detected_dev]+_} ]]; then 820 | break 821 | fi 822 | detected_dev="" 823 | fi 824 | done 825 | 826 | 827 | if [ -z "$detected_dev" ]; then 828 | fallback=$(echo "$device_info" | cut -d' ' -f2- | tr ' ' '_') 829 | for link in /dev/serial/by-id/*; do 830 | if [[ $(basename "$link") == *"$fallback"* ]]; then 831 | detected_dev=$(readlink -f "$link") 832 | if [[ -z ${seen_dev[$detected_dev]+_} ]]; then 833 | break 834 | fi 835 | detected_dev="" 836 | fi 837 | done 838 | fi 839 | 840 | if [ -z "$detected_dev" ]; then 841 | third_fallback=$(echo "$device_info" | tr ' ' '_' | tr '/' '_' | sed 's/^/usb-/') 842 | for link in /dev/serial/by-id/*; do 843 | if [[ $(basename "$link") == *"$third_fallback"* ]]; then 844 | detected_dev=$(readlink -f "$link") 845 | if [[ -z ${seen_dev[$detected_dev]+_} ]]; then 846 | break 847 | fi 848 | detected_dev="" 849 | fi 850 | done 851 | fi 852 | 853 | # mark as taken so later iterations won’t reuse it 854 | if [[ -n "$detected_dev" ]]; then 855 | seen_dev["$detected_dev"]=1 856 | fi 857 | 858 | detected_devs[idx]="$detected_dev" 859 | # If we found a detected_dev, try to get its firmware version. 860 | if [ -n "$detected_dev" ]; then 861 | lockedService=$(get_locked_service "$detected_dev") 862 | if [ -n "$lockedService" ] && [ "$lockedService" != "None" ]; then 863 | spinner 864 | #echo "Stopping $lockedService" 865 | sudo systemctl stop "$lockedService" 866 | fi 867 | spinner 868 | # Run meshtastic --device-metadata and extract the firmware_version. 869 | # Redirect stderr to hide extra log messages. 870 | # Attempt to get the firmware version with a 10 second timeout. 871 | version=$(timeout 12 meshtastic --port "$detected_dev" --device-metadata 2>/dev/null | awk -F': ' '/^firmware_version:/ {print $2; exit}' || true) 872 | if [ -z "$version" ]; then 873 | version="unknown version" 874 | fi 875 | 876 | if [ -n "$lockedService" ] && [ "$lockedService" != "None" ]; then 877 | spinner 878 | #echo "Starting $lockedService" 879 | sudo systemctl start "$lockedService" 880 | fi 881 | spinner 882 | else 883 | version="unknown" 884 | fi 885 | menu_options[idx]="${device_info} -> ${detected_dev} (${version})" 886 | done 887 | # Print the menu with version information. 888 | printf "\r" 889 | for idx in "${!menu_options[@]}"; do 890 | printf "%d) %s\n" $((idx + 1)) "${menu_options[$idx]}" 891 | done 892 | # Prompt user selection. 893 | while true; do 894 | read -r -p "Please select a device [1-${#menu_options[@]}]: " selection $detected_dev" >"${DEVICE_INFO_FILE}" 909 | normalize "$detected_raw" >"${DETECTED_PRODUCT_FILE}" 910 | } 911 | 912 | # Match the firmware files against the detected device. 913 | match_firmware_files() { 914 | local chosen_tag download_pattern detected_product 915 | chosen_tag=$(cat "${CHOSEN_TAG_FILE}") 916 | download_pattern=$(cat "${DOWNLOAD_PATTERN_FILE}") 917 | detected_product=$(cat "${DETECTED_PRODUCT_FILE}") 918 | detected_info_file=$(cat "${DEVICE_INFO_FILE}") 919 | device_name=$(echo "$detected_info_file" | awk -F'-> ' '{print $1}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') 920 | device_port_name=$(echo "$detected_info_file" | awk -F'-> ' '{print $2}') 921 | # Remove everything up to (and including) "ID " 922 | temp="${device_name#*ID }" 923 | # Remove the first word from the remainder (the device ID) plus the following space. 924 | result="${temp#* }" 925 | echo "$result -> $device_port_name" 926 | 927 | USBproduct=$(lsusb -v 2>/dev/null | 928 | grep -A 20 "${device_name}" | 929 | grep "iProduct" | 930 | grep -vi "Controller" | 931 | sed -n 's/.*2[[:space:]]\+\([^[:space:]]\+\).*/\1/p' | 932 | head -n 1 | 933 | tr '[:upper:]' '[:lower:]') 934 | 935 | declare -A product_files 936 | declare -A product_files_full 937 | 938 | while IFS= read -r -d '' file; do 939 | local fname prod prodNorm 940 | fname=$(basename "$file") 941 | # Updated regex: group 1 is the prefix, group 2 is the product part. 942 | if [[ "$fname" =~ ^(firmware-)(.*)${download_pattern//v/}(-update)?\.(bin|uf2|zip)$ ]]; then 943 | prod="${BASH_REMATCH[2]}" 944 | prodNorm=$(normalize "$prod") 945 | 946 | # strip any tft|inkhud|eink suffix for grouping 947 | if [[ $prodNorm =~ ^(.+?)(tft|inkhud|eink)$ ]]; then 948 | base=${BASH_REMATCH[1]} 949 | else 950 | base=$prodNorm 951 | fi 952 | 953 | product_files["$base"]+="$file"$'\n' 954 | product_files_full["$base"]+="$prod"$'\n' 955 | fi 956 | spinner 957 | done < <( find "$FIRMWARE_ROOT/${chosen_tag}" -type f \( -iname "firmware-*" \) -print0 ) 958 | 959 | matching_keys=() 960 | if [ -z "${product_files+x}" ] || [ ${#product_files[@]} -eq 0 ]; then 961 | for prod in "${!product_files[@]}"; do 962 | local norm_prod 963 | norm_prod=$(normalize "$prod") 964 | if [[ "$norm_prod" == *"$detected_product"* ]] || [[ "$detected_product" == *"$norm_prod"* ]]; then 965 | printf "\r" 966 | echo "Firmware file match on: $(echo "${product_files_full[$prod]}" | head -n1)" 967 | matching_keys+=("$prod") 968 | fi 969 | spinner 970 | done 971 | fi 972 | 973 | if [ -z "${matching_files+x}" ] || [ ${#matching_files[@]} -eq 0 ]; then 974 | IFS=$'\n' read -r -d '' -a matching_files < <( 975 | for key in "${matching_keys[@]}"; do 976 | echo "${product_files[$key]}" 977 | spinner 978 | done 979 | printf '\0' 980 | ) 981 | fi 982 | 983 | printf "\r" 984 | if [ ${#matching_files[@]} -eq 0 ]; then 985 | echo "Doing a deep search for $USBproduct in $FIRMWARE_ROOT/${chosen_tag}/*" 986 | # Capture all matching file paths (each on a new line) 987 | found_files=$(grep -aFrin --exclude="*-ota.zip" "$USBproduct" "$FIRMWARE_ROOT/${chosen_tag}" | cut -d: -f1 || true) 988 | 989 | if [ -z "$found_files" ]; then 990 | echo "No firmware files match the detected product ($detected_product) ($USBproduct). Exiting." 991 | exit 1 992 | fi 993 | 994 | # Filter the found files so that only files whose basename starts with "firmware-" are kept. 995 | found_files=$(echo "$found_files" | while IFS= read -r line; do 996 | base=$(basename "$line") 997 | if [[ "$base" == firmware-* ]]; then 998 | echo "$line" 999 | fi 1000 | done) 1001 | 1002 | # Populate matching_files array with all found file paths. 1003 | IFS=$'\n' read -r -d '' -a matching_files < <( 1004 | echo "$found_files" 1005 | printf '\0' 1006 | ) 1007 | 1008 | fi 1009 | 1010 | # If no matches are found for the device, fall back to *all* firmware files in the chosen tag. 1011 | if [ "${#matching_files[@]}" -eq 0 ]; then 1012 | echo "No firmware matched for the detected device: $detected_product" 1013 | mapfile -t matching_files < <( 1014 | find "$FIRMWARE_ROOT/${chosen_tag}" -type f \( -iname "firmware-*" \) -print0 1015 | while IFS= read -r -d '' file; do 1016 | # Print "basenamefull_path" 1017 | echo -e "$(basename "$file")\t$file" 1018 | done | sort -f -k1,1 | cut -f2- 1019 | ) 1020 | fi 1021 | 1022 | # Post-process the matching_files array to remove duplicate entries. 1023 | readarray -t matching_files < <(printf "%s\n" "${matching_files[@]}" | sort -u) 1024 | 1025 | printf "%s\n" "${matching_files[@]}" >"${MATCHING_FILES_FILE}" 1026 | } 1027 | 1028 | # Determine whether to perform an update or install operation. 1029 | choose_operation() { 1030 | readarray -t matching_files <"${MATCHING_FILES_FILE}" 1031 | selected_file=$(cat "${SELECTED_FILE_FILE}") 1032 | architecture=$(cat "${ARCHITECTURE_FILE}") 1033 | 1034 | local operation 1035 | operation="update" 1036 | if [ -n "$OPERATION_ARG" ]; then 1037 | operation="$OPERATION_ARG" 1038 | else 1039 | if echo "$architecture" | grep -qi "esp32"; then 1040 | if [[ "$selected_file" == *"-update"* ]]; then 1041 | operation="update" 1042 | else 1043 | operation="install" 1044 | fi 1045 | fi 1046 | fi 1047 | 1048 | echo "$operation" >"${OPERATION_FILE}" 1049 | echo "Operation chosen: $operation" 1050 | } 1051 | 1052 | # Let the user select which firmware file to use if multiple are found. 1053 | select_firmware_file() { 1054 | local matching_files count selected_file file_choice firmware_candidates=() 1055 | readarray -t matching_files <"${MATCHING_FILES_FILE}" 1056 | count=${#matching_files[@]} 1057 | 1058 | if [ "$count" -eq 0 ]; then 1059 | echo "No matching firmware files found." 1060 | exit 1 1061 | fi 1062 | 1063 | # If only one file, no choice needed: 1064 | if [ "$count" -eq 1 ]; then 1065 | selected_file="${matching_files[0]}" 1066 | else 1067 | 1068 | for f in "${matching_files[@]}"; do 1069 | if [[ "$(basename "$f")" =~ \.(bin|uf2)$ ]]; then 1070 | firmware_candidates+=("$f") 1071 | fi 1072 | done 1073 | # Sort the firmware_candidates array. 1074 | readarray -t firmware_candidates < <(printf '%s\n' "${firmware_candidates[@]}" | sort) 1075 | 1076 | # If exactly one firmware candidate, auto-select it. 1077 | if [ ${#firmware_candidates[@]} -eq 1 ]; then 1078 | echo "Auto-selecting firmware candidate: $(basename "${firmware_candidates[0]}")" 1079 | selected_file="${firmware_candidates[0]}" 1080 | elif [ ${#firmware_candidates[@]} -gt 1 ]; then 1081 | echo "Multiple matching firmware candidates files found:" 1082 | # Figure out how many lines we'll print. 1083 | count_candidates=${#firmware_candidates[@]} 1084 | # The number of digits in that count — e.g., 2 if 10..99, 3 if 100..999 1085 | idx_width=${#count_candidates} 1086 | 1087 | for i in "${!firmware_candidates[@]}"; do 1088 | # Print each line so that indices are right-aligned to idx_width. 1089 | printf "%${idx_width}d. %s\n" \ 1090 | $((i + 1)) \ 1091 | "$(basename "${firmware_candidates[$i]}")" 1092 | done 1093 | read -r -p "Select which firmware file to use [1-${#firmware_candidates[@]}]: " file_choice "${SELECTED_FILE_FILE}" 1109 | } 1110 | 1111 | # prompt_for_firmware: 1112 | # Prompts the user to select from all matching_files in interactive mode. 1113 | prompt_for_firmware() { 1114 | local file_list=("${matching_files[@]}") 1115 | local count_choice 1116 | echo "Multiple matching firmware files found:" 1117 | for i in "${!file_list[@]}"; do 1118 | echo "$((i + 1)). $(basename "${file_list[$i]}")" 1119 | done 1120 | read -r -p "Select which firmware file to use [1-${#file_list[@]}]: " count_choice ' '{print $2}') 1139 | architecture=$(cat "${ARCHITECTURE_FILE}") 1140 | 1141 | script_to_run="" 1142 | abs_selected="" 1143 | if [ "$selected_file" ]; then 1144 | if [ "$operation" = "update" ]; then 1145 | script_to_run="$(dirname "$selected_file")/device-update.sh" 1146 | elif [ "$operation" = "install" ]; then 1147 | script_to_run="$(dirname "$selected_file")/device-install.sh" 1148 | fi 1149 | abs_selected="$(cd "$(dirname "$selected_file")" && pwd)/$(basename "$selected_file")" 1150 | fi 1151 | 1152 | # Adjust baud rate for ESP32 firmware. 1153 | if echo "$architecture" | grep -qi "esp32"; then 1154 | if [ -f "$script_to_run" ]; then 1155 | # Changes for update 1156 | if [[ "$script_to_run" == *update* ]]; then 1157 | # Ensure the baud rate is set correctly 1158 | sed -i 's/--baud 115200/--baud 1200/g' "$script_to_run" 1159 | 1160 | # Remove any existing --port argument 1161 | sed -i 's/--port [^ ]* //g' "$script_to_run" 1162 | 1163 | # Add the new --port argument before --baud 1200, using a different delimiter (|) 1164 | sed -i "s|--baud 1200|--port ${device_port_name} --baud 1200 |g" "$script_to_run" 1165 | else 1166 | # Changes for install 1167 | if ! grep -q '^.*sleep 5$' "$script_to_run"; then 1168 | 1169 | # Create a temporary diff file. 1170 | diff_file=$(mktemp) 1171 | 1172 | cat << 'EOF' > "$diff_file" 1173 | index bacf48f..c75bcd9 100755 1174 | --- a/device-install.sh 1175 | +++ b/device-install.sh 1176 | @@ -56,6 +56,7 @@ else 1177 | echo "Error: esptool not found" 1178 | exit 1 1179 | fi 1180 | +ESPTOOL_CMD="$ESPTOOL_CMD --baud 1200" 1181 | 1182 | set -e 1183 | 1184 | # Usage info 1185 | @@ -190,13 +191,21 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then 1186 | exit 1 1187 | fi 1188 | 1189 | - echo "Trying to flash ${FILENAME}, but first erasing and writing system information" 1190 | + echo "" 1191 | + echo "First erasing the flash" 1192 | $ESPTOOL_CMD erase_flash 1193 | + sleep 5 1194 | + echo "" 1195 | + echo "Trying to flash ${FILENAME} at offset 0x00" 1196 | $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" 1197 | + sleep 7 1198 | + echo "" 1199 | echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" 1200 | - $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" 1201 | + $ESPTOOL_CMD write_flash ${OTA_OFFSET} "${OTAFILE}" 1202 | + sleep 9 1203 | + echo "" 1204 | echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" 1205 | - $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" 1206 | + $ESPTOOL_CMD write_flash ${OFFSET} "${SPIFFSFILE}" 1207 | 1208 | else 1209 | show_help 1210 | EOF 1211 | # Apply the diff to $script_to_run 1212 | patch --fuzz=3 --ignore-whitespace "$script_to_run" < "$diff_file" 1213 | 1214 | # Remove the temporary diff file. 1215 | rm -f "$diff_file" 1216 | fi 1217 | fi 1218 | 1219 | else 1220 | echo "No $(basename "$script_to_run") found. Skipping baud rate change." 1221 | fi 1222 | fi 1223 | 1224 | abs_script="" 1225 | if [ "$script_to_run" ]; then 1226 | if [ ! -x "$script_to_run" ]; then 1227 | chmod +x "$script_to_run" 1228 | fi 1229 | abs_script="$(cd "$(dirname "$script_to_run")" && pwd)/$(basename "$script_to_run")" 1230 | fi 1231 | 1232 | printf "%s\n" "$abs_script" "$abs_selected" >"${CMD_FILE}" 1233 | } 1234 | 1235 | check_tty_lock () { 1236 | local dev=$1 1237 | [[ -e $dev ]] || { return 2; } 1238 | 1239 | # Open the device on fd 3 read-write (<>). Most distros let "dialout" 1240 | # members do this without sudo. 1241 | exec 3<>"$dev" 2>/dev/null || { return 2; } 1242 | 1243 | # Try to grab an exclusive, *non-blocking* lock on fd 3. 1244 | if flock -n 3; then # got the lock. device is FREE 1245 | #echo "FREE" 1246 | flock -u 3 # immediately unlock 1247 | exec 3>&- # close fd 1248 | return 0 1249 | else # lock failed. someone else holds it 1250 | #echo "BUSY" 1251 | exec 3>&- 1252 | return 1 1253 | fi 1254 | } 1255 | 1256 | 1257 | get_locked_service() { 1258 | # If the input contains "-> ", extract the part after it; otherwise, use the whole input. 1259 | if [[ "$1" == *"-> "* ]]; then 1260 | device_name=$(echo "$1" | awk -F'-> ' '{print $2}') 1261 | else 1262 | device_name="$1" 1263 | fi 1264 | # Accept an optional argument for the device; default to /dev/ttyACM0. 1265 | #local device_name="/dev/ttyACM0" 1266 | #echo "Device: $device_name" 1267 | 1268 | if check_tty_lock "$device_name"; then 1269 | return 0 1270 | fi 1271 | 1272 | # Get all users locking the device (skip the header line) 1273 | echo "Finding the service that has $device_name locked" > /dev/tty 1274 | local users 1275 | if ! command -v lsof &>/dev/null; then 1276 | sudo apt install -y lsof 1277 | fi 1278 | users=$(sudo lsof "$device_name" 2>/dev/null | awk 'NR>1 {print $3}' | sort -u) 1279 | if [ -z "$users" ]; then 1280 | #echo "No process found locking ${device_name}." 1281 | return 0 1282 | fi 1283 | #echo "User(s): $users" 1284 | 1285 | # For each user, get all their PIDs. 1286 | local pids 1287 | pids=$(ps -u "$users" -o pid= | tr -s ' ' | tr '\n' ' ') 1288 | #echo "PIDs: $pids" 1289 | 1290 | local found_service="" 1291 | #local last_pid="" 1292 | for pid in $pids; do 1293 | #echo "PID: $pid" 1294 | 1295 | # Get the full command line for the process. 1296 | local cmd 1297 | cmd=$(ps -p "$pid" -o cmd= | awk '{$1=$1};1') 1298 | #echo "Command: $cmd" 1299 | 1300 | # Search for a systemd service file referencing the executable. 1301 | # Using || true so that grep failing does not exit the script. 1302 | local raw_service 1303 | raw_service=$({ sudo grep -sR "$cmd" /etc/systemd/system/ 2>/dev/null || true; } | awk -F: '{print $1}' | sort -u) 1304 | #echo "Raw service info: $raw_service" 1305 | 1306 | local service 1307 | if [ -n "$raw_service" ]; then 1308 | service=$(echo "$raw_service" | xargs -n1 basename | sort -u) 1309 | else 1310 | service="None" 1311 | fi 1312 | #echo "Service: $service" 1313 | 1314 | # If a service file was found, store it. 1315 | if [ "$service" != "None" ]; then 1316 | found_service="$found_service $service" 1317 | fi 1318 | #last_pid="$pid" 1319 | done 1320 | 1321 | #if [ -n "$found_service" ] && [ "$found_service" != "None" ]; then 1322 | # echo "Service locking $device_name: $found_service" 1323 | #else 1324 | # echo "Found matching process(es), but no systemd service file was identified." 1325 | # echo "Last checked PID: $last_pid" 1326 | # return 1 1327 | #fi 1328 | echo "$found_service" | awk '{$1=$1};1' 1329 | } 1330 | 1331 | detect_esp() { 1332 | selected_file=$(cat "${SELECTED_FILE_FILE}") 1333 | chosen_tag=$(cat "${CHOSEN_TAG_FILE}") 1334 | architecture="" 1335 | echo "$architecture" > "${ARCHITECTURE_FILE}" 1336 | 1337 | if echo "$selected_file" | grep -qi "esp32"; then 1338 | architecture="esp32" 1339 | 1340 | echo "$architecture" > "${ARCHITECTURE_FILE}" 1341 | return 1342 | fi 1343 | 1344 | if grep -E -q "${chosen_tag}.*nightly" "$VERSIONS_LABELS_FILE"; then 1345 | update_hardware_list 1346 | echo "Searching for the hardware type; is this ESP32?" 1347 | 1348 | # Get just the filename. 1349 | base=$(basename "$selected_file") 1350 | # Remove the "firmware-" prefix. 1351 | result=${base#firmware-} 1352 | # Remove the trailing -update.bin 1353 | result=${result%-update.bin} 1354 | 1355 | # Build a pattern that should be removed at the end. 1356 | pattern="-$chosen_tag.bin" 1357 | # Remove the trailing pattern. 1358 | result=${result%"$pattern"} 1359 | 1360 | # Build a pattern that should be removed at the end. 1361 | pattern="-$chosen_tag" 1362 | # Remove the trailing pattern. 1363 | result=${result%"$pattern"} 1364 | 1365 | norm_device=$(normalize "$result") 1366 | json_data=$( cat "$RESOURCES_FILE" ) 1367 | 1368 | # Convert the JSON string to an array of objects and loop over each 1369 | echo "$json_data" | jq -c '.[]' | while read -r entry; do 1370 | # Extract platformioTarget and displayName using jq 1371 | pt=$(echo "$entry" | jq -r '.platformioTarget') 1372 | dn=$(echo "$entry" | jq -r '.displayName') 1373 | 1374 | # Normalize values (assuming you have a normalize function or just convert to lowercase) 1375 | norm_pt=$(normalize "$pt") 1376 | norm_dn=$(normalize "$dn") 1377 | 1378 | # If either normalized field matches the normalized device name, extract the architecture 1379 | if [[ "$norm_pt" == *"$norm_device"* ]] || [[ "$norm_device" == *"$norm_pt"* ]] || [[ "$norm_dn" == *"$norm_device"* ]] || [[ "$norm_device" == *"$norm_dn"* ]]; then 1380 | architecture=$(echo "$entry" | jq -r '.architecture') 1381 | echo "$architecture" > "${ARCHITECTURE_FILE}" 1382 | break 1383 | fi 1384 | spinner 1385 | done 1386 | printf "\r" 1387 | fi 1388 | } 1389 | 1390 | list_block_devs() { 1391 | lsblk -nrpo NAME | sort; 1392 | } 1393 | 1394 | # Run the firmware update/install script. 1395 | run_update_script() { 1396 | local cmd user_choice PYTHON ESPTOOL_CMD newpath device_name 1397 | mapfile -t cmd_array <"$CMD_FILE" 1398 | abs_script="${cmd_array[0]}" 1399 | abs_selected="${cmd_array[1]}" 1400 | cmd="${cmd_array[*]}" 1401 | detected_dev=$(cat "${DEVICE_INFO_FILE}") 1402 | device_name=$(echo "$detected_dev" | awk -F'-> ' '{print $1}' | sed -E 's/^Bus [0-9]+ Device [0-9]+: ID [[:alnum:]]+:[[:alnum:]]+ //') 1403 | device_name=$(echo "$device_name" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr -s '[:space:]') 1404 | architecture=$(cat "${ARCHITECTURE_FILE}") 1405 | operation=$(cat "${OPERATION_FILE}") 1406 | basename_selected="$(basename "$abs_selected")" 1407 | device_port_name=$(echo "$detected_dev" | awk -F'-> ' '{print $2}') 1408 | 1409 | if echo "$architecture" | grep -qi "esp32"; then 1410 | update_bleota 1411 | 1412 | echo "Command to run for firmware $operation:" 1413 | echo "$abs_script -p ${device_port_name} -f $basename_selected" 1414 | else 1415 | echo "$basename_selected" 1416 | fi 1417 | 1418 | if $RUN_UPDATE; then 1419 | user_choice="y" 1420 | else 1421 | read -r -p "Would you like to $operation the firmware? (y/N): " user_choice /dev/null; then 1431 | echo "Installing pipx" 1432 | sudo apt -y install pipx 1433 | fi 1434 | if ! command -v meshtastic &>/dev/null; then 1435 | pipx install "meshtastic[cli]" 1436 | pipx ensurepath 1437 | # shellcheck disable=SC1091 1438 | source "$HOME/.bashrc" 1439 | fi 1440 | 1441 | # Locate a Python interpreter. 1442 | PYTHON="" 1443 | for candidate in python3 python; do 1444 | if command -v "$candidate" >/dev/null 2>&1; then 1445 | PYTHON=$(command -v "$candidate") 1446 | break 1447 | fi 1448 | done 1449 | if [ -z "$PYTHON" ]; then 1450 | echo "No Python interpreter found. Installing python3..." 1451 | sudo apt update && sudo apt install -y python3 pipx 1452 | PYTHON=$(command -v python3) || { 1453 | echo "Failed to install python3" 1454 | exit 1 1455 | } 1456 | fi 1457 | 1458 | # Determine the esptool command. 1459 | if echo "$architecture" | grep -qi "esp32"; then 1460 | if "$PYTHON" -m esptool version >/dev/null 2>&1; then 1461 | ESPTOOL_CMD="$PYTHON -m esptool" 1462 | elif command -v esptool >/dev/null 2>&1; then 1463 | ESPTOOL_CMD="esptool" 1464 | elif command -v esptool.py >/dev/null 2>&1; then 1465 | ESPTOOL_CMD="esptool.py" 1466 | else 1467 | pipx install esptool 1468 | ESPTOOL_CMD="esptool.py" 1469 | pipx ensurepath 1470 | # shellcheck disable=SC1091 1471 | source "$HOME/.bashrc" 1472 | fi 1473 | fi 1474 | 1475 | # Check if any services are locking up the device 1476 | echo "$detected_dev" 1477 | lockedService=$(get_locked_service "$detected_dev") 1478 | if [ -n "$lockedService" ] && [ "$lockedService" != "None" ]; then 1479 | echo "Stopping service $lockedService..." 1480 | sudo systemctl stop "$lockedService" 1481 | fi 1482 | 1483 | 1484 | # Make a backup of the config. 1485 | echo "Making a backup of the configuration." 1486 | basename_device_port_name="$(basename "$device_port_name")" 1487 | backup_config_name="config_backup.${architecture}.${device_name}.${basename_device_port_name}.$(date +%s).yaml" 1488 | backup_config_name_sanitized=$(echo "$backup_config_name" | tr '/' '_' | tr ' ' '_') 1489 | while true; do 1490 | if meshtastic --port "${device_port_name}" --export-config > "${backup_config_name_sanitized}"; then 1491 | echo "Backup configuration created: ${backup_config_name_sanitized}" 1492 | break 1493 | else 1494 | echo "Warning: Timed out waiting for connection completion. Config backup not done." >&2 1495 | read -rp "Press Enter to try again or type 'skip' to skip the creation: " response 1496 | if [ "$response" = "skip" ]; then 1497 | echo "Skipping config backup." 1498 | rm -f "${backup_config_name_sanitized}" 1499 | break 1500 | fi 1501 | sleep 1 1502 | fi 1503 | done 1504 | 1505 | # Execute update for ESP32 or non-ESP32 devices. 1506 | if echo "$architecture" | grep -qi "esp32"; then 1507 | export ESPTOOL_PORT=$device_port_name 1508 | echo "Setting device into bootloader mode via baud 1200" 1509 | $ESPTOOL_CMD --port "${device_port_name}" --baud 1200 chip_id || true 1510 | sleep 8 1511 | # Change directory to the script's folder. 1512 | pushd "$(dirname "$abs_selected")" > /dev/null || { echo "Failed to change directory"; exit 1; } 1513 | 1514 | echo "Running: \"$abs_script\" -p \"${device_port_name}\" -f \"$basename_selected\"" 1515 | "$abs_script" -p "${device_port_name}" -f "$basename_selected" 1516 | echo "" 1517 | echo "If you see no errors above then" 1518 | echo "Firmware $operation for ESP32 device ${device_name} completed on port ${device_port_name}." 1519 | popd > /dev/null 1520 | if [ -f "${backup_config_name_sanitized}" ]; then 1521 | echo "Configuration can be restored using this if it was wiped out" 1522 | echo "meshtastic --configure \"${backup_config_name_sanitized}\"" 1523 | fi 1524 | 1525 | else 1526 | attempt=0 1527 | max_attempts=3 1528 | device_id="" 1529 | 1530 | while [ $attempt -lt $max_attempts ]; do 1531 | echo "Setting device into bootloader mode via meshtastic --enter-dfu --port ${device_port_name}" 1532 | old_output=$(list_block_devs) 1533 | 1534 | meshtastic --enter-dfu --port "${device_port_name}" || true 1535 | sleep 5 1536 | 1537 | new_output=$(list_block_devs) 1538 | 1539 | device_id=$(comm -13 <(echo "$old_output") <(echo "$new_output") | head -n 1) 1540 | 1541 | if [ -n "$device_id" ]; then 1542 | break # New block device found, exit the loop. 1543 | fi 1544 | 1545 | echo "Error: Device failed to enter DFU mode (no new block devices detected)." 1546 | attempt=$((attempt + 1)) 1547 | if [ $attempt -lt $max_attempts ]; then 1548 | echo "Retrying ($attempt/$max_attempts)..." 1549 | sleep 5 1550 | fi 1551 | done 1552 | 1553 | if [ -z "$device_id" ]; then 1554 | echo "Error: Device failed to enter DFU mode after $max_attempts attempts." 1555 | exit 1 1556 | fi 1557 | 1558 | # Check if the device is already mounted by looking in /proc/mounts. 1559 | if grep -q "^$device_id " /proc/mounts; then 1560 | echo "$device_id is already mounted." 1561 | else 1562 | echo "$device_id is not mounted. Mounting now..." 1563 | sudo mkdir -p "$MOUNT_FOLDER" 1564 | sudo mount "$device_id" "$MOUNT_FOLDER" 1565 | fi 1566 | 1567 | echo "Contents of $MOUNT_FOLDER:" 1568 | ls "$MOUNT_FOLDER" 1569 | 1570 | sudo cp -v "$abs_selected" "$MOUNT_FOLDER/" 1571 | echo "" 1572 | echo "Firmware $operation for ESP32 device ${device_name} completed on port ${device_port_name}." 1573 | if [ -f "${backup_config_name_sanitized}" ]; then 1574 | echo "Configuration can be restored using this if it was wiped out" 1575 | echo "meshtastic --configure \"${backup_config_name_sanitized}\"" 1576 | fi 1577 | 1578 | fi 1579 | 1580 | # Restart the stopped service. 1581 | if [ -n "$lockedService" ] && [ "$lockedService" != "None" ]; then 1582 | echo "Starting service $lockedService..." 1583 | sudo systemctl start "$lockedService" 1584 | fi 1585 | } 1586 | 1587 | ################## 1588 | # Main Execution # 1589 | ################## 1590 | parse_args "$@" 1591 | update_releases 1592 | 1593 | # Build the release menu and allow selection. 1594 | release_json=$(get_release_data) 1595 | 1596 | build_release_menu "$release_json" # ${VERSIONS_TAGS_FILE} ${VERSIONS_LABELS_FILE} 1597 | select_release # ${CHOSEN_TAG_FILE} 1598 | 1599 | chosen_tag=$(cat "${CHOSEN_TAG_FILE}") 1600 | if grep -E -q "${chosen_tag}.*nightly" "$VERSIONS_LABELS_FILE"; then 1601 | download_pattern="-${chosen_tag}" 1602 | echo "Nightly build selected; skipping download and unzip." 1603 | echo "$download_pattern" >"${DOWNLOAD_PATTERN_FILE}" 1604 | else 1605 | download_assets # ${DOWNLOAD_PATTERN_FILE} 1606 | unzip_assets 1607 | fi 1608 | detect_device # ${DEVICE_INFO_FILE} ${DETECTED_PRODUCT_FILE} 1609 | match_firmware_files # ${MATCHING_FILES_FILE} 1610 | select_firmware_file # ${SELECTED_FILE_FILE} 1611 | detect_esp # ${ARCHITECTURE_FILE} 1612 | choose_operation # ${OPERATION_FILE} 1613 | prepare_script # ${CMD_FILE} 1614 | run_update_script 1615 | --------------------------------------------------------------------------------