├── .gitignore ├── .gitmodules ├── .project ├── LICENSE.md ├── README.md ├── altbuild.sh ├── bufferflow.go ├── bufferflow_default.go ├── bufferflow_dummypause.go ├── bufferflow_grbl.go ├── bufferflow_tinyg.go ├── compile_webidebridge.sh ├── config.ini ├── conn.go ├── discovery.go ├── download.go ├── dummy.go ├── fakecerts ├── cert.pem └── key.pem ├── home.html ├── hub.go ├── main.go ├── programmer.go ├── queue.go ├── serial.go ├── seriallist.go ├── seriallist_darwin.go ├── seriallist_linux.go ├── seriallist_windows.go ├── serialport.go ├── snapshot ├── README.md └── downloads.md ├── trayicon.go ├── trayicon_linux_arm.go ├── update.go └── utilities.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | bufferflow_tinyg_old.md 3 | 4 | serial-port-json-server 5 | 6 | snapshot/* 7 | public/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "arduino"] 2 | path = arduino 3 | url = https://github.com/facchinm/arduino-flash-tools 4 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | sps 4 | 5 | 6 | 7 | 8 | 9 | com.googlecode.goclipse.goBuilder 10 | 11 | 12 | 13 | 14 | 15 | goclipse.goNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 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 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /altbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "About to cross-compile Serial Port JSON Server" 4 | #echo '$0 = ' $0 5 | #echo '$1 = ' $1 6 | #echo '$2 = ' $2 7 | 8 | if [ "$1" = "" ]; then 9 | echo "You need to pass in the version number as the first parameter." 10 | exit 11 | fi 12 | 13 | # turn on echo 14 | set -x 15 | #set -v 16 | 17 | # Windows x32 and x64, Linux 18 | goxc -bc=windows,linux -d="." -pv=$1 -tasks-=pkg-build default -GOARM=6 19 | 20 | # Rename arm to arm6 21 | #set +x 22 | FILE=$1'/serial-port-json-server_'$1'_linux_arm.tar.gz' 23 | FILE2=$1'/serial-port-json-server_'$1'_linux_armv6.tar.gz' 24 | #set -x 25 | mv $FILE $FILE2 26 | 27 | # Special build for armv7 for BBB and Raspi2 28 | goxc -bc=linux,arm -d="." -pv=$1 -tasks-=pkg-build default -GOARM=7 29 | FILE3=$1'/serial-port-json-server_'$1'_linux_armv7.tar.gz' 30 | mv $FILE $FILE3 31 | 32 | # Special build for armv8 33 | goxc -bc=linux,arm -d="." -pv=$1 -tasks-=pkg-build default -GOARM=8 34 | FILE4=$1'/serial-port-json-server_'$1'_linux_armv8.tar.gz' 35 | mv $FILE $FILE4 36 | 37 | -------------------------------------------------------------------------------- /bufferflow.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"log" 5 | //"time" 6 | ) 7 | 8 | var availableBufferAlgorithms = []string{"default", "tinyg", "dummypause", "grbl"} 9 | 10 | type BufferMsg struct { 11 | Cmd string 12 | Port string 13 | TriggeringResponse string 14 | //Desc string 15 | //Desc string 16 | } 17 | 18 | type Bufferflow interface { 19 | BlockUntilReady(cmd string, id string) (bool, bool) // implement this method 20 | //JustQueue(cmd string, id string) bool // implement this method 21 | OnIncomingData(data string) // implement this method 22 | ClearOutSemaphore() // implement this method 23 | BreakApartCommands(cmd string) []string // implement this method 24 | Pause() // implement this method 25 | Unpause() // implement this method 26 | SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool // implement this method 27 | SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool // implement this method 28 | SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool // implement this method 29 | SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool // implement this method 30 | SeeIfSpecificCommandsReturnNoResponse(cmd string) bool // implement this method 31 | ReleaseLock() // implement this method 32 | IsBufferGloballySendingBackIncomingData() bool // implement this method 33 | Close() // implement this method 34 | } 35 | 36 | /*data packets returned to client*/ 37 | type DataCmdComplete struct { 38 | Cmd string 39 | Id string 40 | P string 41 | BufSize int `json:"-"` 42 | D string `json:"-"` 43 | } 44 | 45 | type DataPerLine struct { 46 | P string 47 | D string 48 | } 49 | -------------------------------------------------------------------------------- /bufferflow_default.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | //"regexp" 6 | //"strconv" 7 | //"time" 8 | ) 9 | 10 | type BufferflowDefault struct { 11 | Name string 12 | Port string 13 | } 14 | 15 | var () 16 | 17 | func (b *BufferflowDefault) Init() { 18 | log.Println("Initting default buffer flow (which means no buffering)") 19 | } 20 | 21 | func (b *BufferflowDefault) BlockUntilReady(cmd string, id string) (bool, bool) { 22 | //log.Printf("BlockUntilReady() start\n") 23 | return true, false 24 | } 25 | 26 | func (b *BufferflowDefault) OnIncomingData(data string) { 27 | //log.Printf("OnIncomingData() start. data:%v\n", data) 28 | } 29 | 30 | // Clean out b.sem so it can truly block 31 | func (b *BufferflowDefault) ClearOutSemaphore() { 32 | } 33 | 34 | func (b *BufferflowDefault) BreakApartCommands(cmd string) []string { 35 | return []string{cmd} 36 | } 37 | 38 | func (b *BufferflowDefault) Pause() { 39 | return 40 | } 41 | 42 | func (b *BufferflowDefault) Unpause() { 43 | return 44 | } 45 | 46 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 47 | return false 48 | } 49 | 50 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 51 | return false 52 | } 53 | 54 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 55 | return false 56 | } 57 | 58 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 59 | return false 60 | } 61 | 62 | func (b *BufferflowDefault) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 63 | return false 64 | } 65 | 66 | func (b *BufferflowDefault) ReleaseLock() { 67 | } 68 | 69 | func (b *BufferflowDefault) IsBufferGloballySendingBackIncomingData() bool { 70 | return false 71 | } 72 | 73 | func (b *BufferflowDefault) Close() { 74 | } 75 | -------------------------------------------------------------------------------- /bufferflow_dummypause.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | type BufferflowDummypause struct { 9 | Name string 10 | Port string 11 | NumLines int 12 | Paused bool 13 | } 14 | 15 | func (b *BufferflowDummypause) Init() { 16 | } 17 | 18 | func (b *BufferflowDummypause) BlockUntilReady(cmd string, id string) (bool, bool) { 19 | log.Printf("BlockUntilReady() start. numLines:%v\n", b.NumLines) 20 | log.Printf("buffer:%v\n", b) 21 | //for b.Paused { 22 | log.Println("We are paused for 3 seconds. Yeilding send.") 23 | time.Sleep(3000 * time.Millisecond) 24 | //} 25 | log.Printf("BlockUntilReady() end\n") 26 | return true, false 27 | } 28 | 29 | func (b *BufferflowDummypause) OnIncomingData(data string) { 30 | log.Printf("OnIncomingData() start. data:%v\n", data) 31 | b.NumLines++ 32 | //time.Sleep(3000 * time.Millisecond) 33 | log.Printf("OnIncomingData() end. numLines:%v\n", b.NumLines) 34 | } 35 | 36 | // Clean out b.sem so it can truly block 37 | func (b *BufferflowDummypause) ClearOutSemaphore() { 38 | } 39 | 40 | func (b *BufferflowDummypause) BreakApartCommands(cmd string) []string { 41 | return []string{cmd} 42 | } 43 | 44 | func (b *BufferflowDummypause) Pause() { 45 | return 46 | } 47 | 48 | func (b *BufferflowDummypause) Unpause() { 49 | return 50 | } 51 | 52 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 53 | return false 54 | } 55 | 56 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 57 | return false 58 | } 59 | 60 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 61 | return false 62 | } 63 | 64 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 65 | return false 66 | } 67 | 68 | func (b *BufferflowDummypause) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 69 | /* 70 | // remove comments 71 | cmd = b.reComment.ReplaceAllString(cmd, "") 72 | cmd = b.reComment2.ReplaceAllString(cmd, "") 73 | if match := b.reNoResponse.MatchString(cmd); match { 74 | log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) 75 | return true 76 | } 77 | */ 78 | return false 79 | } 80 | 81 | func (b *BufferflowDummypause) ReleaseLock() { 82 | } 83 | 84 | func (b *BufferflowDummypause) IsBufferGloballySendingBackIncomingData() bool { 85 | return false 86 | } 87 | 88 | func (b *BufferflowDummypause) Close() { 89 | } 90 | -------------------------------------------------------------------------------- /bufferflow_grbl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "sync" 11 | ) 12 | 13 | type BufferflowGrbl struct { 14 | Name string 15 | Port string 16 | Paused bool 17 | BufferMax int 18 | q *Queue 19 | 20 | // use thread locking for b.Paused 21 | lock *sync.Mutex 22 | 23 | sem chan int 24 | LatestData string 25 | LastStatus string 26 | version string 27 | quit chan int 28 | parent_serport *serport 29 | 30 | reNewLine *regexp.Regexp 31 | ok *regexp.Regexp 32 | err *regexp.Regexp 33 | initline *regexp.Regexp 34 | qry *regexp.Regexp 35 | rpt *regexp.Regexp 36 | } 37 | 38 | func (b *BufferflowGrbl) Init() { 39 | b.lock = &sync.Mutex{} 40 | b.SetPaused(false, 1) 41 | 42 | log.Println("Initting GRBL buffer flow") 43 | b.BufferMax = 127 //max buffer size 127 bytes available 44 | 45 | b.q = NewQueue() 46 | 47 | //create channels 48 | b.sem = make(chan int) 49 | 50 | //define regex 51 | b.reNewLine, _ = regexp.Compile("\\r{0,1}\\n{1,2}") //\\r{0,1} 52 | b.ok, _ = regexp.Compile("^ok") 53 | b.err, _ = regexp.Compile("^error") 54 | b.initline, _ = regexp.Compile("^Grbl") 55 | b.qry, _ = regexp.Compile("\\?") 56 | b.rpt, _ = regexp.Compile("^<") 57 | 58 | //initialize query loop 59 | b.rptQueryLoop(b.parent_serport) 60 | } 61 | 62 | func (b *BufferflowGrbl) BlockUntilReady(cmd string, id string) (bool, bool) { 63 | log.Printf("BlockUntilReady() start\n") 64 | 65 | b.q.Push(cmd, id) 66 | 67 | log.Printf("New line length: %v, buffer size increased to:%v\n", len(cmd), b.q.LenOfCmds()) 68 | log.Println(b.q) 69 | 70 | if b.q.LenOfCmds() >= b.BufferMax { 71 | b.SetPaused(true, 0) 72 | log.Printf("Buffer Full - Will send this command when space is available") 73 | } 74 | 75 | if b.GetPaused() { 76 | log.Println("It appears we are being asked to pause, so we will wait on b.sem") 77 | // We are being asked to pause our sending of commands 78 | 79 | // clear all b.sem signals so when we block below, we truly block 80 | b.ClearOutSemaphore() 81 | 82 | log.Println("Blocking on b.sem until told from OnIncomingData to go") 83 | unblockType, ok := <-b.sem // will block until told from OnIncomingData to go 84 | 85 | log.Printf("Done blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) 86 | 87 | // we get an unblockType of 1 for normal unblocks 88 | // we get an unblockType of 2 when we're being asked to wipe the buffer, i.e. from a % cmd 89 | if unblockType == 2 { 90 | log.Println("This was an unblock of type 2, which means we're being asked to wipe internal buffer. so return false.") 91 | // returning false asks the calling method to wipe the serial send once 92 | // this function returns 93 | return false, false 94 | } 95 | 96 | log.Printf("BlockUntilReady(cmd:%v, id:%v) end\n", cmd, id) 97 | } 98 | return true, true 99 | } 100 | 101 | func (b *BufferflowGrbl) OnIncomingData(data string) { 102 | log.Printf("OnIncomingData() start. data:%q\n", data) 103 | 104 | b.LatestData += data 105 | 106 | //it was found ok was only received with status responses until the grbl buffer is full. 107 | //b.LatestData = regexp.MustCompile(">\\r\\nok").ReplaceAllString(b.LatestData, ">") //remove oks from status responses 108 | 109 | arrLines := b.reNewLine.Split(b.LatestData, -1) 110 | log.Printf("arrLines:%v\n", arrLines) 111 | 112 | if len(arrLines) > 1 { 113 | // that means we found a newline and have 2 or greater array values 114 | // so we need to analyze our arrLines[] lines but keep last line 115 | // for next trip into OnIncomingData 116 | log.Printf("We have data lines to analyze. numLines:%v\n", len(arrLines)) 117 | 118 | } else { 119 | // we don't have a newline yet, so just exit and move on 120 | // we don't have to reset b.LatestData because we ended up 121 | // without any newlines so maybe we will next time into this method 122 | log.Printf("Did not find newline yet, so nothing to analyze\n") 123 | return 124 | } 125 | 126 | // if we made it here we have lines to analyze 127 | // so analyze all of them except the last line 128 | for index, element := range arrLines[:len(arrLines)-1] { 129 | log.Printf("Working on element:%v, index:%v", element, index) 130 | 131 | //check for 'ok' or 'error' response indicating a gcode line has been processed 132 | if b.ok.MatchString(element) || b.err.MatchString(element){ 133 | if b.q.Len() > 0 { 134 | doneCmd, id := b.q.Poll() 135 | 136 | if b.ok.MatchString(element){ 137 | // Send cmd:"Complete" back 138 | m := DataCmdComplete{"Complete", id, b.Port, b.q.LenOfCmds(), doneCmd} 139 | bm, err := json.Marshal(m) 140 | if err == nil { 141 | h.broadcastSys <- bm 142 | } 143 | } else if b.err.MatchString(element){ 144 | // Send cmd:"Error" back 145 | log.Printf("Error Response Received:%v, id:%v", doneCmd, id) 146 | m := DataCmdComplete{"Error", id, b.Port, b.q.LenOfCmds(), doneCmd} 147 | bm, err := json.Marshal(m) 148 | if err == nil { 149 | h.broadcastSys <- bm 150 | } 151 | } 152 | 153 | log.Printf("Buffer decreased to itemCnt:%v, lenOfBuf:%v\n", b.q.Len(), b.q.LenOfCmds()) 154 | } else { 155 | log.Printf("We should NEVER get here cuz we should have a command in the queue to dequeue when we get the r:{} response. If you see this debug stmt this is BAD!!!!") 156 | } 157 | 158 | if b.q.LenOfCmds() < b.BufferMax { 159 | 160 | log.Printf("Grbl just completed a line of gcode\n") 161 | 162 | // if we are paused, tell us to unpause cuz we have clean buffer room now 163 | if b.GetPaused() { b.SetPaused(false, 1) } 164 | } 165 | 166 | //check for the grbl init line indicating the arduino is ready to accept commands 167 | //could also pull version from this string, if we find a need for that later 168 | } else if b.initline.MatchString(element) { 169 | //grbl init line received, clear anything from current buffer and unpause 170 | b.LocalBufferWipe(b.parent_serport) 171 | 172 | //unpause buffer but wipe the command in the queue as grbl has restarted. 173 | if b.GetPaused() { b.SetPaused(false, 2) } 174 | 175 | b.version = element //save element in version 176 | 177 | //Check for report output, compare to last report output, if different return to client to update status; otherwise ignore status. 178 | } else if b.rpt.MatchString(element){ 179 | if(element == b.LastStatus){ 180 | log.Println("Grbl status has not changed, not reporting to client") 181 | continue //skip this element as the cnc position has not changed, and move on to the next element. 182 | } 183 | 184 | b.LastStatus = element //if we make it here something has changed with the status string and laststatus needs updating 185 | } 186 | 187 | // handle communication back to client 188 | m := DataPerLine{b.Port, element + "\n"} 189 | bm, err := json.Marshal(m) 190 | if err == nil { 191 | h.broadcastSys <- bm 192 | } 193 | 194 | } // for loop 195 | 196 | // now wipe the LatestData to only have the last line that we did not analyze 197 | // because we didn't know/think that was a full command yet 198 | b.LatestData = arrLines[len(arrLines)-1] 199 | 200 | //time.Sleep(3000 * time.Millisecond) 201 | log.Printf("OnIncomingData() end.\n") 202 | } 203 | 204 | // Clean out b.sem so it can truly block 205 | func (b *BufferflowGrbl) ClearOutSemaphore() { 206 | ctr := 0 207 | 208 | keepLooping := true 209 | for keepLooping { 210 | select { 211 | case d, ok := <-b.sem: 212 | log.Printf("Consuming b.sem queue to clear it before we block. ok:%v, d:%v\n", ok, string(d)) 213 | ctr++ 214 | if ok == false { 215 | keepLooping = false 216 | } 217 | default: 218 | keepLooping = false 219 | log.Println("Hit default in select clause") 220 | } 221 | } 222 | log.Printf("Done consuming b.sem queue so we're good to block on it now. ctr:%v\n", ctr) 223 | // ok, all b.sem signals are now consumed into la-la land 224 | } 225 | 226 | func (b *BufferflowGrbl) BreakApartCommands(cmd string) []string { 227 | 228 | // add newline after !~% 229 | log.Printf("Command Before Break-Apart: %q\n", cmd) 230 | 231 | cmds := strings.Split(cmd, "\n") 232 | finalCmds := []string{} 233 | for _, item := range cmds { 234 | //remove comments and whitespace from item 235 | item = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(item, "") 236 | item = regexp.MustCompile(";.*").ReplaceAllString(item, "") 237 | item = strings.Replace(item," ","",-1) 238 | 239 | if item == "*init*"{ //return init string to update grbl widget when already connected to grbl 240 | m := DataPerLine{b.Port, b.version + "\n"} 241 | bm, err := json.Marshal(m) 242 | if err == nil { 243 | h.broadcastSys <- bm 244 | } 245 | } else if item == "*status*"{ //return status when client first connects to existing open port 246 | m := DataPerLine{b.Port, b.LastStatus + "\n"} 247 | bm, err := json.Marshal(m) 248 | if err == nil { 249 | h.broadcastSys <- bm 250 | } 251 | } else if item == "?"{ 252 | log.Printf("Query added without newline: %q\n", item) 253 | finalCmds = append(finalCmds, item) //append query request without newline character 254 | } else if item == "%" { 255 | log.Printf("Wiping Grbl BufferFlow") 256 | b.LocalBufferWipe(b.parent_serport) 257 | //dont add this command to the list of finalCmds 258 | } else if item != "" { 259 | log.Printf("Re-adding newline to item:%v\n", item) 260 | s := item + "\n" 261 | finalCmds = append(finalCmds, s) 262 | log.Printf("New cmd item:%v\n", s) 263 | } 264 | 265 | } 266 | log.Printf("Final array of cmds after BreakApartCommands(). finalCmds:%v\n", finalCmds) 267 | 268 | return finalCmds 269 | //return []string{cmd} //do not process string 270 | } 271 | 272 | func (b *BufferflowGrbl) Pause() { 273 | b.SetPaused(true, 0) 274 | //b.BypassMode = false // turn off bypassmode in case it's on 275 | log.Println("Paused buffer on next BlockUntilReady() call") 276 | } 277 | 278 | func (b *BufferflowGrbl) Unpause() { 279 | //unpause buffer by setting paused to false and passing a 1 to b.sem 280 | b.SetPaused(false, 1) 281 | log.Println("Unpaused buffer inside BlockUntilReady() call") 282 | } 283 | 284 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 285 | // remove comments 286 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 287 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 288 | if match, _ := regexp.MatchString("[!~\\?]|(\u0018)", cmd); match { 289 | log.Printf("Found cmd that should skip buffer. cmd:%v\n", cmd) 290 | return true 291 | } 292 | return false 293 | } 294 | 295 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 296 | // remove comments 297 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 298 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 299 | if match, _ := regexp.MatchString("[!]", cmd); match { 300 | log.Printf("Found cmd that should pause buffer. cmd:%v\n", cmd) 301 | return true 302 | } 303 | return false 304 | } 305 | 306 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 307 | 308 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 309 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 310 | if match, _ := regexp.MatchString("[~]", cmd); match { 311 | log.Printf("Found cmd that should unpause buffer. cmd:%v\n", cmd) 312 | return true 313 | } 314 | return false 315 | } 316 | 317 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 318 | 319 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 320 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 321 | if match, _ := regexp.MatchString("(\u0018)", cmd); match { 322 | log.Printf("Found cmd that should wipe out and reset buffer. cmd:%v\n", cmd) 323 | 324 | //b.q.Delete() //delete tracking queue, all buffered commands will be wiped. 325 | 326 | //log.Println("Buffer variables cleared for new input.") 327 | return true 328 | } 329 | return false 330 | } 331 | 332 | func (b *BufferflowGrbl) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 333 | /* 334 | // remove comments 335 | cmd = b.reComment.ReplaceAllString(cmd, "") 336 | cmd = b.reComment2.ReplaceAllString(cmd, "") 337 | if match := b.reNoResponse.MatchString(cmd); match { 338 | log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) 339 | return true 340 | } 341 | */ 342 | return false 343 | } 344 | 345 | func (b *BufferflowGrbl) ReleaseLock() { 346 | log.Println("Lock being released in GRBL buffer") 347 | 348 | b.q.Delete() 349 | 350 | log.Println("ReleaseLock(), so we will send signal of 2 to b.sem to unpause the BlockUntilReady() thread") 351 | 352 | //release lock, send signal 2 to b.sem 353 | b.SetPaused(false, 2) 354 | } 355 | 356 | func (b *BufferflowGrbl) IsBufferGloballySendingBackIncomingData() bool { 357 | //telling json server that we are handling client responses 358 | return true 359 | } 360 | 361 | //Use this function to open a connection, write directly to serial port and close connection. 362 | //This is used for sending query requests outside of the normal buffered operations that will pause to wait for room in the grbl buffer 363 | //'?' is asynchronous to the normal buffer load and does not need to be paused when buffer full 364 | func (b *BufferflowGrbl) rptQueryLoop(p *serport){ 365 | b.parent_serport = p //make note of this port for use in clearing the buffer later, on error. 366 | ticker := time.NewTicker(250 * time.Millisecond) 367 | b.quit = make(chan int) 368 | go func() { 369 | for { 370 | select { 371 | case <- ticker.C: 372 | 373 | n2, err := p.portIo.Write([]byte("?")) 374 | 375 | log.Print("Just wrote ", n2, " bytes to serial: ?") 376 | 377 | if err != nil { 378 | errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." 379 | log.Print(errstr) 380 | h.broadcastSys <- []byte(errstr) 381 | ticker.Stop() //stop query loop if we can't write to the port 382 | break 383 | } 384 | case <- b.quit: 385 | ticker.Stop() 386 | return 387 | } 388 | } 389 | }() 390 | } 391 | 392 | func (b *BufferflowGrbl) Close() { 393 | //stop the status query loop when the serial port is closed off. 394 | log.Println("Stopping the status query loop") 395 | b.quit <- 1 396 | } 397 | 398 | // Gets the paused state of this buffer 399 | // go-routine safe. 400 | func (b *BufferflowGrbl) GetPaused() bool { 401 | b.lock.Lock() 402 | defer b.lock.Unlock() 403 | return b.Paused 404 | } 405 | 406 | // Sets the paused state of this buffer 407 | // go-routine safe. 408 | func (b *BufferflowGrbl) SetPaused(isPaused bool, semRelease int) { 409 | b.lock.Lock() 410 | defer b.lock.Unlock() 411 | b.Paused = isPaused 412 | 413 | //if we are unpausing the buffer, we need to send a signal to release the channel 414 | if isPaused == false{ 415 | go func() { 416 | // sending a 2 asks BlockUntilReady() to cancel the send 417 | b.sem <- semRelease 418 | defer func() { 419 | log.Printf("Unpause Semaphore just got consumed by the BlockUntilReady()\n") 420 | }() 421 | }() 422 | } 423 | } 424 | 425 | //local version of buffer wipe loop needed to handle pseudo clear buffer (%) without passing that value on to 426 | func (b *BufferflowGrbl) LocalBufferWipe(p *serport){ 427 | log.Printf("Pseudo command received to wipe grbl buffer but *not* send on to grbl controller.") 428 | 429 | // consume all stuff queued 430 | func() { 431 | ctr := 0 432 | 433 | keepLooping := true 434 | for keepLooping { 435 | select { 436 | case d, ok := <-p.sendBuffered: 437 | log.Printf("Consuming sendBuffered queue. ok:%v, d:%v, id:%v\n", ok, string(d.data), string(d.id)) 438 | ctr++ 439 | 440 | p.itemsInBuffer-- 441 | if ok == false { 442 | keepLooping = false 443 | } 444 | default: 445 | keepLooping = false 446 | log.Println("Hit default in select clause") 447 | } 448 | } 449 | log.Printf("Done consuming sendBuffered cmds. ctr:%v\n", ctr) 450 | }() 451 | 452 | b.ReleaseLock() 453 | 454 | // let user know we wiped queue 455 | log.Printf("itemsInBuffer:%v\n", p.itemsInBuffer) 456 | h.broadcastSys <- []byte("{\"Cmd\":\"WipedQueue\",\"QCnt\":" + strconv.Itoa(p.itemsInBuffer) + ",\"Port\":\"" + p.portConf.Name + "\"}") 457 | } 458 | -------------------------------------------------------------------------------- /compile_webidebridge.sh: -------------------------------------------------------------------------------- 1 | # git submodule init 2 | # git submodule update 3 | 4 | #dependencies 5 | #go install github.com/sanbornm/go-selfupdate 6 | 7 | VERSION=xxx 8 | APP_NAME=Arduino_Create_Bridge 9 | 10 | # OUTPUT-COLORING 11 | red='\e[0;31m' 12 | green='\e[0;32m' 13 | NC='\e[0m' # No Color 14 | 15 | extractVersionFromMain() 16 | { 17 | VERSION=`grep versionFloat main.go | cut -d "(" -f2 | cut -d ")" -f1` 18 | } 19 | 20 | createZipEmbeddableFileArduino() 21 | { 22 | echo 'In createZipEmbeddableFileArduino' 23 | GOOS=$1 24 | GOARCH=$2 25 | 26 | # start clean 27 | rm arduino/arduino.zip 28 | rm -r arduino/arduino 29 | mkdir arduino/arduino 30 | cp -r arduino/hardware arduino/tools\_$GOOS\_$GOARCH arduino/boards.json arduino/arduino 31 | cp config.ini arduino 32 | cp fakecerts/*.pem arduino/ 33 | mv arduino/arduino/tools* arduino/arduino/tools 34 | cd arduino 35 | zip -r arduino.zip arduino/* config.ini *.pem > /dev/null 36 | cd .. 37 | cat arduino/arduino.zip >> $3 38 | zip --adjust-sfx $3 39 | mkdir -p snapshot/$GOOS\_$GOARCH 40 | cp $3 snapshot/$GOOS\_$GOARCH/$3 41 | ls -la snapshot/$GOOS\_$GOARCH/$3 42 | } 43 | 44 | bootstrapPlatforms() 45 | { 46 | echo 'In bootstrapPlatforms' 47 | #export PATH=$PATH:/opt/osxcross/target/bin 48 | cd $GOROOT/src 49 | env CC_FOR_TARGET=o64-clang CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./make.bash --no-clean 50 | env CC_FOR_TARGET=gcc CGO_ENABLED=1 GOOS=linux GOARCH=amd64 ./make.bash --no-clean 51 | env CC_FOR_TARGET=gcc CGO_ENABLED=1 GOOS=linux GOARCH=386 ./make.bash --no-clean 52 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm ./make.bash --no-clean 53 | env CC_FOR_TARGET=i686-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=386 ./make.bash --no-clean 54 | } 55 | 56 | set -x 57 | compilePlatform() 58 | { 59 | echo 'In compilePlatform' 60 | GOOS=$1 61 | GOARCH=$2 62 | CC=$3 63 | CGO_ENABLED=$4 64 | NAME=$APP_NAME 65 | if [ $GOOS == "windows" ] 66 | then 67 | NAME=$NAME".exe" 68 | fi 69 | env GOOS=$GOOS GOARCH=$GOARCH CC=$CC CXX=$CC CGO_ENABLED=$CGO_ENABLED go build -o=$NAME 70 | if [ $? != 0 ] 71 | then 72 | echo -e "${red}Target $GOOS, $GOARCH failed${NC}" 73 | exit 1 74 | fi 75 | echo createZipEmbeddableFileArduino $GOOS $GOARCH $NAME 76 | createZipEmbeddableFileArduino $GOOS $GOARCH $NAME 77 | GOOS=$GOOS GOARCH=$GOARCH go-selfupdate $NAME $VERSION 78 | rm -rf $NAME* 79 | } 80 | 81 | extractVersionFromMain 82 | compilePlatform darwin amd64 o64-clang 1 83 | #compilePlatformLinux linux 386 gcc 84 | compilePlatform linux amd64 gcc 1 85 | compilePlatform linux arm 0 86 | compilePlatform windows 386 i686-w64-mingw32-gcc 1 87 | 88 | 89 | exit 0 90 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | addr = :8989 # http service address 2 | bufflowdebug = off # off = (default) We do not send back any debug JSON, on = We will send back a JSON response with debug info based on the configuration of the buffer flow that the user picked 3 | configUpdateInterval = 0 # Update interval for re-reading config file set via -config flag. Zero disables config file re-reading. 4 | gc = std # Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage) 5 | hostname = unknown-hostname # Override the hostname we get from the OS 6 | ls = false # launch self 5 seconds later 7 | regex = usb|acm|com # Regular expression to filter serial port list 8 | v = true # show debug logging 9 | appName = Arduino_Create_Bridge 10 | updateUrl = http://localhost/ 11 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // Supports Windows, Linux, Mac, and Raspberry Pi 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "github.com/googollee/go-socket.io" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | type connection struct { 13 | // The websocket connection. 14 | ws socketio.Socket 15 | 16 | // Buffered channel of outbound messages. 17 | send chan []byte 18 | incoming chan []byte 19 | } 20 | 21 | func (c *connection) writer() { 22 | for message := range c.send { 23 | err := c.ws.Emit("message", string(message)) 24 | if err != nil { 25 | break 26 | } 27 | } 28 | } 29 | 30 | // WsServer overrides socket.io server to set the CORS 31 | type WsServer struct { 32 | Server *socketio.Server 33 | } 34 | 35 | func (s *WsServer) ServeHTTP(c *gin.Context) { 36 | s.Server.ServeHTTP(c.Writer, c.Request) 37 | } 38 | 39 | func uploadHandler(c *gin.Context) { 40 | log.Print("Received a upload") 41 | port := c.PostForm("port") 42 | if port == "" { 43 | c.String(http.StatusBadRequest, "port is required") 44 | return 45 | } 46 | board := c.PostForm("board") 47 | if board == "" { 48 | c.String(http.StatusBadRequest, "board is required") 49 | return 50 | } 51 | board_rewrite := c.PostForm("board_rewrite") 52 | sketch, header, err := c.Request.FormFile("sketch_hex") 53 | if err != nil { 54 | c.String(http.StatusBadRequest, err.Error()) 55 | } 56 | 57 | if header != nil { 58 | path, err := saveFileonTempDir(header.Filename, sketch) 59 | if err != nil { 60 | c.String(http.StatusBadRequest, err.Error()) 61 | } 62 | 63 | go spProgramRW(port, board, board_rewrite, path) 64 | } 65 | } 66 | 67 | func wsHandler() *WsServer { 68 | server, err := socketio.NewServer(nil) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | 73 | server.On("connection", func(so socketio.Socket) { 74 | c := &connection{send: make(chan []byte, 256*10), ws: so} 75 | h.register <- c 76 | so.On("command", func(message string) { 77 | h.broadcast <- []byte(message) 78 | }) 79 | so.On("disconnection", func() { 80 | h.unregister <- c 81 | }) 82 | go c.writer() 83 | }) 84 | server.On("error", func(so socketio.Socket, err error) { 85 | log.Println("error:", err) 86 | }) 87 | 88 | wrapper := WsServer{ 89 | Server: server, 90 | } 91 | 92 | return &wrapper 93 | } 94 | -------------------------------------------------------------------------------- /discovery.go: -------------------------------------------------------------------------------- 1 | // 2 | // discovery.go 3 | // 4 | // Created by Martino Facchin 5 | // Copyright (c) 2015 Arduino LLC 6 | // 7 | // Permission is hereby granted, free of charge, to any person 8 | // obtaining a copy of this software and associated documentation 9 | // files (the "Software"), to deal in the Software without 10 | // restriction, including without limitation the rights to use, 11 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the 13 | // Software is furnished to do so, subject to the following 14 | // conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | // OTHER DEALINGS IN THE SOFTWARE. 27 | // 28 | 29 | package main 30 | 31 | import ( 32 | "github.com/oleksandr/bonjour" 33 | "log" 34 | "net" 35 | "strings" 36 | "time" 37 | ) 38 | 39 | const timeoutConst = 2 40 | 41 | // SavedNetworkPorts contains the ports which we know are already connected 42 | var SavedNetworkPorts []OsSerialPort 43 | 44 | // GetNetworkList returns a list of Network Ports 45 | // The research of network ports is articulated in two phases. First we add new ports coming from 46 | // the bonjour module, then we prune the boards who don't respond to a ping 47 | func GetNetworkList() ([]OsSerialPort, error) { 48 | newPorts, err := getPorts() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | SavedNetworkPorts = Filter(SavedNetworkPorts, func(port OsSerialPort) bool { 54 | any := true 55 | for _, p := range newPorts { 56 | if p.Name == port.Name && p.FriendlyName == port.FriendlyName { 57 | any = false 58 | return any 59 | } 60 | } 61 | return any 62 | }) 63 | 64 | SavedNetworkPorts, err = pruneUnreachablePorts(SavedNetworkPorts) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | SavedNetworkPorts = append(SavedNetworkPorts, newPorts...) 70 | 71 | return SavedNetworkPorts, nil 72 | } 73 | 74 | func checkAvailability(ip string) bool { 75 | timeout := time.Duration(1500 * time.Millisecond) 76 | // Check if the port 80 is open 77 | conn, err := net.DialTimeout("tcp", ip+":80", timeout) 78 | if err != nil { 79 | log.Println(err) 80 | // Check if the port 22 is open 81 | conn, err = net.DialTimeout("tcp", ip+":22", timeout) 82 | if err != nil { 83 | log.Println(err) 84 | return false 85 | } 86 | conn.Close() 87 | return true 88 | } 89 | conn.Close() 90 | return true 91 | } 92 | 93 | func pruneUnreachablePorts(ports []OsSerialPort) ([]OsSerialPort, error) { 94 | times := 2 95 | 96 | ports = Filter(ports, func(port OsSerialPort) bool { 97 | any := false 98 | for i := 0; i < times; i++ { 99 | if checkAvailability(port.Name) { 100 | any = true 101 | } 102 | } 103 | return any 104 | }) 105 | 106 | return ports, nil 107 | } 108 | 109 | func getPorts() ([]OsSerialPort, error) { 110 | resolver, err := bonjour.NewResolver(nil) 111 | if err != nil { 112 | log.Println("Failed to initialize resolver:", err.Error()) 113 | return nil, err 114 | } 115 | 116 | results := make(chan *bonjour.ServiceEntry) 117 | 118 | timeout := make(chan bool, 1) 119 | go func(exitCh chan<- bool) { 120 | time.Sleep(timeoutConst * time.Second) 121 | exitCh <- true 122 | close(results) 123 | }(resolver.Exit) 124 | 125 | arrPorts := []OsSerialPort{} 126 | go func(results chan *bonjour.ServiceEntry, exitCh chan<- bool) { 127 | for e := range results { 128 | var boardInfosSlice []string 129 | for _, element := range e.Text { 130 | if strings.Contains(element, "board=yun") { 131 | boardInfosSlice = append(boardInfosSlice, "arduino:avr:yun") 132 | } 133 | } 134 | arrPorts = append(arrPorts, OsSerialPort{Name: e.AddrIPv4.String(), FriendlyName: e.Instance, NetworkPort: true, RelatedNames: boardInfosSlice}) 135 | } 136 | timeout <- true 137 | }(results, resolver.Exit) 138 | 139 | err = resolver.Browse("_arduino._tcp", "", results) 140 | if err != nil { 141 | log.Println("Failed to browse:", err.Error()) 142 | return nil, err 143 | } 144 | // wait for some kind of timeout and return arrPorts 145 | select { 146 | case <-timeout: 147 | return arrPorts, nil 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | // download.go 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | func saveFileonTempDir(filename string, sketch io.Reader) (path string, err error) { 16 | // create tmp dir 17 | tmpdir, err := ioutil.TempDir("", "serial-port-json-server") 18 | if err != nil { 19 | return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?") 20 | } 21 | 22 | filename, _ = filepath.Abs(tmpdir + "/" + filename) 23 | 24 | output, err := os.Create(filename) 25 | if err != nil { 26 | log.Println("Error while creating", filename, "-", err) 27 | return filename, err 28 | } 29 | defer output.Close() 30 | 31 | n, err := io.Copy(output, sketch) 32 | if err != nil { 33 | log.Println("Error while copying", err) 34 | return filename, err 35 | } 36 | 37 | log.Println(n, "bytes saved") 38 | 39 | return filename, nil 40 | 41 | } 42 | 43 | func downloadFromUrl(url string) (filename string, err error) { 44 | 45 | // clean up url 46 | // remove newlines and space at end 47 | url = strings.TrimSpace(url) 48 | 49 | // create tmp dir 50 | tmpdir, err := ioutil.TempDir("", "serial-port-json-server") 51 | if err != nil { 52 | return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?") 53 | } 54 | tokens := strings.Split(url, "/") 55 | filePrefix := tokens[len(tokens)-1] 56 | log.Println("The filePrefix is", filePrefix) 57 | 58 | fileName, _ := filepath.Abs(tmpdir + "/" + filePrefix) 59 | log.Println("Downloading", url, "to", fileName) 60 | 61 | // TODO: check file existence first with io.IsExist 62 | output, err := os.Create(fileName) 63 | if err != nil { 64 | log.Println("Error while creating", fileName, "-", err) 65 | return fileName, err 66 | } 67 | defer output.Close() 68 | 69 | response, err := http.Get(url) 70 | if err != nil { 71 | log.Println("Error while downloading", url, "-", err) 72 | return fileName, err 73 | } 74 | defer response.Body.Close() 75 | 76 | n, err := io.Copy(output, response.Body) 77 | if err != nil { 78 | log.Println("Error while downloading", url, "-", err) 79 | return fileName, err 80 | } 81 | 82 | log.Println(n, "bytes downloaded.") 83 | 84 | return fileName, nil 85 | } 86 | -------------------------------------------------------------------------------- /dummy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type dummy struct { 10 | //myvar mytype string 11 | } 12 | 13 | var d = dummy{ 14 | //myvar: make(mytype string), 15 | } 16 | 17 | func (d *dummy) run() { 18 | for { 19 | //h.broadcast <- message 20 | log.Print("dummy data") 21 | //h.broadcast <- []byte("dummy data") 22 | time.Sleep(8000 * time.Millisecond) 23 | h.broadcast <- []byte("list") 24 | 25 | // open com4 (tinyg) 26 | h.broadcast <- []byte("open com4 115200 tinyg") 27 | time.Sleep(1000 * time.Millisecond) 28 | 29 | // send some commands 30 | //h.broadcast <- []byte("send com4 ?\n") 31 | //time.Sleep(3000 * time.Millisecond) 32 | h.broadcast <- []byte("send com4 {\"qr\":\"\"}\n") 33 | h.broadcast <- []byte("send com4 g21 g90\n") // mm 34 | //h.broadcast <- []byte("send com4 {\"qr\":\"\"}\n") 35 | //h.broadcast <- []byte("send com4 {\"sv\":0}\n") 36 | //time.Sleep(3000 * time.Millisecond) 37 | for i := 0.0; i < 10.0; i = i + 0.001 { 38 | h.broadcast <- []byte("send com4 G1 X" + fmt.Sprintf("%.3f", i) + " F100\n") 39 | time.Sleep(10 * time.Millisecond) 40 | } 41 | /* 42 | h.broadcast <- []byte("send com4 G1 X1\n") 43 | h.broadcast <- []byte("send com4 G1 X2\n") 44 | h.broadcast <- []byte("send com4 G1 X3\n") 45 | h.broadcast <- []byte("send com4 G1 X4\n") 46 | h.broadcast <- []byte("send com4 G1 X5\n") 47 | h.broadcast <- []byte("send com4 G1 X6\n") 48 | h.broadcast <- []byte("send com4 G1 X7\n") 49 | h.broadcast <- []byte("send com4 G1 X8\n") 50 | h.broadcast <- []byte("send com4 G1 X9\n") 51 | h.broadcast <- []byte("send com4 G1 X10\n") 52 | h.broadcast <- []byte("send com4 G1 X1\n") 53 | h.broadcast <- []byte("send com4 G1 X2\n") 54 | h.broadcast <- []byte("send com4 G1 X3\n") 55 | h.broadcast <- []byte("send com4 G1 X4\n") 56 | h.broadcast <- []byte("send com4 G1 X5\n") 57 | h.broadcast <- []byte("send com4 G1 X6\n") 58 | h.broadcast <- []byte("send com4 G1 X7\n") 59 | h.broadcast <- []byte("send com4 G1 X8\n") 60 | h.broadcast <- []byte("send com4 G1 X9\n") 61 | h.broadcast <- []byte("send com4 G1 X10\n") 62 | h.broadcast <- []byte("send com4 G1 X1\n") 63 | h.broadcast <- []byte("send com4 G1 X2\n") 64 | h.broadcast <- []byte("send com4 G1 X3\n") 65 | h.broadcast <- []byte("send com4 G1 X4\n") 66 | h.broadcast <- []byte("send com4 G1 X5\n") 67 | h.broadcast <- []byte("send com4 G1 X6\n") 68 | h.broadcast <- []byte("send com4 G1 X7\n") 69 | h.broadcast <- []byte("send com4 G1 X8\n") 70 | h.broadcast <- []byte("send com4 G1 X9\n") 71 | h.broadcast <- []byte("send com4 G1 X10\n") 72 | */ 73 | break 74 | } 75 | log.Println("dummy process exited") 76 | } 77 | -------------------------------------------------------------------------------- /fakecerts/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBDCCAewCCQD9LvUUyrKRXzANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJV 3 | UzEUMBIGA1UEChMLQXJkdWlubyBMTEMxCzAJBgNVBAsTAklUMRIwEAYDVQQDEwls 4 | b2NhbGhvc3QwHhcNMTUwNjExMDcyNTM2WhcNMTYwNjEwMDcyNTM2WjBEMQswCQYD 5 | VQQGEwJVUzEUMBIGA1UEChMLQXJkdWlubyBMTEMxCzAJBgNVBAsTAklUMRIwEAYD 6 | VQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7 7 | 3UTunwOvcw1hgFDN6bdHxssg/H33faLILjUJIBZwyRCVLc8zcExk7D0YLxnMBaxs 8 | t4DoPfMlAXiJ+GT3fiOaYTKHmGT3Qz85WC6Yngz9A/usDQSSYe9rI4k3oLNA4F+b 9 | 6VU5FBj1V3nj79gs3IMEkTMnit+9HZ8PNlJEZSzEuxbrQdIV530H8MvHpUoCkinP 10 | uPur9zGFysIS1euoVybNKmVXcxmy0QHDNfOddH2SBaFD6QriucOIMW6AVHjseL9E 11 | A4IKKum88LLDiKPeu3gr12vNnjydLFGnNg+wlTnXUeeuXgMfNVaTDaIC8NEA+mSQ 12 | M6Ag6CymhWqpe/cqa45hAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBABCXfQZLhYI4 13 | B0VjgwDDRohTAhsSyaQvFEhGOx2Oo28RhKgY2xRtfjDlm7IzTLga4ApsHqIMf85F 14 | z4bpDDciwPhQDJ6a0ueXJv+xb6RGBnGAbnOiHPfgdd/P2vGfsR7mRaTdXJC4r9PI 15 | C3kzjwTlplaulLbHs5XIXImGvAvRX5sPmkqlBAQs8PVG4I71pKXo1M4kl7uzr96+ 16 | +DKnVz2oACAPCW6zTlT/MlBH4nSeCQfGiE8iWAqODviONOQjFnaTKLw6d1YnbnLp 17 | 1gokB8sk1TAM3qjb6giZpe945xISSNUDAVRW+3NIKag+qOURnN+QGi9ac3cPepXb 18 | 4Kr4QM+kV+g= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /fakecerts/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAu91E7p8Dr3MNYYBQzem3R8bLIPx9932iyC41CSAWcMkQlS3P 3 | M3BMZOw9GC8ZzAWsbLeA6D3zJQF4ifhk934jmmEyh5hk90M/OVgumJ4M/QP7rA0E 4 | kmHvayOJN6CzQOBfm+lVORQY9Vd54+/YLNyDBJEzJ4rfvR2fDzZSRGUsxLsW60HS 5 | Fed9B/DLx6VKApIpz7j7q/cxhcrCEtXrqFcmzSplV3MZstEBwzXznXR9kgWhQ+kK 6 | 4rnDiDFugFR47Hi/RAOCCirpvPCyw4ij3rt4K9drzZ48nSxRpzYPsJU511Hnrl4D 7 | HzVWkw2iAvDRAPpkkDOgIOgspoVqqXv3KmuOYQIDAQABAoIBADzL4df7W/z2ldj8 8 | 5qmMvkSbP/d/3uAuJ3TzBzTZzolXeGdeuNRjvkVW8nqWG5ocJ+3TAQOnsL1EGZdE 9 | 7J/vkWQPmoOpPNuMRrSvJf08AOYM2PCYEeexjAK3MFvxRLF1K1vQikT7jQww8ABl 10 | CSeTgU8EEnL0jW2tXWFV6g+6Ul+jwfq5IvbUpMsMOPuUEQy85rm04bCw/vUnhZXk 11 | gFSpAp5mKPI6J/v2fkJTjgxi0wURxHKFdH/dFr69k9G7Vv9L8meiZYwA0QsYcmeJ 12 | EAGpZHQXpTCmmmzWM85vz9vg4qUBwF8ypXcWjuqfjAopXvuenyIkfa9paDriRnNM 13 | A3JmUQECgYEA66fdY8cU1cJw5jmhc10mi6lDz0Xswba4kaYnb2fwr7tlkBFzYYVn 14 | GY6f2fNxdx8ZpOCYzembJ67qCXODw4LLle1slgHzOWHQ4RJd2EDw/i9woPHGsT3a 15 | TIk0tX3wOjtJJEXNzQEiqcDqGqrpY3pnm4lFGR4RSE6vIYfPwyO9y7ECgYEAzBUv 16 | hKy19GSFK8tkYa/WcmNMCGwLJB0pEMuT3heDjSEKYNaTh8YcIpeEY3aCWbMKxWdu 17 | O5TIouyNJHtm4Q0umG08ZGekLTZy22it2UJabROvHVHeSnPki9a12Uc3KgB6mBzb 18 | nnHXQ8hR60o0GTPMudVW12aZh9gy+EcGWQEwibECgYAIdQ3M9ugYg9HpcUgKC93G 19 | RVzZo3jLuvMExyHDLcfFhsQuV3H8nf0FPcke2U3JKKSS9jyFp0yaL/zWOf8QlTEZ 20 | QFwVRhykgo4shaw4hpwfgzRXklW/Jqke9g2eNdbZQCdv1dF8+f10eifsrRWTLGFr 21 | g5GnRRz5q1k9qtIZ/r5hAQKBgCegMXLMaiQC5IylWF9TWgsp1piICf3AH7ogHRzx 22 | adycPrygzVJ+l/Xrf4wkdZjfF8dOluvArthbn+gmgcpO2e5Ev4YrTYht2w1ZHPBj 23 | XtVxDf5eaBACwqyYSwTePArOvv8ME2SHbCnAGo/Z/5WpJiYrE0qNpF/pDbSBbe0Y 24 | OwlxAoGAdPEjpeeCpyUcoJMVnIxM7AtsOqomd0lBrRgRq05FYvGMAygoKzz+OBp+ 25 | VgptcGJp+6t5MY9R3asRaFp3rOcXvX5R4wBMfijlzoezMEFZN/+xpm7LN2E0domO 26 | xyku5Kcn9G/KTxCduepOtjqoNYkKYAcrdkmRfZ9C9xvuB2lKjk8= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Serial Port Example 4 | 5 | 46 | 83 | 84 | 85 |
86 |
87 | 88 | 89 |
90 | 91 | 92 | -------------------------------------------------------------------------------- /hub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/kardianos/osext" 6 | "log" 7 | //"os" 8 | "os/exec" 9 | //"path" 10 | //"path/filepath" 11 | //"runtime" 12 | //"debug" 13 | "encoding/json" 14 | "runtime" 15 | "runtime/debug" 16 | "strconv" 17 | "strings" 18 | ) 19 | 20 | type hub struct { 21 | // Registered connections. 22 | connections map[*connection]bool 23 | 24 | // Inbound messages from the connections. 25 | broadcast chan []byte 26 | 27 | // Inbound messages from the system 28 | broadcastSys chan []byte 29 | 30 | // Register requests from the connections. 31 | register chan *connection 32 | 33 | // Unregister requests from connections. 34 | unregister chan *connection 35 | } 36 | 37 | var h = hub{ 38 | // buffered. go with 1000 cuz should never surpass that 39 | broadcast: make(chan []byte, 1000), 40 | broadcastSys: make(chan []byte, 1000), 41 | // non-buffered 42 | //broadcast: make(chan []byte), 43 | //broadcastSys: make(chan []byte), 44 | register: make(chan *connection), 45 | unregister: make(chan *connection), 46 | connections: make(map[*connection]bool), 47 | } 48 | 49 | func (h *hub) run() { 50 | for { 51 | select { 52 | case c := <-h.register: 53 | h.connections[c] = true 54 | // send supported commands 55 | c.send <- []byte("{\"Version\" : \"" + version + "\"} ") 56 | c.send <- []byte("{\"Commands\" : [\"list\", \"open [portName] [baud] [bufferAlgorithm (optional)]\", \"send [portName] [cmd]\", \"sendnobuf [portName] [cmd]\", \"close [portName]\", \"bufferalgorithms\", \"baudrates\", \"restart\", \"exit\", \"program [portName] [board:name] [$path/to/filename/without/extension]\", \"programfromurl [portName] [board:name] [urlToHexFile]\"]} ") 57 | c.send <- []byte("{\"Hostname\" : \"" + *hostname + "\"} ") 58 | case c := <-h.unregister: 59 | delete(h.connections, c) 60 | // put close in func cuz it was creating panics and want 61 | // to isolate 62 | func() { 63 | // this method can panic if websocket gets disconnected 64 | // from users browser and we see we need to unregister a couple 65 | // of times, i.e. perhaps from incoming data from serial triggering 66 | // an unregister. (NOT 100% sure why seeing c.send be closed twice here) 67 | defer func() { 68 | if e := recover(); e != nil { 69 | log.Println("Got panic: ", e) 70 | } 71 | }() 72 | close(c.send) 73 | }() 74 | case m := <-h.broadcast: 75 | if len(m) > 0 { 76 | //log.Print(string(m)) 77 | //log.Print(h.broadcast) 78 | checkCmd(m) 79 | //log.Print("-----") 80 | 81 | for c := range h.connections { 82 | select { 83 | case c.send <- m: 84 | //log.Print("did broadcast to ") 85 | //log.Print(c.ws.RemoteAddr()) 86 | //c.send <- []byte("hello world") 87 | default: 88 | delete(h.connections, c) 89 | close(c.send) 90 | } 91 | } 92 | } 93 | case m := <-h.broadcastSys: 94 | for c := range h.connections { 95 | select { 96 | case c.send <- m: 97 | //log.Print("did broadcast to ") 98 | //log.Print(c.ws.RemoteAddr()) 99 | //c.send <- []byte("hello world") 100 | default: 101 | delete(h.connections, c) 102 | close(c.send) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | func checkCmd(m []byte) { 110 | //log.Print("Inside checkCmd") 111 | s := string(m[:]) 112 | 113 | sl := strings.ToLower(strings.Trim(s, "\n")) 114 | 115 | if strings.HasPrefix(sl, "open") { 116 | 117 | // check if user wants to open this port as a secondary port 118 | // this doesn't mean much other than allowing the UI to show 119 | // a port as primary and make other ports sort of act less important 120 | isSecondary := false 121 | if strings.HasPrefix(s, "open secondary") { 122 | isSecondary = true 123 | // swap out the word secondary 124 | s = strings.Replace(s, "open secondary", "open", 1) 125 | } 126 | 127 | args := strings.Split(s, " ") 128 | if len(args) < 3 { 129 | go spErr("You did not specify a port and baud rate in your open cmd") 130 | return 131 | } 132 | if len(args[1]) < 1 { 133 | go spErr("You did not specify a serial port") 134 | return 135 | } 136 | 137 | baudStr := strings.Replace(args[2], "\n", "", -1) 138 | baud, err := strconv.Atoi(baudStr) 139 | if err != nil { 140 | go spErr("Problem converting baud rate " + args[2]) 141 | return 142 | } 143 | // pass in buffer type now as string. if user does not 144 | // ask for a buffer type pass in empty string 145 | bufferAlgorithm := "" 146 | if len(args) > 3 { 147 | // cool. we got a buffer type request 148 | buftype := strings.Replace(args[3], "\n", "", -1) 149 | bufferAlgorithm = buftype 150 | } 151 | go spHandlerOpen(args[1], baud, bufferAlgorithm, isSecondary) 152 | 153 | } else if strings.HasPrefix(sl, "close") { 154 | 155 | args := strings.Split(s, " ") 156 | if len(args) > 1 { 157 | go spClose(args[1]) 158 | } else { 159 | go spErr("You did not specify a port to close") 160 | } 161 | 162 | } else if strings.HasPrefix(sl, "programfromurl") { 163 | 164 | args := strings.Split(s, " ") 165 | if len(args) == 4 { 166 | go spProgramFromUrl(args[1], args[2], args[3]) 167 | } else { 168 | go spErr("You did not specify a port, a board to program and/or a URL") 169 | } 170 | 171 | } else if strings.HasPrefix(sl, "program") { 172 | 173 | args := strings.Split(s, " ") 174 | if len(args) > 3 { 175 | var slice []string = args[3:len(args)] 176 | go spProgram(args[1], args[2], strings.Join(slice, " ")) 177 | } else { 178 | go spErr("You did not specify a port, a board to program and/or a filename") 179 | } 180 | 181 | } else if strings.HasPrefix(sl, "sendjson") { 182 | // will catch sendjson 183 | 184 | go spWriteJson(s) 185 | 186 | } else if strings.HasPrefix(sl, "send") { 187 | // will catch send and sendnobuf 188 | 189 | //args := strings.Split(s, "send ") 190 | go spWrite(s) 191 | 192 | } else if strings.HasPrefix(sl, "list") { 193 | go spList(false) 194 | go spList(true) 195 | // log.Println("Stack info", runtime.NumGoroutine()) 196 | // if runtime.NumGoroutine() > 30 { 197 | // output := make([]byte, 1<<16) 198 | // runtime.Stack(output, true) 199 | // log.Println(output) 200 | // log.Fatal("Crash because something is going wrong") 201 | // } 202 | //go getListViaWmiPnpEntity() 203 | } else if strings.HasPrefix(sl, "bufferalgorithm") { 204 | go spBufferAlgorithms() 205 | } else if strings.HasPrefix(sl, "baudrate") { 206 | go spBaudRates() 207 | } else if strings.HasPrefix(sl, "broadcast") { 208 | go broadcast(s) 209 | } else if strings.HasPrefix(sl, "restart") { 210 | restart() 211 | } else if strings.HasPrefix(sl, "exit") { 212 | exit() 213 | } else if strings.HasPrefix(sl, "memstats") { 214 | memoryStats() 215 | } else if strings.HasPrefix(sl, "gc") { 216 | garbageCollection() 217 | } else if strings.HasPrefix(sl, "bufflowdebug") { 218 | bufflowdebug(sl) 219 | } else if strings.HasPrefix(sl, "hostname") { 220 | getHostname() 221 | } else if strings.HasPrefix(sl, "version") { 222 | getVersion() 223 | } else { 224 | go spErr("Could not understand command.") 225 | } 226 | 227 | //log.Print("Done with checkCmd") 228 | } 229 | 230 | func bufflowdebug(sl string) { 231 | log.Println("bufflowdebug start") 232 | if strings.HasPrefix(sl, "bufflowdebug on") { 233 | *bufFlowDebugType = "on" 234 | } else if strings.HasPrefix(sl, "bufflowdebug off") { 235 | *bufFlowDebugType = "off" 236 | } 237 | h.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *bufFlowDebugType + "\"}") 238 | log.Println("bufflowdebug end") 239 | } 240 | 241 | func memoryStats() { 242 | var memStats runtime.MemStats 243 | runtime.ReadMemStats(&memStats) 244 | json, _ := json.Marshal(memStats) 245 | log.Printf("memStats:%v\n", string(json)) 246 | h.broadcastSys <- json 247 | } 248 | 249 | func getHostname() { 250 | h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}") 251 | } 252 | 253 | func getVersion() { 254 | h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}") 255 | } 256 | 257 | func garbageCollection() { 258 | log.Printf("Starting garbageCollection()\n") 259 | h.broadcastSys <- []byte("{\"gc\":\"starting\"}") 260 | memoryStats() 261 | debug.SetGCPercent(100) 262 | debug.FreeOSMemory() 263 | debug.SetGCPercent(-1) 264 | log.Printf("Done with garbageCollection()\n") 265 | h.broadcastSys <- []byte("{\"gc\":\"done\"}") 266 | memoryStats() 267 | } 268 | 269 | func exit() { 270 | log.Println("Starting new spjs process") 271 | h.broadcastSys <- []byte("{\"Exiting\" : true}") 272 | log.Fatal("Exited current spjs cuz asked to") 273 | 274 | } 275 | 276 | func restart() { 277 | // relaunch ourself and exit 278 | // the relaunch works because we pass a cmdline in 279 | // that has serial-port-json-server only initialize 5 seconds later 280 | // which gives us time to exit and unbind from serial ports and TCP/IP 281 | // sockets like :8989 282 | log.Println("Starting new spjs process") 283 | h.broadcastSys <- []byte("{\"Restarting\" : true}") 284 | 285 | // figure out current path of executable so we know how to restart 286 | // this process 287 | /* 288 | dir, err2 := filepath.Abs(filepath.Dir(os.Args[0])) 289 | if err2 != nil { 290 | //log.Fatal(err2) 291 | fmt.Printf("Error getting executable file path. err: %v\n", err2) 292 | } 293 | fmt.Printf("The path to this exe is: %v\n", dir) 294 | 295 | // alternate approach 296 | _, filename, _, _ := runtime.Caller(1) 297 | f, _ := os.Open(path.Join(path.Dir(filename), "serial-port-json-server")) 298 | fmt.Println(f) 299 | */ 300 | 301 | // using osext 302 | exePath, err3 := osext.Executable() 303 | if err3 != nil { 304 | fmt.Printf("Error getting exe path using osext lib. err: %v\n", err3) 305 | } 306 | fmt.Printf("exePath using osext: %v\n", exePath) 307 | 308 | // figure out garbageCollection flag 309 | //isGcFlag := "false" 310 | 311 | var cmd *exec.Cmd 312 | /*if *isGC { 313 | //isGcFlag = "true" 314 | cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc") 315 | } else { 316 | cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter) 317 | 318 | }*/ 319 | cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc", *gcType) 320 | 321 | //cmd := exec.Command("./serial-port-json-server", "ls") 322 | err := cmd.Start() 323 | if err != nil { 324 | log.Printf("Got err restarting spjs: %v\n", err) 325 | h.broadcastSys <- []byte("{\"Error\" : \"" + fmt.Sprintf("%v", err) + "\"}") 326 | } else { 327 | h.broadcastSys <- []byte("{\"Restarted\" : true}") 328 | } 329 | log.Fatal("Exited current spjs for restart") 330 | //log.Printf("Waiting for command to finish...") 331 | //err = cmd.Wait() 332 | //log.Printf("Command finished with error: %v", err) 333 | } 334 | 335 | type CmdBroadcast struct { 336 | Cmd string 337 | Msg string 338 | } 339 | 340 | func broadcast(arg string) { 341 | // we will get a string of broadcast asdf asdf asdf 342 | log.Println("Inside broadcast arg: " + arg) 343 | arg = strings.TrimPrefix(arg, " ") 344 | //log.Println("arg after trim: " + arg) 345 | args := strings.SplitN(arg, " ", 2) 346 | if len(args) != 2 { 347 | errstr := "Could not parse broadcast command: " + arg 348 | log.Println(errstr) 349 | spErr(errstr) 350 | return 351 | } 352 | broadcastcmd := strings.Trim(args[1], " ") 353 | log.Println("The broadcast cmd is:" + broadcastcmd + "---") 354 | 355 | bcmd := CmdBroadcast{ 356 | Cmd: "Broadcast", 357 | Msg: broadcastcmd, 358 | } 359 | json, _ := json.Marshal(bcmd) 360 | log.Printf("bcmd:%v\n", string(json)) 361 | h.broadcastSys <- json 362 | 363 | } 364 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Version 1.82 2 | // Supports Windows, Linux, Mac, and Raspberry Pi, Beagle Bone Black 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "go/build" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | //"net/http/pprof" 14 | "github.com/kardianos/osext" 15 | //"github.com/sanbornm/go-selfupdate/selfupdate" #included in update.go to change heavily 16 | //"github.com/sanderhahn/gozip" 17 | "github.com/gin-gonic/gin" 18 | "github.com/itsjamie/gin-cors" 19 | "github.com/kardianos/service" 20 | "github.com/vharitonsky/iniflags" 21 | "runtime/debug" 22 | "text/template" 23 | "time" 24 | ) 25 | 26 | var ( 27 | version = "1.83" 28 | versionFloat = float32(1.83) 29 | addr = flag.String("addr", ":8989", "http service address") 30 | addrSSL = flag.String("addrSSL", ":8990", "https service address") 31 | //assets = flag.String("assets", defaultAssetPath(), "path to assets") 32 | verbose = flag.Bool("v", true, "show debug logging") 33 | //verbose = flag.Bool("v", false, "show debug logging") 34 | //homeTempl *template.Template 35 | isLaunchSelf = flag.Bool("ls", false, "launch self 5 seconds later") 36 | 37 | configIni = flag.String("configFile", "config.ini", "config file path") 38 | // regular expression to sort the serial port list 39 | // typically this wouldn't be provided, but if the user wants to clean 40 | // up their list with a regexp so it's cleaner inside their end-user interface 41 | // such as ChiliPeppr, this can make the massive list that Linux gives back 42 | // to you be a bit more manageable 43 | regExpFilter = flag.String("regex", "usb|acm|com", "Regular expression to filter serial port list") 44 | 45 | // allow garbageCollection() 46 | //isGC = flag.Bool("gc", false, "Is garbage collection on? Off by default.") 47 | //isGC = flag.Bool("gc", true, "Is garbage collection on? Off by default.") 48 | gcType = flag.String("gc", "std", "Type of garbage collection. std = Normal garbage collection allowing system to decide (this has been known to cause a stop the world in the middle of a CNC job which can cause lost responses from the CNC controller and thus stalled jobs. use max instead to solve.), off = let memory grow unbounded (you have to send in the gc command manually to garbage collect or you will run out of RAM eventually), max = Force garbage collection on each recv or send on a serial port (this minimizes stop the world events and thus lost serial responses, but increases CPU usage)") 49 | 50 | // whether to do buffer flow debugging 51 | bufFlowDebugType = flag.String("bufflowdebug", "off", "off = (default) We do not send back any debug JSON, on = We will send back a JSON response with debug info based on the configuration of the buffer flow that the user picked") 52 | 53 | // hostname. allow user to override, otherwise we look it up 54 | hostname = flag.String("hostname", "unknown-hostname", "Override the hostname we get from the OS") 55 | 56 | updateUrl = flag.String("updateUrl", "", "") 57 | appName = flag.String("appName", "", "") 58 | ) 59 | 60 | var globalConfigMap map[string]interface{} 61 | 62 | type NullWriter int 63 | 64 | func (NullWriter) Write([]byte) (int, error) { return 0, nil } 65 | 66 | func defaultAssetPath() string { 67 | //p, err := build.Default.Import("gary.burd.info/go-websocket-chat", "", build.FindOnly) 68 | p, err := build.Default.Import("github.com/johnlauer/serial-port-json-server", "", build.FindOnly) 69 | if err != nil { 70 | return "." 71 | } 72 | return p.Dir 73 | } 74 | 75 | func homeHandler(c *gin.Context) { 76 | homeTemplate.Execute(c.Writer, c.Request.Host) 77 | } 78 | 79 | func launchSelfLater() { 80 | log.Println("Going to launch myself 5 seconds later.") 81 | time.Sleep(2 * 1000 * time.Millisecond) 82 | log.Println("Done waiting 5 secs. Now launching...") 83 | } 84 | 85 | var logger service.Logger 86 | 87 | type program struct{} 88 | 89 | func (p *program) Start(s service.Service) error { 90 | // Start should not block. Do the actual work async. 91 | go p.run() 92 | return nil 93 | } 94 | func (p *program) run() { 95 | startDaemon() 96 | } 97 | func (p *program) Stop(s service.Service) error { 98 | // Stop should not block. Return with a few seconds. 99 | <-time.After(time.Second * 13) 100 | return nil 101 | } 102 | 103 | func main() { 104 | svcConfig := &service.Config{ 105 | Name: "ArduinoCreateBridge", 106 | DisplayName: "Arduino Create Bridge", 107 | Description: "A bridge that allows Arduino Create to operate on the boards connected to the computer", 108 | } 109 | 110 | prg := &program{} 111 | s, err := service.New(prg, svcConfig) 112 | if err != nil { 113 | log.Fatal(err) 114 | } 115 | if len(os.Args) > 1 { 116 | err = service.Control(s, os.Args[1]) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | return 121 | } 122 | 123 | logger, err = s.Logger(nil) 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | 128 | err = s.Install() 129 | if err != nil { 130 | logger.Error(err) 131 | } 132 | 133 | err = s.Run() 134 | if err != nil { 135 | logger.Error(err) 136 | } 137 | } 138 | 139 | func startDaemon() { 140 | // setupSysTray() 141 | go func() { 142 | 143 | // autoextract self 144 | src, _ := osext.Executable() 145 | dest := filepath.Dir(src) 146 | 147 | // save the config.ini (if it exists) 148 | if _, err := os.Stat(dest + "/" + *configIni); os.IsNotExist(err) { 149 | fmt.Println("First run, unzipping self") 150 | err := Unzip(src, dest) 151 | fmt.Println("Self extraction, err:", err) 152 | } 153 | 154 | if _, err := os.Stat(dest + "/" + *configIni); os.IsNotExist(err) { 155 | flag.Parse() 156 | fmt.Println("No config.ini at", *configIni) 157 | } else { 158 | flag.Set("config", dest+"/"+*configIni) 159 | iniflags.Parse() 160 | } 161 | 162 | // setup logging 163 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 164 | 165 | // see if we are supposed to wait 5 seconds 166 | if *isLaunchSelf { 167 | launchSelfLater() 168 | } 169 | 170 | var updater = &Updater{ 171 | CurrentVersion: version, 172 | ApiURL: *updateUrl, 173 | BinURL: *updateUrl, 174 | DiffURL: "", 175 | Dir: "update/", 176 | CmdName: *appName, 177 | } 178 | 179 | if updater != nil { 180 | go updater.BackgroundRun() 181 | } 182 | 183 | // data, err := Asset("arduino.zip") 184 | // if err != nil { 185 | // log.Println("arduino tools not found") 186 | // } 187 | 188 | createGlobalConfigMap(&globalConfigMap) 189 | 190 | //getList() 191 | f := flag.Lookup("addr") 192 | log.Println("Version:" + version) 193 | 194 | // hostname 195 | hn, _ := os.Hostname() 196 | if *hostname == "unknown-hostname" { 197 | *hostname = hn 198 | } 199 | log.Println("Hostname:", *hostname) 200 | 201 | // turn off garbage collection 202 | // this is dangerous, as u could overflow memory 203 | //if *isGC { 204 | if *gcType == "std" { 205 | log.Println("Garbage collection is on using Standard mode, meaning we just let Golang determine when to garbage collect.") 206 | } else if *gcType == "max" { 207 | log.Println("Garbage collection is on for MAXIMUM real-time collecting on each send/recv from serial port. Higher CPU, but less stopping of the world to garbage collect since it is being done on a constant basis.") 208 | } else { 209 | log.Println("Garbage collection is off. Memory use will grow unbounded. You WILL RUN OUT OF RAM unless you send in the gc command to manually force garbage collection. Lower CPU, but progressive memory footprint.") 210 | debug.SetGCPercent(-1) 211 | } 212 | 213 | ip := "0.0.0.0" 214 | log.Print("Starting server and websocket on " + ip + "" + f.Value.String()) 215 | //homeTempl = template.Must(template.ParseFiles(filepath.Join(*assets, "home.html"))) 216 | 217 | log.Println("The Serial Port JSON Server is now running.") 218 | log.Println("If you are using ChiliPeppr, you may go back to it and connect to this server.") 219 | 220 | // see if they provided a regex filter 221 | if len(*regExpFilter) > 0 { 222 | log.Printf("You specified a serial port regular expression filter: %v\n", *regExpFilter) 223 | } 224 | 225 | // list serial ports 226 | portList, _ := GetList(false) 227 | /*if errSys != nil { 228 | log.Printf("Got system error trying to retrieve serial port list. Err:%v\n", errSys) 229 | log.Fatal("Exiting") 230 | }*/ 231 | log.Println("Your serial ports:") 232 | if len(portList) == 0 { 233 | log.Println("\tThere are no serial ports to list.") 234 | } 235 | for _, element := range portList { 236 | log.Printf("\t%v\n", element) 237 | 238 | } 239 | 240 | if !*verbose { 241 | log.Println("You can enter verbose mode to see all logging by starting with the -v command line switch.") 242 | log.SetOutput(new(NullWriter)) //route all logging to nullwriter 243 | } 244 | 245 | // launch the hub routine which is the singleton for the websocket server 246 | go h.run() 247 | // launch our serial port routine 248 | go sh.run() 249 | // launch our dummy data routine 250 | //go d.run() 251 | 252 | go discoverLoop() 253 | 254 | r := gin.New() 255 | 256 | socketHandler := wsHandler().ServeHTTP 257 | 258 | r.Use(cors.Middleware(cors.Config{ 259 | Origins: "https://create.arduino.cc, http://create.arduino.cc, https://create-dev.arduino.cc, http://create-dev.arduino.cc, http://webide.arduino.cc:8080", 260 | Methods: "GET, PUT, POST, DELETE", 261 | RequestHeaders: "Origin, Authorization, Content-Type", 262 | ExposedHeaders: "", 263 | MaxAge: 50 * time.Second, 264 | Credentials: true, 265 | ValidateHeaders: false, 266 | })) 267 | 268 | r.GET("/", homeHandler) 269 | r.POST("/upload", uploadHandler) 270 | r.GET("/socket.io/", socketHandler) 271 | r.POST("/socket.io/", socketHandler) 272 | r.Handle("WS", "/socket.io/", socketHandler) 273 | r.Handle("WSS", "/socket.io/", socketHandler) 274 | go func() { 275 | if err := r.RunTLS(*addrSSL, filepath.Join(dest, "cert.pem"), filepath.Join(dest, "key.pem")); err != nil { 276 | fmt.Printf("Error trying to bind to port: %v, so exiting...", err) 277 | log.Fatal("Error ListenAndServe:", err) 278 | } 279 | }() 280 | 281 | if err := r.Run(*addr); err != nil { 282 | fmt.Printf("Error trying to bind to port: %v, so exiting...", err) 283 | log.Fatal("Error ListenAndServe:", err) 284 | } 285 | }() 286 | 287 | } 288 | 289 | var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHtml)) 290 | 291 | // If you navigate to this server's homepage, you'll get this HTML 292 | // so you can directly interact with the serial port server 293 | const homeTemplateHtml = ` 294 | 295 | 296 | Serial Port Example 297 | 298 | 299 | 344 | 381 | 382 | 383 |
384 |
385 | 386 | 387 |
388 | 389 | 390 | ` 391 | -------------------------------------------------------------------------------- /programmer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/facchinm/go-serial" 9 | "github.com/kardianos/osext" 10 | "io" 11 | "log" 12 | "mime/multipart" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | var compiling = false 23 | 24 | // Download the file from URL first, store in tmp folder, then pass to spProgram 25 | func spProgramFromUrl(portname string, boardname string, url string) { 26 | mapB, _ := json.Marshal(map[string]string{"ProgrammerStatus": "DownloadStart", "Url": url}) 27 | h.broadcastSys <- mapB 28 | filename, err := downloadFromUrl(url) 29 | mapB, _ = json.Marshal(map[string]string{"ProgrammerStatus": "DownloadDone", "Filename": filename, "Url": url}) 30 | h.broadcastSys <- mapB 31 | 32 | if err != nil { 33 | spErr(err.Error()) 34 | return 35 | } else { 36 | spProgram(portname, boardname, filename) 37 | } 38 | 39 | // delete file 40 | 41 | } 42 | 43 | func colonToUnderscore(input string) string { 44 | output := strings.Replace(input, ":", "_", -1) 45 | return output 46 | } 47 | 48 | func spProgramNetwork(portname string, boardname string, filePath string) error { 49 | 50 | log.Println("Starting network upload") 51 | log.Println("Board Name: " + boardname) 52 | 53 | // Prepare a form that you will submit to that URL. 54 | _url := "http://" + portname + "/data/upload_sketch_silent" 55 | var b bytes.Buffer 56 | w := multipart.NewWriter(&b) 57 | // Add your image file 58 | filePath = strings.Trim(filePath, "\n") 59 | f, err := os.Open(filePath) 60 | if err != nil { 61 | log.Println("Error opening file" + filePath + " err: " + err.Error()) 62 | return err 63 | } 64 | fw, err := w.CreateFormFile("sketch_hex", filePath) 65 | if err != nil { 66 | log.Println("Error creating form file") 67 | return err 68 | } 69 | if _, err = io.Copy(fw, f); err != nil { 70 | log.Println("Error copying form file") 71 | return err 72 | } 73 | // Add the other fields 74 | if fw, err = w.CreateFormField("board"); err != nil { 75 | log.Println("Error creating form field") 76 | return err 77 | } 78 | if _, err = fw.Write([]byte(colonToUnderscore(boardname))); err != nil { 79 | log.Println("Error writing form field") 80 | return err 81 | } 82 | // Don't forget to close the multipart writer. 83 | // If you don't close it, your request will be missing the terminating boundary. 84 | w.Close() 85 | 86 | // Now that you have a form, you can submit it to your handler. 87 | req, err := http.NewRequest("POST", _url, &b) 88 | if err != nil { 89 | log.Println("Error creating post request") 90 | return err 91 | } 92 | // Don't forget to set the content type, this will contain the boundary. 93 | req.Header.Set("Content-Type", w.FormDataContentType()) 94 | req.SetBasicAuth("root", "arduino") 95 | 96 | //h.broadcastSys <- []byte("Start flashing with command " + cmdString) 97 | log.Printf("Network flashing on " + portname) 98 | mapD := map[string]string{"ProgrammerStatus": "Starting", "Cmd": "POST"} 99 | mapB, _ := json.Marshal(mapD) 100 | h.broadcastSys <- mapB 101 | 102 | // Submit the request 103 | client := &http.Client{} 104 | res, err := client.Do(req) 105 | if err != nil { 106 | log.Println("Error during post request") 107 | return err 108 | } 109 | 110 | // Check the response 111 | if res.StatusCode != http.StatusOK { 112 | err = fmt.Errorf("bad status: %s", res.Status) 113 | } 114 | 115 | if err != nil { 116 | log.Printf("Command finished with error: %v ", err) 117 | mapD := map[string]string{"ProgrammerStatus": "Error " + res.Status, "Msg": "Could not program the board", "Output": "", "Err": "Could not program the board"} 118 | mapB, _ := json.Marshal(mapD) 119 | h.broadcastSys <- mapB 120 | } else { 121 | log.Printf("Finished without error. Good stuff.") 122 | mapD := map[string]string{"ProgrammerStatus": "Done", "Flash": "Ok", "Output": ""} 123 | mapB, _ := json.Marshal(mapD) 124 | h.broadcastSys <- mapB 125 | // analyze stdin 126 | } 127 | return err 128 | } 129 | 130 | func spProgramLocal(portname string, boardname string, filePath string) { 131 | isFound, flasher, mycmd := assembleCompilerCommand(boardname, portname, filePath) 132 | mapD := map[string]string{"ProgrammerStatus": "CommandReady", "IsFound": strconv.FormatBool(isFound), "Flasher": flasher, "Cmd": strings.Join(mycmd, " ")} 133 | mapB, _ := json.Marshal(mapD) 134 | h.broadcastSys <- mapB 135 | 136 | if isFound { 137 | spHandlerProgram(flasher, mycmd) 138 | } else { 139 | spErr("Could not find the board " + boardname + " that you were trying to program.") 140 | mapD := map[string]string{"ProgrammerStatus": "Failed", "IsFound": strconv.FormatBool(isFound), "Flasher": flasher, "Cmd": strings.Join(mycmd, " "), "Err": "Could not find the board " + boardname + " that you were trying to program."} 141 | mapB, _ := json.Marshal(mapD) 142 | h.broadcastSys <- mapB 143 | return 144 | } 145 | } 146 | 147 | func spProgram(portname string, boardname string, filePath string) { 148 | 149 | spProgramRW(portname, boardname, "", filePath) 150 | } 151 | 152 | func spProgramRW(portname string, boardname string, boardname_rewrite string, filePath string) { 153 | compiling = true 154 | 155 | defer func() { 156 | time.Sleep(1500 * time.Millisecond) 157 | compiling = false 158 | }() 159 | 160 | // check if the port is physical or network 161 | var networkPort bool 162 | myport, exist := findPortByNameRerun(portname, false) 163 | if !exist { 164 | // it could be a network port that has not been found at the second lap 165 | networkPort = true 166 | } else { 167 | networkPort = myport.NetworkPort 168 | } 169 | 170 | var err error 171 | 172 | if networkPort { 173 | if boardname_rewrite == "" { 174 | err = spProgramNetwork(portname, boardname_rewrite, filePath) 175 | } else { 176 | err = spProgramNetwork(portname, boardname, filePath) 177 | } 178 | if err != nil { 179 | mapD := map[string]string{"ProgrammerStatus": "Error " + err.Error(), "Msg": "Could not program the board", "Output": "", "Err": err.Error()} 180 | mapB, _ := json.Marshal(mapD) 181 | h.broadcastSys <- mapB 182 | } 183 | } else { 184 | spProgramLocal(portname, boardname, filePath) 185 | } 186 | } 187 | 188 | func spHandlerProgram(flasher string, cmdString []string) { 189 | 190 | var oscmd *exec.Cmd 191 | // if runtime.GOOS == "darwin" { 192 | // sh, _ := exec.LookPath("sh") 193 | // // prepend the flasher to run it via sh 194 | // cmdString = append([]string{flasher}, cmdString...) 195 | // oscmd = exec.Command(sh, cmdString...) 196 | // } else { 197 | oscmd = exec.Command(flasher, cmdString...) 198 | // } 199 | 200 | // Stdout buffer 201 | //var cmdOutput []byte 202 | 203 | //h.broadcastSys <- []byte("Start flashing with command " + cmdString) 204 | log.Printf("Flashing with command:" + strings.Join(cmdString, " ")) 205 | mapD := map[string]string{"ProgrammerStatus": "Starting", "Cmd": strings.Join(cmdString, " ")} 206 | mapB, _ := json.Marshal(mapD) 207 | h.broadcastSys <- mapB 208 | 209 | cmdOutput, err := oscmd.CombinedOutput() 210 | 211 | if err != nil { 212 | log.Printf("Command finished with error: %v "+string(cmdOutput), err) 213 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board", "Output": string(cmdOutput), "Err": string(cmdOutput)} 214 | mapB, _ := json.Marshal(mapD) 215 | h.broadcastSys <- mapB 216 | } else { 217 | log.Printf("Finished without error. Good stuff. stdout: " + string(cmdOutput)) 218 | mapD := map[string]string{"ProgrammerStatus": "Done", "Flash": "Ok", "Output": string(cmdOutput)} 219 | mapB, _ := json.Marshal(mapD) 220 | h.broadcastSys <- mapB 221 | // analyze stdin 222 | 223 | } 224 | } 225 | 226 | func formatCmdline(cmdline string, boardOptions map[string]string) (string, bool) { 227 | 228 | list := strings.Split(cmdline, "{") 229 | if len(list) == 1 { 230 | return cmdline, false 231 | } 232 | cmdline = "" 233 | for _, item := range list { 234 | item_s := strings.Split(item, "}") 235 | item = boardOptions[item_s[0]] 236 | if len(item_s) == 2 { 237 | cmdline += item + item_s[1] 238 | } else { 239 | if item != "" { 240 | cmdline += item 241 | } else { 242 | cmdline += item_s[0] 243 | } 244 | } 245 | } 246 | log.Println(cmdline) 247 | return cmdline, true 248 | } 249 | 250 | func containsStr(s []string, e string) bool { 251 | for _, a := range s { 252 | if a == e { 253 | return true 254 | } 255 | } 256 | return false 257 | } 258 | 259 | func findNewPortName(slice1 []string, slice2 []string) string { 260 | m := map[string]int{} 261 | 262 | for _, s1Val := range slice1 { 263 | m[s1Val] = 1 264 | } 265 | for _, s2Val := range slice2 { 266 | m[s2Val] = m[s2Val] + 1 267 | } 268 | 269 | for mKey, mVal := range m { 270 | if mVal == 1 { 271 | return mKey 272 | } 273 | } 274 | 275 | return "" 276 | } 277 | 278 | func assembleCompilerCommand(boardname string, portname string, filePath string) (bool, string, []string) { 279 | 280 | // get executable (self)path and use it as base for all other paths 281 | execPath, _ := osext.Executable() 282 | 283 | boardFields := strings.Split(boardname, ":") 284 | if len(boardFields) != 3 { 285 | mapD := map[string]string{"Err": "Board need to be specified in core:architecture:name format"} 286 | mapB, _ := json.Marshal(mapD) 287 | h.broadcastSys <- mapB 288 | return false, "", nil 289 | } 290 | tempPath := (filepath.Dir(execPath) + "/" + boardFields[0] + "/hardware/" + boardFields[1] + "/boards.txt") 291 | file, err := os.Open(tempPath) 292 | if err != nil { 293 | mapD := map[string]string{"Err": "Could not find board: " + boardname} 294 | mapB, _ := json.Marshal(mapD) 295 | h.broadcastSys <- mapB 296 | log.Println("Error:", err) 297 | return false, "", nil 298 | } 299 | scanner := bufio.NewScanner(file) 300 | 301 | boardOptions := make(map[string]string) 302 | uploadOptions := make(map[string]string) 303 | 304 | for scanner.Scan() { 305 | // map everything matching with boardname 306 | if strings.Contains(scanner.Text(), boardFields[2]) { 307 | arr := strings.Split(scanner.Text(), "=") 308 | arr[0] = strings.Replace(arr[0], boardFields[2]+".", "", 1) 309 | boardOptions[arr[0]] = arr[1] 310 | } 311 | } 312 | 313 | if len(boardOptions) == 0 { 314 | mapD := map[string]string{"Err": "Board " + boardFields[2] + " is not part of " + boardFields[0] + ":" + boardFields[1]} 315 | mapB, _ := json.Marshal(mapD) 316 | h.broadcastSys <- mapB 317 | return false, "", nil 318 | } 319 | 320 | // filepath need special care; the project_name var is the filename minus its extension (hex or bin) 321 | // if we are going to modify standard IDE files we also could pass ALL filename 322 | filePath = strings.Trim(filePath, "\n") 323 | boardOptions["build.path"] = filepath.Dir(filePath) 324 | boardOptions["build.project_name"] = strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filepath.Base(filePath))) 325 | 326 | file.Close() 327 | 328 | // get infos about the programmer 329 | tempPath = (filepath.Dir(execPath) + "/" + boardFields[0] + "/hardware/" + boardFields[1] + "/platform.txt") 330 | file, err = os.Open(tempPath) 331 | if err != nil { 332 | mapD := map[string]string{"Err": "Could not find board: " + boardname} 333 | mapB, _ := json.Marshal(mapD) 334 | h.broadcastSys <- mapB 335 | log.Println("Error:", err) 336 | return false, "", nil 337 | } 338 | scanner = bufio.NewScanner(file) 339 | 340 | tool := boardOptions["upload.tool"] 341 | 342 | for scanner.Scan() { 343 | // map everything matching with upload 344 | if strings.Contains(scanner.Text(), tool) { 345 | arr := strings.Split(scanner.Text(), "=") 346 | uploadOptions[arr[0]] = arr[1] 347 | arr[0] = strings.Replace(arr[0], "tools."+tool+".", "", 1) 348 | boardOptions[arr[0]] = arr[1] 349 | // we have a "=" in command line 350 | if len(arr) > 2 { 351 | boardOptions[arr[0]] = arr[1] + "=" + arr[2] 352 | } 353 | } 354 | } 355 | file.Close() 356 | 357 | // multiple verisons of the same programmer can be handled if "version" is specified 358 | version := uploadOptions["runtime.tools."+tool+".version"] 359 | path := (filepath.Dir(execPath) + "/" + boardFields[0] + "/tools/" + tool + "/" + version) 360 | if err != nil { 361 | mapD := map[string]string{"Err": "Could not find board: " + boardname} 362 | mapB, _ := json.Marshal(mapD) 363 | h.broadcastSys <- mapB 364 | log.Println("Error:", err) 365 | return false, "", nil 366 | } 367 | 368 | boardOptions["runtime.tools."+tool+".path"] = path 369 | 370 | cmdline := boardOptions["upload.pattern"] 371 | // remove cmd.path as it is handled differently 372 | cmdline = strings.Replace(cmdline, "\"{cmd.path}\"", " ", 1) 373 | cmdline = strings.Replace(cmdline, "\"{path}/{cmd}\"", " ", 1) 374 | cmdline = strings.Replace(cmdline, "\"", "", -1) 375 | 376 | initialPortName := portname 377 | 378 | // some boards (eg. Leonardo, Yun) need a special procedure to enter bootloader 379 | if boardOptions["upload.use_1200bps_touch"] == "true" { 380 | // triggers bootloader mode 381 | // the portname could change in this occasion (expecially on Windows) so change portname 382 | // with the port which will reappear 383 | log.Println("Restarting in bootloader mode") 384 | 385 | mode := &serial.Mode{ 386 | BaudRate: 1200, 387 | Vmin: 1, 388 | Vtimeout: 0, 389 | } 390 | port, err := serial.OpenPort(portname, mode) 391 | if err != nil { 392 | log.Println(err) 393 | return false, "", nil 394 | } 395 | //port.SetDTR(false) 396 | port.Close() 397 | time.Sleep(time.Second / 2.0) 398 | 399 | timeout := false 400 | go func() { 401 | time.Sleep(2 * time.Second) 402 | timeout = true 403 | }() 404 | 405 | // time.Sleep(time.Second / 4) 406 | // wait for port to reappear 407 | if boardOptions["upload.wait_for_upload_port"] == "true" { 408 | after_reset_ports, _ := serial.GetPortsList() 409 | log.Println(after_reset_ports) 410 | var ports []string 411 | for { 412 | ports, _ = serial.GetPortsList() 413 | log.Println(ports) 414 | time.Sleep(time.Millisecond * 200) 415 | portname = findNewPortName(ports, after_reset_ports) 416 | if portname != "" { 417 | break 418 | } 419 | if timeout { 420 | break 421 | } 422 | } 423 | } 424 | } 425 | 426 | if portname == "" { 427 | portname = initialPortName 428 | } 429 | 430 | boardOptions["serial.port"] = portname 431 | boardOptions["serial.port.file"] = filepath.Base(portname) 432 | 433 | // split the commandline in substrings and recursively replace mapped strings 434 | cmdlineSlice := strings.Split(cmdline, " ") 435 | var winded = true 436 | for index, _ := range cmdlineSlice { 437 | winded = true 438 | for winded != false { 439 | cmdlineSlice[index], winded = formatCmdline(cmdlineSlice[index], boardOptions) 440 | } 441 | } 442 | 443 | tool = (filepath.Dir(execPath) + "/" + boardFields[0] + "/tools/" + tool + "/bin/" + tool) 444 | // the file doesn't exist, we are on windows 445 | if _, err := os.Stat(tool); err != nil { 446 | tool = tool + ".exe" 447 | // convert all "/" to "\" 448 | tool = strings.Replace(tool, "/", "\\", -1) 449 | } 450 | 451 | // remove blanks from cmdlineSlice 452 | var cmdlineSliceOut []string 453 | for _, element := range cmdlineSlice { 454 | if element != "" { 455 | cmdlineSliceOut = append(cmdlineSliceOut, element) 456 | } 457 | } 458 | 459 | return (tool != ""), tool, cmdlineSliceOut 460 | } 461 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | // 2 | // queue.go 3 | // 4 | // Created by Hicham Bouabdallah 5 | // Copyright (c) 2012 SimpleRocket LLC 6 | // 7 | // Permission is hereby granted, free of charge, to any person 8 | // obtaining a copy of this software and associated documentation 9 | // files (the "Software"), to deal in the Software without 10 | // restriction, including without limitation the rights to use, 11 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the 13 | // Software is furnished to do so, subject to the following 14 | // conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | // OTHER DEALINGS IN THE SOFTWARE. 27 | // 28 | 29 | package main 30 | 31 | import "sync" 32 | 33 | type queuenode struct { 34 | data string 35 | id string 36 | next *queuenode 37 | } 38 | 39 | // A go-routine safe FIFO (first in first out) data stucture. 40 | type Queue struct { 41 | head *queuenode 42 | tail *queuenode 43 | count int 44 | lock *sync.Mutex 45 | lenOfCmds int 46 | } 47 | 48 | // Creates a new pointer to a new queue. 49 | func NewQueue() *Queue { 50 | q := &Queue{} 51 | q.lock = &sync.Mutex{} 52 | return q 53 | } 54 | 55 | // Returns the number of elements in the queue (i.e. size/length) 56 | // go-routine safe. 57 | func (q *Queue) Len() int { 58 | q.lock.Lock() 59 | defer q.lock.Unlock() 60 | return q.count 61 | } 62 | 63 | // Returns the length of the data (gcode cmd) in the queue (i.e. size/length) 64 | // go-routine safe. 65 | func (q *Queue) LenOfCmds() int { 66 | q.lock.Lock() 67 | defer q.lock.Unlock() 68 | return q.lenOfCmds 69 | } 70 | 71 | // Pushes/inserts a value at the end/tail of the queue. 72 | // Note: this function does mutate the queue. 73 | // go-routine safe. 74 | func (q *Queue) Push(item string, id string) { 75 | q.lock.Lock() 76 | defer q.lock.Unlock() 77 | 78 | n := &queuenode{data: item, id: id} 79 | 80 | if q.tail == nil { 81 | q.tail = n 82 | q.head = n 83 | } else { 84 | q.tail.next = n 85 | q.tail = n 86 | } 87 | q.count++ 88 | q.lenOfCmds += len(item) 89 | } 90 | 91 | // Returns the value at the front of the queue. 92 | // i.e. the oldest value in the queue. 93 | // Note: this function does mutate the queue. 94 | // go-routine safe. 95 | func (q *Queue) Poll() (string, string) { 96 | q.lock.Lock() 97 | defer q.lock.Unlock() 98 | 99 | if q.head == nil { 100 | return "", "" 101 | } 102 | 103 | n := q.head 104 | q.head = n.next 105 | 106 | if q.head == nil { 107 | q.tail = nil 108 | } 109 | q.count-- 110 | q.lenOfCmds -= len(n.data) 111 | 112 | return n.data, n.id 113 | } 114 | 115 | // Returns a read value at the front of the queue. 116 | // i.e. the oldest value in the queue. 117 | // Note: this function does NOT mutate the queue. 118 | // go-routine safe. 119 | func (q *Queue) Peek() (string, string) { 120 | q.lock.Lock() 121 | defer q.lock.Unlock() 122 | 123 | n := q.head 124 | if n == nil || n.data == "" { 125 | return "", "" 126 | } 127 | 128 | return n.data, n.id 129 | } 130 | 131 | // Returns a read value at the front of the queue. 132 | // i.e. the oldest value in the queue. 133 | // Note: this function does NOT mutate the queue. 134 | // go-routine safe. 135 | func (q *Queue) Delete() { 136 | q.lock.Lock() 137 | defer q.lock.Unlock() 138 | 139 | q.head = nil 140 | q.tail = nil 141 | q.count = 0 142 | q.lenOfCmds = 0 143 | } 144 | -------------------------------------------------------------------------------- /serial.go: -------------------------------------------------------------------------------- 1 | // Supports Windows, Linux, Mac, BeagleBone Black, and Raspberry Pi 2 | 3 | package main 4 | 5 | import ( 6 | //"bufio" 7 | "encoding/json" 8 | "fmt" 9 | //"path/filepath" 10 | //"github.com/kballard/go-shellquote" 11 | //"github.com/johnlauer/goserial" 12 | //"github.com/mikepb/go-serial" 13 | //"github.com/facchinm/go-serial" 14 | //"github.com/kardianos/osext" 15 | "log" 16 | //"os" 17 | "regexp" 18 | "runtime/debug" 19 | "strconv" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | type writeRequest struct { 25 | p *serport 26 | d string 27 | buffer bool 28 | id string 29 | } 30 | 31 | type writeRequestJson struct { 32 | p *serport 33 | P string 34 | Data []writeRequestJsonData 35 | } 36 | 37 | type writeRequestJsonData struct { 38 | D string 39 | Id string 40 | Buf string 41 | } 42 | 43 | type qReportJson struct { 44 | Cmd string 45 | QCnt int 46 | P string 47 | Data []qReportJsonData 48 | } 49 | 50 | type qReportJsonData struct { 51 | D string 52 | Id string 53 | Buf string `json:"-"` 54 | Parts int `json:"-"` 55 | } 56 | 57 | type qReport struct { 58 | Cmd string 59 | QCnt int 60 | Type []string `json:"-"` 61 | Ids []string 62 | D []string //`json:"-"` 63 | Port string 64 | } 65 | 66 | type serialhub struct { 67 | // Opened serial ports. 68 | ports map[*serport]bool 69 | 70 | //open chan *io.ReadWriteCloser 71 | //write chan *serport, chan []byte 72 | write chan writeRequest 73 | //read chan []byte 74 | 75 | writeJson chan writeRequestJson 76 | 77 | // Register requests from the connections. 78 | register chan *serport 79 | 80 | // Unregister requests from connections. 81 | unregister chan *serport 82 | 83 | // regexp for json trimming 84 | reJsonTrim *regexp.Regexp 85 | } 86 | 87 | type SpPortList struct { 88 | Ports []SpPortItem 89 | Network bool 90 | } 91 | 92 | type SpPortItem struct { 93 | Name string 94 | Friendly string 95 | SerialNumber string 96 | DeviceClass string 97 | IsOpen bool 98 | IsPrimary bool 99 | RelatedNames []string 100 | Baud int 101 | BufferAlgorithm string 102 | AvailableBufferAlgorithms []string 103 | Ver float32 104 | NetworkPort bool 105 | } 106 | 107 | // SerialPorts contains the ports attached to the machine 108 | var SerialPorts SpPortList 109 | 110 | // NetworkPorts contains the ports on the network 111 | var NetworkPorts SpPortList 112 | 113 | var sh = serialhub{ 114 | //write: make(chan *serport, chan []byte), 115 | write: make(chan writeRequest), 116 | writeJson: make(chan writeRequestJson), 117 | register: make(chan *serport), 118 | unregister: make(chan *serport), 119 | ports: make(map[*serport]bool), 120 | reJsonTrim: regexp.MustCompile("sendjson"), 121 | } 122 | 123 | func (sh *serialhub) run() { 124 | 125 | log.Print("Inside run of serialhub") 126 | //cmdIdCtr := 0 127 | 128 | //s := ser.open() 129 | //ser.s := s 130 | //ser.write(s, []byte("hello serial data")) 131 | for { 132 | select { 133 | case p := <-sh.register: 134 | log.Print("Registering a port: ", p.portConf.Name) 135 | isPrimary := "false" 136 | if p.IsPrimary { 137 | isPrimary = "true" 138 | } 139 | h.broadcastSys <- []byte("{\"Cmd\":\"Open\",\"Desc\":\"Got register/open on port.\",\"Port\":\"" + p.portConf.Name + "\",\"IsPrimary\":" + isPrimary + ",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + ",\"BufferType\":\"" + p.BufferType + "\"}") 140 | //log.Print(p.portConf.Name) 141 | sh.ports[p] = true 142 | case p := <-sh.unregister: 143 | log.Print("Unregistering a port: ", p.portConf.Name) 144 | h.broadcastSys <- []byte("{\"Cmd\":\"Close\",\"Desc\":\"Got unregister/close on port.\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}") 145 | delete(sh.ports, p) 146 | close(p.sendBuffered) 147 | close(p.sendNoBuf) 148 | case wrj := <-sh.writeJson: 149 | // if the user sent in the commands as json 150 | writeJson(wrj) 151 | 152 | case wr := <-sh.write: 153 | // if user sent in the commands as one text mode line 154 | write(wr, "") 155 | } 156 | } 157 | } 158 | 159 | func writeJson(wrj writeRequestJson) { 160 | // we'll parse this json request and then do a write() as if 161 | // the cmd was sent in as text mode 162 | 163 | // create array to hold our qReportJsonData 164 | qReportDataArr := []qReportJsonData{} 165 | 166 | for _, cmdJson := range wrj.Data { 167 | var wr writeRequest 168 | wr.d = cmdJson.D //[]byte(cmdJson.D) 169 | //wr.id = cmdJson.Id 170 | wr.p = wrj.p 171 | if cmdJson.Buf == "Buf" { 172 | wr.buffer = true 173 | } else if cmdJson.Buf == "NoBuf" { 174 | wr.buffer = false 175 | } else { 176 | wr.buffer = true 177 | } 178 | //write(wr, cmdJson.Id, true) 179 | 180 | // we are sending 1 cmd in, but we may get back multiple cmds 181 | // because the BreakApartCommands() can add/modify stuff, so keep 182 | // that in mind 183 | cmds, idArr, bufTypeArr := createCommands(wr, cmdJson.Id) 184 | 185 | for index, _ := range cmds { 186 | // create q report data 187 | cmdId := idArr[index] 188 | // append stuff to the id if this cmd was one line and just got broken up 189 | // the first line will have a normal id like "123" 190 | // the 2nd, 3rd,e tc line will have id like "123-part-2-3", "123-part-3-3" 191 | if index > 0 { 192 | cmdId = fmt.Sprintf("%v-part-%v-%v", cmdId, (index + 1), len(cmds)) 193 | } 194 | qrd := qReportJsonData{D: cmds[index], Id: cmdId, Parts: len(cmds)} 195 | // if user forced the buffer type, just use it 196 | if cmdJson.Buf == "Buf" || cmdJson.Buf == "NoBuf" { 197 | qrd.Buf = cmdJson.Buf 198 | } else { 199 | // else use the buffer type figured out in createCommands() 200 | qrd.Buf = bufTypeArr[index] 201 | } 202 | qReportDataArr = append(qReportDataArr, qrd) 203 | 204 | } 205 | 206 | } 207 | 208 | // do our own report 209 | qr := qReportJson{ 210 | Cmd: "Queued", 211 | Data: qReportDataArr, 212 | QCnt: wrj.p.itemsInBuffer, 213 | P: wrj.p.portConf.Name, 214 | } 215 | json, _ := json.Marshal(qr) 216 | h.broadcastSys <- json 217 | 218 | // now send off the commands to the appropriate channel 219 | for _, qrd := range qReportDataArr { 220 | 221 | if qrd.Buf == "Buf" { 222 | //log.Println("Json sending to wr.p.sendBuffered") 223 | wrj.p.sendBuffered <- Cmd{qrd.D, qrd.Id, false, false} 224 | } else { 225 | log.Println("Json sending to wr.p.sendNoBuf") 226 | wrj.p.sendNoBuf <- Cmd{qrd.D, qrd.Id, true, false} 227 | } 228 | } 229 | 230 | // garbage collect 231 | if *gcType == "max" { 232 | debug.FreeOSMemory() 233 | } 234 | } 235 | 236 | func write(wr writeRequest, id string) { 237 | cmds, idArr, bufTypeArr := createCommands(wr, id) 238 | 239 | qr := qReport{ 240 | Cmd: "Queued", 241 | //Type: bufTypeArr, 242 | Ids: idArr, 243 | D: cmds, 244 | QCnt: wr.p.itemsInBuffer, 245 | Port: wr.p.portConf.Name, 246 | } 247 | json, _ := json.Marshal(qr) 248 | h.broadcastSys <- json 249 | 250 | // now send off the commands to the appropriate channel 251 | for index, cmdToSendToChannel := range cmds { 252 | //cmdIdCtr++ 253 | //cmdId := "fakeid-" + strconv.Itoa(cmdIdCtr) 254 | cmdId := idArr[index] 255 | if bufTypeArr[index] == "Buf" { 256 | log.Println("Send was normal send, so sending to wr.p.sendBuffered") 257 | wr.p.sendBuffered <- Cmd{cmdToSendToChannel, cmdId, false, false} 258 | } else { 259 | log.Println("Send was sendnobuf, so sending to wr.p.sendNoBuf") 260 | wr.p.sendNoBuf <- Cmd{cmdToSendToChannel, cmdId, true, false} 261 | } 262 | } 263 | 264 | // garbage collect 265 | if *gcType == "max" { 266 | debug.FreeOSMemory() 267 | } 268 | 269 | } 270 | 271 | func createCommands(wr writeRequest, id string) ([]string, []string, []string) { 272 | //log.Print("Got a write to a port") 273 | //log.Print("Port: ", string(wr.p.portConf.Name)) 274 | //log.Print(wr.p) 275 | //log.Print("Data is: ") 276 | //log.Println(wr.d) 277 | //log.Print("Data:" + string(wr.d)) 278 | //log.Print("-----") 279 | log.Printf("Got write to id:%v, port:%v, buffer:%v, data:%v", id, wr.p.portConf.Name, wr.buffer, string(wr.d)) 280 | 281 | dataCmd := string(wr.d) 282 | 283 | // break the data into individual commands for queuing 284 | // this is important because: 285 | // 1) we could be sent multiple serial commands at once and the 286 | // serial device may want them sent in smaller chunks to give 287 | // better feedback. For example, if we're sent G0 X0\nG0 Y10\n we 288 | // could happily send that to a CNC controller like a TinyG. However, 289 | // on something like TinyG that would chew up 2 buffer planners. So, 290 | // to better match what will happen, we break those into 2 commands 291 | // so we get a better granularity of getting back qr responses or 292 | // other feedback. 293 | // 2) we need to break apart specific commands potentially that do 294 | // not need newlines. For example, on TinyG we need !~% to be different 295 | // commands because we need to pivot off of what they mean. ! means pause 296 | // the sending. So, we need that command as its own command in order of 297 | // how they were sent to us. 298 | cmds := wr.p.bufferwatcher.BreakApartCommands(dataCmd) 299 | dataArr := []string{} 300 | bufTypeArr := []string{} 301 | idArr := []string{} 302 | for _, cmd := range cmds { 303 | 304 | // push this cmd onto dataArr for reporting 305 | dataArr = append(dataArr, cmd) 306 | idArr = append(idArr, id) 307 | 308 | // do extra check to see if certain command should wipe out 309 | // the entire internal serial port buffer we're holding in wr.p.sendBuffered 310 | wipeBuf := wr.p.bufferwatcher.SeeIfSpecificCommandsShouldWipeBuffer(cmd) 311 | if wipeBuf { 312 | log.Printf("We got a command that is asking us to wipe the sendBuffered buf. cmd:%v\n", cmd) 313 | // just wipe out the current channel and create new 314 | // hopefully garbage collection works here 315 | 316 | // close the channel 317 | //close(wr.p.sendBuffered) 318 | 319 | // consume all stuff queued 320 | func() { 321 | ctr := 0 322 | /* 323 | for data := range wr.p.sendBuffered { 324 | log.Printf("Consuming sendBuffered queue. d:%v\n", string(data)) 325 | ctr++ 326 | }*/ 327 | 328 | keepLooping := true 329 | for keepLooping { 330 | select { 331 | case d, ok := <-wr.p.sendBuffered: 332 | log.Printf("Consuming sendBuffered queue. ok:%v, d:%v, id:%v\n", ok, string(d.data), string(d.id)) 333 | ctr++ 334 | // since we just consumed a buffer item, we need to decrement bufcount 335 | // we are doing this artificially because we artifically threw 336 | // away what was in the bufer 337 | wr.p.itemsInBuffer-- 338 | if ok == false { 339 | keepLooping = false 340 | } 341 | default: 342 | keepLooping = false 343 | log.Println("Hit default in select clause") 344 | } 345 | } 346 | log.Printf("Done consuming sendBuffered cmds. ctr:%v\n", ctr) 347 | }() 348 | 349 | // we still will likely have a sendBuffered that is in the BlockUntilReady() 350 | // that we have to deal with so it doesn't send to the serial port 351 | // when we release it 352 | // send semaphore release if there is one on the BlockUntilReady() 353 | // this method will release the BlockUntilReady() but with an unblock 354 | // of type 2 which means cancel the send 355 | wr.p.bufferwatcher.ReleaseLock() 356 | 357 | // let user know we wiped queue 358 | log.Printf("itemsInBuffer:%v\n", wr.p.itemsInBuffer) 359 | h.broadcastSys <- []byte("{\"Cmd\":\"WipedQueue\",\"QCnt\":" + strconv.Itoa(wr.p.itemsInBuffer) + ",\"Port\":\"" + wr.p.portConf.Name + "\"}") 360 | 361 | } 362 | 363 | // do extra check to see if any specific commands should pause 364 | // the buffer. this means we'll trigger a BlockUntilReady() block 365 | pauseBuf := wr.p.bufferwatcher.SeeIfSpecificCommandsShouldPauseBuffer(cmd) 366 | if pauseBuf { 367 | log.Printf("We need to pause our internal buffer.\n") 368 | wr.p.bufferwatcher.Pause() 369 | } 370 | 371 | // do extra check to see if any specific commands should unpause 372 | // the buffer. this means we'll release the BlockUntilReady() block 373 | unpauseBuf := wr.p.bufferwatcher.SeeIfSpecificCommandsShouldUnpauseBuffer(cmd) 374 | if unpauseBuf { 375 | log.Printf("We need to unpause our internal buffer.\n") 376 | wr.p.bufferwatcher.Unpause() 377 | } 378 | 379 | // do extra check to see if certain commands for this buffer type 380 | // should skip the internal serial port buffering 381 | // for example ! on tinyg and grbl should skip 382 | typeBuf := "" // set in if stmt below for reporting afterwards 383 | 384 | if wr.buffer { 385 | bufferSkip := wr.p.bufferwatcher.SeeIfSpecificCommandsShouldSkipBuffer(cmd) 386 | if bufferSkip { 387 | log.Printf("Forcing this cmd to skip buffer. cmd:%v\n", cmd) 388 | //wr.buffer = false 389 | typeBuf = "NoBuf" 390 | } else { 391 | typeBuf = "Buf" 392 | } 393 | } else { 394 | typeBuf = "NoBuf" 395 | } 396 | 397 | /* 398 | if wr.buffer { 399 | //log.Println("Send was normal send, so sending to wr.p.sendBuffered") 400 | //wr.p.sendBuffered <- []byte(cmd) 401 | typeBuf = "Buf" 402 | } else { 403 | //log.Println("Send was sendnobuf, so sending to wr.p.sendNoBuf") 404 | //wr.p.sendNoBuf <- []byte(cmd) 405 | typeBuf = "NoBuf" 406 | } 407 | */ 408 | // increment queue counter for reporting 409 | wr.p.itemsInBuffer++ 410 | log.Printf("itemsInBuffer:%v\n", wr.p.itemsInBuffer) 411 | 412 | // push the type of this command to bufTypeArr 413 | bufTypeArr = append(bufTypeArr, typeBuf) 414 | 415 | } // for loop on broken apart commands 416 | 417 | return cmds, idArr, bufTypeArr 418 | } 419 | 420 | func writeToChannels(cmds []string, idArr []string, bufTypeArr []string) { 421 | 422 | } 423 | 424 | // spList broadcasts a Json representation of the ports found 425 | func spList(network bool) { 426 | var list SpPortList 427 | if network { 428 | list = NetworkPorts 429 | } else { 430 | list = SerialPorts 431 | } 432 | ls, err := json.MarshalIndent(list, "", "\t") 433 | if err != nil { 434 | log.Println(err) 435 | h.broadcastSys <- []byte("Error creating json on port list " + 436 | err.Error()) 437 | } else { 438 | h.broadcastSys <- ls 439 | } 440 | } 441 | 442 | // discoverLoop periodically update the list of ports found 443 | func discoverLoop() { 444 | SerialPorts.Network = false 445 | SerialPorts.Ports = make([]SpPortItem, 0) 446 | NetworkPorts.Network = true 447 | NetworkPorts.Ports = make([]SpPortItem, 0) 448 | 449 | go func() { 450 | for { 451 | if !compiling { 452 | spListDual(false) 453 | } 454 | time.Sleep(2 * time.Second) 455 | } 456 | }() 457 | go func() { 458 | for { 459 | spListDual(true) 460 | time.Sleep(2 * time.Second) 461 | } 462 | }() 463 | } 464 | 465 | func spListDual(network bool) { 466 | 467 | // call our os specific implementation of getting the serial list 468 | list, _ := GetList(network) 469 | 470 | // do a quick loop to see if any of our open ports 471 | // did not end up in the list port list. this can 472 | // happen on windows in a fallback scenario where an 473 | // open port can't be identified because it is locked, 474 | // so just solve that by manually inserting 475 | for port := range sh.ports { 476 | 477 | isFound := false 478 | for _, item := range list { 479 | if strings.ToLower(port.portConf.Name) == strings.ToLower(item.Name) { 480 | isFound = true 481 | } 482 | } 483 | 484 | if !isFound { 485 | // artificially push to front of port list 486 | log.Println(fmt.Sprintf("Did not find an open port in the serial port list. We are going to artificially push it onto the list. port:%v", port.portConf.Name)) 487 | var ossp OsSerialPort 488 | ossp.Name = port.portConf.Name 489 | ossp.FriendlyName = port.portConf.Name 490 | list = append([]OsSerialPort{ossp}, list...) 491 | } 492 | } 493 | 494 | // we have a full clean list of ports now. iterate thru them 495 | // to append the open/close state, baud rates, etc to make 496 | // a super clean nice list to send back to browser 497 | n := len(list) 498 | spl := SpPortList{make([]SpPortItem, n, n), network} 499 | 500 | ctr := 0 501 | for _, item := range list { 502 | 503 | /* 504 | Name string 505 | Friendly string 506 | IsOpen bool 507 | IsPrimary bool 508 | RelatedNames []string 509 | Baud int 510 | RtsOn bool 511 | DtrOn bool 512 | BufferAlgorithm string 513 | AvailableBufferAlgorithms []string 514 | Ver float32 515 | */ 516 | spl.Ports[ctr] = SpPortItem{ 517 | Name: item.Name, 518 | Friendly: item.FriendlyName, 519 | SerialNumber: item.SerialNumber, 520 | DeviceClass: item.DeviceClass, 521 | IsOpen: false, 522 | IsPrimary: false, 523 | RelatedNames: item.RelatedNames, 524 | Baud: 0, 525 | BufferAlgorithm: "", 526 | AvailableBufferAlgorithms: availableBufferAlgorithms, 527 | Ver: versionFloat, 528 | NetworkPort: item.NetworkPort, 529 | } 530 | 531 | // figure out if port is open 532 | //spl.Ports[ctr].IsOpen = false 533 | myport, isFound := findPortByName(item.Name) 534 | 535 | if isFound { 536 | // we found our port 537 | spl.Ports[ctr].IsOpen = true 538 | spl.Ports[ctr].Baud = myport.portConf.Baud 539 | spl.Ports[ctr].BufferAlgorithm = myport.BufferType 540 | spl.Ports[ctr].IsPrimary = myport.IsPrimary 541 | } 542 | //ls += "{ \"name\" : \"" + item.Name + "\", \"friendly\" : \"" + item.FriendlyName + "\" },\n" 543 | ctr++ 544 | } 545 | 546 | if network { 547 | NetworkPorts = spl 548 | } else { 549 | SerialPorts = spl 550 | } 551 | } 552 | 553 | func spListOld() { 554 | ls := "{\"serialports\" : [\n" 555 | list, _ := getList() 556 | for _, item := range list { 557 | ls += "{ \"name\" : \"" + item.Name + "\", \"friendly\" : \"" + item.FriendlyName + "\" },\n" 558 | } 559 | ls = strings.TrimSuffix(ls, "},\n") 560 | ls += "}\n" 561 | ls += "]}\n" 562 | h.broadcastSys <- []byte(ls) 563 | } 564 | 565 | func spErr(err string) { 566 | log.Println("Sending err back: ", err) 567 | //h.broadcastSys <- []byte(err) 568 | h.broadcastSys <- []byte("{\"Error\" : \"" + err + "\"}") 569 | } 570 | 571 | func spClose(portname string) { 572 | // look up the registered port by name 573 | // then call the close method inside serialport 574 | // that should cause an unregister channel call back 575 | // to myself 576 | 577 | myport, isFound := findPortByName(portname) 578 | 579 | if isFound { 580 | // we found our port 581 | spHandlerClose(myport) 582 | } else { 583 | // we couldn't find the port, so send err 584 | spErr("We could not find the serial port " + portname + " that you were trying to close.") 585 | } 586 | } 587 | 588 | func spWriteJson(arg string) { 589 | 590 | log.Printf("spWriteJson. arg:%v\n", arg) 591 | 592 | // remove sendjson string 593 | arg = sh.reJsonTrim.ReplaceAllString(arg, "") 594 | //log.Printf("string we're going to parse:%v\n", arg) 595 | 596 | // this is a structured command now for sending in serial commands multiple at a time 597 | // with an ID so we can send back the ID when the command is done 598 | var m writeRequestJson 599 | /* 600 | m.P = "COM22" 601 | var data writeRequestJsonData 602 | data.Id = "234" 603 | str := "yeah yeah" 604 | data.D = str //[]byte(str) //[]byte(string("blah blah")) 605 | m.Data = append(m.Data, data) 606 | //m.Data = append(m.Data, data) 607 | bm, err2 := json.Marshal(m) 608 | if err2 == nil { 609 | log.Printf("Test json serialize:%v\n", string(bm)) 610 | } 611 | */ 612 | 613 | err := json.Unmarshal([]byte(arg), &m) 614 | 615 | if err != nil { 616 | log.Printf("Problem decoding json. giving up. json:%v, err:%v\n", arg, err) 617 | spErr(fmt.Sprintf("Problem decoding json. giving up. json:%v, err:%v", arg, err)) 618 | return 619 | } 620 | 621 | // see if we have this port open 622 | portname := m.P 623 | myport, isFound := findPortByName(portname) 624 | 625 | if !isFound { 626 | // we couldn't find the port, so send err 627 | spErr("We could not find the serial port " + portname + " that you were trying to write to.") 628 | return 629 | } 630 | 631 | // we found our port 632 | m.p = myport 633 | 634 | // send it to the writeJson channel 635 | sh.writeJson <- m 636 | } 637 | 638 | func spWrite(arg string) { 639 | // we will get a string of comXX asdf asdf asdf 640 | log.Println("Inside spWrite arg: " + arg) 641 | arg = strings.TrimPrefix(arg, " ") 642 | //log.Println("arg after trim: " + arg) 643 | args := strings.SplitN(arg, " ", 3) 644 | if len(args) != 3 { 645 | errstr := "Could not parse send command: " + arg 646 | log.Println(errstr) 647 | spErr(errstr) 648 | return 649 | } 650 | portname := strings.Trim(args[1], " ") 651 | //log.Println("The port to write to is:" + portname + "---") 652 | //log.Println("The data is:" + args[2] + "---") 653 | 654 | // see if we have this port open 655 | myport, isFound := findPortByName(portname) 656 | 657 | if !isFound { 658 | // we couldn't find the port, so send err 659 | spErr("We could not find the serial port " + portname + " that you were trying to write to.") 660 | return 661 | } 662 | 663 | // we found our port 664 | // create our write request 665 | var wr writeRequest 666 | wr.p = myport 667 | 668 | // see if args[0] is send or sendnobuf 669 | if args[0] != "sendnobuf" { 670 | // we were just given a "send" so buffer it 671 | wr.buffer = true 672 | } else { 673 | log.Println("sendnobuf specified so wr.buffer is false") 674 | wr.buffer = false 675 | } 676 | 677 | // include newline or not in the write? that is the question. 678 | // for now lets skip the newline 679 | //wr.d = []byte(args[2] + "\n") 680 | wr.d = args[2] //[]byte(args[2]) 681 | 682 | // send it to the write channel 683 | sh.write <- wr 684 | 685 | } 686 | 687 | func findPortByName(portname string) (*serport, bool) { 688 | portnamel := strings.ToLower(portname) 689 | for port := range sh.ports { 690 | if strings.ToLower(port.portConf.Name) == portnamel { 691 | // we found our port 692 | //spHandlerClose(port) 693 | return port, true 694 | } 695 | } 696 | return nil, false 697 | } 698 | 699 | func findPortByNameRerun(portname string, network bool) (OsSerialPort, bool) { 700 | portnamel := strings.ToLower(portname) 701 | list, _ := GetList(network) 702 | for _, item := range list { 703 | if strings.ToLower(item.Name) == portnamel { 704 | return item, true 705 | } 706 | } 707 | return OsSerialPort{}, false 708 | } 709 | 710 | func spBufferAlgorithms() { 711 | //arr := []string{"default", "tinyg", "dummypause"} 712 | arr := availableBufferAlgorithms 713 | json := "{\"BufferAlgorithm\" : [" 714 | for _, elem := range arr { 715 | json += "\"" + elem + "\", " 716 | } 717 | json = regexp.MustCompile(", $").ReplaceAllString(json, "]}") 718 | h.broadcastSys <- []byte(json) 719 | } 720 | 721 | func spBaudRates() { 722 | arr := []string{"2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400"} 723 | json := "{\"BaudRate\" : [" 724 | for _, elem := range arr { 725 | json += "" + elem + ", " 726 | } 727 | json = regexp.MustCompile(", $").ReplaceAllString(json, "]}") 728 | h.broadcastSys <- []byte(json) 729 | } 730 | -------------------------------------------------------------------------------- /seriallist.go: -------------------------------------------------------------------------------- 1 | // Supports Windows, Linux, Mac, and Raspberry Pi 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/facchinm/go-serial" 7 | "log" 8 | //"os" 9 | "regexp" 10 | ) 11 | 12 | type OsSerialPort struct { 13 | Name string 14 | FriendlyName string 15 | RelatedNames []string // for some devices there are 2 or more ports, i.e. TinyG v9 has 2 serial ports 16 | SerialNumber string 17 | DeviceClass string 18 | Manufacturer string 19 | Product string 20 | IdProduct string 21 | IdVendor string 22 | NetworkPort bool 23 | } 24 | 25 | func GetList(network bool) ([]OsSerialPort, error) { 26 | 27 | //log.Println("Doing GetList()") 28 | 29 | if network { 30 | netportList, err := GetNetworkList() 31 | return netportList, err 32 | } else { 33 | 34 | // will timeout in 2 seconds 35 | ports, err := serial.GetPortsList() 36 | 37 | arrPorts := []OsSerialPort{} 38 | for _, element := range ports { 39 | arrPorts = append(arrPorts, OsSerialPort{Name: element, FriendlyName: element}) 40 | } 41 | 42 | // see if we should filter the list 43 | if len(*regExpFilter) > 0 { 44 | // yes, user asked for a filter 45 | reFilter := regexp.MustCompile("(?i)" + *regExpFilter) 46 | 47 | newarrPorts := []OsSerialPort{} 48 | for _, element := range arrPorts { 49 | // if matches regex, include 50 | if reFilter.MatchString(element.Name) { 51 | newarrPorts = append(newarrPorts, element) 52 | } else if reFilter.MatchString(element.FriendlyName) { 53 | newarrPorts = append(newarrPorts, element) 54 | } else { 55 | log.Printf("serial port did not match. port: %v\n", element) 56 | } 57 | 58 | } 59 | arrPorts = newarrPorts 60 | } 61 | 62 | arrPorts = removeNonArduinoBoards(arrPorts) 63 | return arrPorts, err 64 | //log.Printf("Done doing GetList(). arrPorts:%v\n", arrPorts) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /seriallist_darwin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"fmt" 5 | //"github.com/tarm/goserial" 6 | "log" 7 | "os" 8 | "strings" 9 | //"encoding/binary" 10 | //"strconv" 11 | //"syscall" 12 | //"fmt" 13 | //"bufio" 14 | "io/ioutil" 15 | "os/exec" 16 | ) 17 | 18 | // execute system_profiler SPUSBDataType | grep "Vendor ID: 0x2341" -A5 -B2 19 | // maybe -B2 is not necessary 20 | // trim whitespace and eol 21 | // map everything with : 22 | // get [map][Location ID] first 5 chars 23 | // search all board.txt files for map[Product ID] 24 | // assign it to name 25 | 26 | func removeNonArduinoBoards(ports []OsSerialPort) []OsSerialPort { 27 | usbcmd := exec.Command("system_profiler", "SPUSBDataType") 28 | grepcmd := exec.Command("grep", "0x2341", "-A5", "-B1") 29 | 30 | cmdOutput, _ := pipe_commands(usbcmd, grepcmd) 31 | 32 | //log.Println(string(cmdOutput)) 33 | cmdOutSlice := strings.Split(string(cmdOutput), "\n") 34 | 35 | var arduino_ports []OsSerialPort 36 | var other_ports []OsSerialPort 37 | 38 | // how many lines is the output? boards attached = lines/8 39 | for i := 0; i < len(cmdOutSlice)/8; i++ { 40 | 41 | cmdOutSliceN := cmdOutSlice[i*8 : (i+1)*8] 42 | 43 | cmdOutMap := make(map[string]string) 44 | 45 | for _, element := range cmdOutSliceN { 46 | if strings.Contains(element, "ID") { 47 | element = strings.TrimSpace(element) 48 | arr := strings.Split(element, ": ") 49 | cmdOutMap[arr[0]] = arr[1] 50 | } 51 | } 52 | 53 | archBoardName, boardName, _ := getBoardName(cmdOutMap["Product ID"]) 54 | 55 | // remove initial 0x and final zeros 56 | ttyHeader := strings.Trim((cmdOutMap["Location ID"]), "0x") 57 | ttyHeader = strings.Split(ttyHeader, " ")[0] 58 | ttyHeader = strings.Trim(ttyHeader, "0") 59 | 60 | for _, port := range ports { 61 | if strings.Contains(port.Name, ttyHeader) { 62 | if !strings.Contains(port.Name, "/cu") { 63 | port.RelatedNames = append(port.RelatedNames, archBoardName) 64 | port.FriendlyName = strings.Trim(boardName, "\n") 65 | arduino_ports = append(arduino_ports, port) 66 | } else { 67 | other_ports = append(other_ports, port) 68 | } 69 | } 70 | } 71 | } 72 | 73 | arduino_ports = append(arduino_ports, other_ports...) 74 | 75 | // additional remove phase 76 | arduino_ports = Filter(arduino_ports, func(port OsSerialPort) bool { 77 | return !strings.Contains(port.Name, "Blue") && !strings.Contains(port.Name, "/cu") 78 | }) 79 | 80 | return arduino_ports 81 | } 82 | 83 | func getList() ([]OsSerialPort, os.SyscallError) { 84 | //return getListViaWmiPnpEntity() 85 | return getListViaTtyList() 86 | } 87 | 88 | func getListViaTtyList() ([]OsSerialPort, os.SyscallError) { 89 | var err os.SyscallError 90 | 91 | log.Println("getting serial list on darwin") 92 | 93 | // make buffer of 100 max serial ports 94 | // return a slice 95 | list := make([]OsSerialPort, 100) 96 | 97 | files, _ := ioutil.ReadDir("/dev/") 98 | ctr := 0 99 | for _, f := range files { 100 | if strings.HasPrefix(f.Name(), "tty.") { 101 | // it is a legitimate serial port 102 | list[ctr].Name = "/dev/" + f.Name() 103 | list[ctr].FriendlyName = f.Name() 104 | log.Println("Added serial port to list: ", list[ctr]) 105 | ctr++ 106 | } 107 | // stop-gap in case going beyond 100 (which should never happen) 108 | // i mean, really, who has more than 100 serial ports? 109 | if ctr > 99 { 110 | ctr = 99 111 | } 112 | //fmt.Println(f.Name()) 113 | //fmt.Println(f.) 114 | } 115 | /* 116 | list := make([]OsSerialPort, 3) 117 | list[0].Name = "tty.serial1" 118 | list[0].FriendlyName = "tty.serial1" 119 | list[1].Name = "tty.serial2" 120 | list[1].FriendlyName = "tty.serial2" 121 | list[2].Name = "tty.Bluetooth-Modem" 122 | list[2].FriendlyName = "tty.Bluetooth-Modem" 123 | */ 124 | 125 | return list[0:ctr], err 126 | } 127 | -------------------------------------------------------------------------------- /seriallist_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"fmt" 5 | //"github.com/tarm/goserial" 6 | //"log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | //"encoding/binary" 11 | //"strconv" 12 | //"syscall" 13 | //"fmt" 14 | //"io" 15 | "bytes" 16 | "io/ioutil" 17 | "log" 18 | "path/filepath" 19 | "regexp" 20 | "sort" 21 | ) 22 | 23 | func removeNonArduinoBoards(ports []OsSerialPort) []OsSerialPort { 24 | usbcmd := exec.Command("lsusb", "-vvv") 25 | grepcmd := exec.Command("grep", "0x2341", "-A1") 26 | grep2cmd := exec.Command("grep", "idProduct") 27 | //awkcmd := exec.Command("awk", "\'{print $2}\'") 28 | //awkcmd := exec.Command("grep", "-E", "-o", "'0x[[:alnum:]]{4}'") 29 | 30 | cmdOutput, _ := pipe_commands(usbcmd, grepcmd, grep2cmd) 31 | 32 | cmdOutSliceT := strings.Split(string(cmdOutput), "\n") 33 | 34 | re := regexp.MustCompile("0x[[:alnum:]]{4}") 35 | 36 | var cmdOutSlice []string 37 | 38 | for _, element := range cmdOutSliceT { 39 | cmdOutSlice = append(cmdOutSlice, re.FindString(element)) 40 | } 41 | 42 | var arduino_ports, other_ports []OsSerialPort 43 | 44 | for _, element := range cmdOutSlice { 45 | 46 | if element == "" { 47 | break 48 | } 49 | 50 | archBoardName, boardName, _ := getBoardName(element) 51 | 52 | for _, port := range ports { 53 | ueventcmd := exec.Command("cat", "/sys/class/tty/"+filepath.Base(port.Name)+"/device/uevent") 54 | grep3cmd := exec.Command("grep", "PRODUCT=") 55 | cutcmd := exec.Command("cut", "-f2", "-d/") 56 | 57 | cmdOutput2, _ := pipe_commands(ueventcmd, grep3cmd, cutcmd) 58 | cmdOutput2S := string(cmdOutput2) 59 | 60 | if strings.Contains(element, strings.Trim(cmdOutput2S, "\n")) && cmdOutput2S != "" { 61 | port.RelatedNames = append(port.RelatedNames, archBoardName) 62 | port.FriendlyName = strings.Trim(boardName, "\n") 63 | arduino_ports = append(arduino_ports, port) 64 | } else { 65 | other_ports = append(other_ports, port) 66 | } 67 | } 68 | } 69 | 70 | arduino_ports = append(arduino_ports, other_ports...) 71 | 72 | return arduino_ports 73 | } 74 | 75 | func getList() ([]OsSerialPort, os.SyscallError) { 76 | 77 | //return getListViaTtyList() 78 | return getAllPortsViaManufacturer() 79 | } 80 | 81 | func getListViaTtyList() ([]OsSerialPort, os.SyscallError) { 82 | var err os.SyscallError 83 | 84 | //log.Println("getting serial list on darwin") 85 | 86 | // make buffer of 1000 max serial ports 87 | // return a slice 88 | list := make([]OsSerialPort, 1000) 89 | 90 | files, _ := ioutil.ReadDir("/dev/") 91 | ctr := 0 92 | for _, f := range files { 93 | if strings.HasPrefix(f.Name(), "tty") { 94 | // it is a legitimate serial port 95 | list[ctr].Name = "/dev/" + f.Name() 96 | list[ctr].FriendlyName = f.Name() 97 | 98 | // see if we can get a better friendly name 99 | //friendly, ferr := getMetaDataForPort(f.Name()) 100 | //if ferr == nil { 101 | // list[ctr].FriendlyName = friendly 102 | //} 103 | 104 | //log.Println("Added serial port to list: ", list[ctr]) 105 | ctr++ 106 | } 107 | // stop-gap in case going beyond 1000 (which should never happen) 108 | // i mean, really, who has more than 1000 serial ports? 109 | if ctr > 999 { 110 | ctr = 999 111 | } 112 | //fmt.Println(f.Name()) 113 | //fmt.Println(f.) 114 | } 115 | /* 116 | list := make([]OsSerialPort, 3) 117 | list[0].Name = "tty.serial1" 118 | list[0].FriendlyName = "tty.serial1" 119 | list[1].Name = "tty.serial2" 120 | list[1].FriendlyName = "tty.serial2" 121 | list[2].Name = "tty.Bluetooth-Modem" 122 | list[2].FriendlyName = "tty.Bluetooth-Modem" 123 | */ 124 | 125 | return list[0:ctr], err 126 | } 127 | 128 | type deviceClass struct { 129 | BaseClass int 130 | Description string 131 | } 132 | 133 | func getDeviceClassList() { 134 | // TODO: take list from http://www.usb.org/developers/defined_class 135 | // and create mapping. 136 | } 137 | 138 | func getAllPortsViaManufacturer() ([]OsSerialPort, os.SyscallError) { 139 | var err os.SyscallError 140 | var list []OsSerialPort 141 | 142 | // LOOK FOR THE WORD MANUFACTURER 143 | // search /sys folder 144 | oscmd := exec.Command("find", "/sys/", "-name", "manufacturer", "-print") //, "2>", "/dev/null") 145 | // Stdout buffer 146 | cmdOutput := &bytes.Buffer{} 147 | // Attach buffer to command 148 | oscmd.Stdout = cmdOutput 149 | 150 | errstart := oscmd.Start() 151 | if errstart != nil { 152 | log.Printf("Got error running find cmd. Maybe they don't have it installed? %v:", errstart) 153 | return nil, err 154 | } 155 | //log.Printf("Waiting for command to finish... %v", oscmd) 156 | 157 | errwait := oscmd.Wait() 158 | 159 | if errwait != nil { 160 | log.Printf("Command finished with error: %v", errwait) 161 | return nil, err 162 | } 163 | 164 | //log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) 165 | 166 | // analyze stdout 167 | // we should be able to split on newline to each file 168 | files := strings.Split(string(cmdOutput.Bytes()), "\n") 169 | /*if len(files) == 0 { 170 | return nil, err 171 | }*/ 172 | 173 | // LOOK FOR THE WORD PRODUCT 174 | oscmd2 := exec.Command("find", "/sys/", "-name", "product", "-print") //, "2>", "/dev/null") 175 | cmdOutput2 := &bytes.Buffer{} 176 | oscmd2.Stdout = cmdOutput2 177 | 178 | oscmd2.Start() 179 | oscmd2.Wait() 180 | 181 | filesFromProduct := strings.Split(string(cmdOutput2.Bytes()), "\n") 182 | 183 | // append both arrays so we have one (then we'll have to de-dupe) 184 | files = append(files, filesFromProduct...) 185 | 186 | // Now get directories from each file 187 | re := regexp.MustCompile("/(manufacturer|product)$") 188 | var mapfile map[string]int 189 | mapfile = make(map[string]int) 190 | for _, element := range files { 191 | // make this directory be a key so it's unique. increment int so we know 192 | // for debug how many times this directory appeared 193 | mapfile[re.ReplaceAllString(element, "")]++ 194 | } 195 | 196 | // sort the directory keys 197 | mapfilekeys := make([]string, len(mapfile)) 198 | i := 0 199 | for key, _ := range mapfile { 200 | mapfilekeys[i] = key 201 | i++ 202 | } 203 | sort.Strings(mapfilekeys) 204 | 205 | //reRemoveManuf, _ := regexp.Compile("/manufacturer$") 206 | reNewLine, _ := regexp.Compile("\n") 207 | 208 | // loop on unique directories 209 | for _, directory := range mapfilekeys { 210 | 211 | if len(directory) == 0 { 212 | continue 213 | } 214 | 215 | // for each manufacturer or product file, we need to read the val from the file 216 | // but more importantly find the tty ports for this directory 217 | 218 | // for example, for the TinyG v9 which creates 2 ports, the cmd: 219 | // find /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/ -name tty[AU]* -print 220 | // will result in: 221 | /* 222 | /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3:1.0/tty/ttyACM0 223 | /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3:1.2/tty/ttyACM1 224 | */ 225 | 226 | // figure out the directory 227 | //directory := reRemoveManuf.ReplaceAllString(element, "") 228 | 229 | // read the device class so we can remove stuff we don't want like hubs 230 | deviceClassBytes, errRead4 := ioutil.ReadFile(directory + "/bDeviceClass") 231 | deviceClass := "" 232 | if errRead4 != nil { 233 | // there must be a permission issue 234 | //log.Printf("Problem reading in serial number text file. Permissions maybe? err:%v", errRead3) 235 | //return nil, err 236 | } 237 | deviceClass = string(deviceClassBytes) 238 | deviceClass = reNewLine.ReplaceAllString(deviceClass, "") 239 | 240 | if deviceClass == "09" || deviceClass == "9" || deviceClass == "09h" { 241 | log.Printf("This is a hub, so skipping. %v", directory) 242 | continue 243 | } 244 | 245 | // read the manufacturer 246 | manufBytes, errRead := ioutil.ReadFile(directory + "/manufacturer") 247 | manuf := "" 248 | if errRead != nil { 249 | // the file could possibly just not exist, which is normal 250 | log.Printf("Problem reading in manufacturer text file. Permissions maybe? err:%v", errRead) 251 | //return nil, err 252 | //continue 253 | } 254 | manuf = string(manufBytes) 255 | manuf = reNewLine.ReplaceAllString(manuf, "") 256 | 257 | // read the product 258 | productBytes, errRead2 := ioutil.ReadFile(directory + "/product") 259 | product := "" 260 | if errRead2 != nil { 261 | // the file could possibly just not exist, which is normal 262 | //log.Printf("Problem reading in product text file. Permissions maybe? err:%v", errRead2) 263 | //return nil, err 264 | } 265 | product = string(productBytes) 266 | product = reNewLine.ReplaceAllString(product, "") 267 | 268 | // read the serial number 269 | serialNumBytes, errRead3 := ioutil.ReadFile(directory + "/serial") 270 | serialNum := "" 271 | if errRead3 != nil { 272 | // the file could possibly just not exist, which is normal 273 | //log.Printf("Problem reading in serial number text file. Permissions maybe? err:%v", errRead3) 274 | //return nil, err 275 | } 276 | serialNum = string(serialNumBytes) 277 | serialNum = reNewLine.ReplaceAllString(serialNum, "") 278 | 279 | // read idvendor 280 | idVendorBytes, _ := ioutil.ReadFile(directory + "/idVendor") 281 | idVendor := "" 282 | idVendor = reNewLine.ReplaceAllString(string(idVendorBytes), "") 283 | 284 | // read idProduct 285 | idProductBytes, _ := ioutil.ReadFile(directory + "/idProduct") 286 | idProduct := "" 287 | idProduct = reNewLine.ReplaceAllString(string(idProductBytes), "") 288 | 289 | log.Printf("%v : %v (%v) DevClass:%v", manuf, product, serialNum, deviceClass) 290 | 291 | // search folder that had manufacturer file in it 292 | log.Printf("\tDirectory searching: %v", directory) 293 | 294 | // -name tty[AU]* -print 295 | oscmd = exec.Command("find", directory, "-name", "tty[AU]*", "-print") 296 | 297 | // Stdout buffer 298 | cmdOutput = &bytes.Buffer{} 299 | // Attach buffer to command 300 | oscmd.Stdout = cmdOutput 301 | 302 | errstart = oscmd.Start() 303 | if errstart != nil { 304 | log.Printf("Got error running find cmd. Maybe they don't have it installed? %v:", errstart) 305 | //return nil, err 306 | continue 307 | } 308 | //log.Printf("Waiting for command to finish... %v", oscmd) 309 | 310 | errwait = oscmd.Wait() 311 | 312 | if errwait != nil { 313 | log.Printf("Command finished with error: %v", errwait) 314 | //return nil, err 315 | continue 316 | } 317 | 318 | //log.Printf("Finished searching manuf directory without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) 319 | //log.Printf(" \n") 320 | 321 | // we should be able to split on newline to each file 322 | filesTty := strings.Split(string(cmdOutput.Bytes()), "\n") 323 | 324 | // generate a unique list of tty ports below 325 | //var ttyPorts []string 326 | var m map[string]int 327 | m = make(map[string]int) 328 | for _, fileTty := range filesTty { 329 | if len(fileTty) == 0 { 330 | continue 331 | } 332 | log.Printf("\t%v", fileTty) 333 | ttyPort := regexp.MustCompile("^.*/").ReplaceAllString(fileTty, "") 334 | ttyPort = reNewLine.ReplaceAllString(ttyPort, "") 335 | m[ttyPort]++ 336 | //ttyPorts = append(ttyPorts, ttyPort) 337 | } 338 | log.Printf("\tlist of ports on this. map:%v\n", m) 339 | log.Printf("\t.") 340 | //sort.Strings(ttyPorts) 341 | 342 | // create order array of ttyPorts so they're in order when 343 | // we send back via json. this makes for more human friendly reading 344 | // cuz anytime you do a hash map you can get out of order 345 | ttyPorts := []string{} 346 | for key, _ := range m { 347 | ttyPorts = append(ttyPorts, key) 348 | } 349 | sort.Strings(ttyPorts) 350 | 351 | // we now have a very nice list of ttyports for this device. many are just 1 port 352 | // however, for some advanced devices there are 2 or more ports associated and 353 | // we have this data correct now, so build out the final OsSerialPort list 354 | for _, key := range ttyPorts { 355 | listitem := OsSerialPort{ 356 | Name: "/dev/" + key, 357 | FriendlyName: manuf, // + " " + product, 358 | SerialNumber: serialNum, 359 | DeviceClass: deviceClass, 360 | Manufacturer: manuf, 361 | Product: product, 362 | IdVendor: idVendor, 363 | IdProduct: idProduct, 364 | } 365 | if len(product) > 0 { 366 | listitem.FriendlyName += " " + product 367 | } 368 | listitem.FriendlyName += " (" + key + ")" 369 | listitem.FriendlyName = friendlyNameCleanup(listitem.FriendlyName) 370 | 371 | // append related tty ports 372 | for _, keyRelated := range ttyPorts { 373 | if key == keyRelated { 374 | continue 375 | } 376 | listitem.RelatedNames = append(listitem.RelatedNames, "/dev/"+keyRelated) 377 | } 378 | list = append(list, listitem) 379 | } 380 | 381 | } 382 | 383 | // sort ports by item.Name 384 | sort.Sort(ByName(list)) 385 | 386 | log.Printf("Final port list: %v", list) 387 | return list, err 388 | } 389 | 390 | // ByAge implements sort.Interface for []Person based on 391 | // the Age field. 392 | type ByName []OsSerialPort 393 | 394 | func (a ByName) Len() int { return len(a) } 395 | func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 396 | func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } 397 | 398 | func friendlyNameCleanup(fnin string) (fnout string) { 399 | // This is an industry intelligence method to just cleanup common names 400 | // out there so we don't get ugly friendly names back 401 | fnout = regexp.MustCompile("\\(www.arduino.cc\\)").ReplaceAllString(fnin, "") 402 | fnout = regexp.MustCompile("Arduino\\s+Arduino").ReplaceAllString(fnout, "Arduino") 403 | fnout = regexp.MustCompile("\\s+").ReplaceAllString(fnout, " ") // multi space to single space 404 | fnout = regexp.MustCompile("^\\s+|\\s+$").ReplaceAllString(fnout, "") // trim 405 | return fnout 406 | } 407 | 408 | func getMetaDataForPort(port string) (string, error) { 409 | // search the folder structure on linux for this port name 410 | 411 | // search /sys folder 412 | oscmd := exec.Command("find", "/sys/devices", "-name", port, "-print") //, "2>", "/dev/null") 413 | 414 | // Stdout buffer 415 | cmdOutput := &bytes.Buffer{} 416 | // Attach buffer to command 417 | oscmd.Stdout = cmdOutput 418 | 419 | err := oscmd.Start() 420 | if err != nil { 421 | log.Fatal(err) 422 | } 423 | log.Printf("Waiting for command to finish... %v", oscmd) 424 | 425 | err = oscmd.Wait() 426 | 427 | if err != nil { 428 | log.Printf("Command finished with error: %v", err) 429 | } else { 430 | log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) 431 | // analyze stdin 432 | 433 | } 434 | 435 | return port + "coolio", nil 436 | } 437 | 438 | func getMetaDataForPortOld(port string) (string, error) { 439 | // search the folder structure on linux for this port name 440 | 441 | // search /sys folder 442 | oscmd := exec.Command("find", "/sys/devices", "-name", port, "-print") //, "2>", "/dev/null") 443 | 444 | // Stdout buffer 445 | cmdOutput := &bytes.Buffer{} 446 | // Attach buffer to command 447 | oscmd.Stdout = cmdOutput 448 | 449 | err := oscmd.Start() 450 | if err != nil { 451 | log.Fatal(err) 452 | } 453 | log.Printf("Waiting for command to finish... %v", oscmd) 454 | 455 | err = oscmd.Wait() 456 | 457 | if err != nil { 458 | log.Printf("Command finished with error: %v", err) 459 | } else { 460 | log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) 461 | // analyze stdin 462 | 463 | } 464 | 465 | return port + "coolio", nil 466 | } 467 | -------------------------------------------------------------------------------- /seriallist_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"fmt" 5 | //"github.com/lxn/win" 6 | "github.com/mattn/go-ole" 7 | "github.com/mattn/go-ole/oleutil" 8 | //"github.com/tarm/goserial" 9 | "github.com/johnlauer/goserial" 10 | "log" 11 | "os" 12 | "strings" 13 | //"encoding/binary" 14 | "strconv" 15 | "sync" 16 | //"syscall" 17 | ) 18 | 19 | var ( 20 | serialListWindowsWg sync.WaitGroup 21 | ) 22 | 23 | func removeNonArduinoBoards(ports []OsSerialPort) []OsSerialPort { 24 | return ports 25 | } 26 | 27 | func getList() ([]OsSerialPort, os.SyscallError) { 28 | // use a queue to do this to avoid conflicts 29 | // we've been getting crashes when this getList is requested 30 | // too many times too fast. i think it's something to do with 31 | // the unsafe syscalls overwriting memory 32 | 33 | // this will only block if waitgroupctr > 0. so first time 34 | // in shouldn't block 35 | serialListWindowsWg.Wait() 36 | 37 | serialListWindowsWg.Add(1) 38 | arr, sysCallErr := getListViaWmiPnpEntity() 39 | serialListWindowsWg.Done() 40 | //arr = make([]OsSerialPort, 0) 41 | 42 | // see if array has any data, if not fallback to the traditional 43 | // com port list model 44 | if len(arr) == 0 { 45 | // assume it failed 46 | arr, sysCallErr = getListViaOpen() 47 | } 48 | 49 | // see if array has any data, if not fallback to looking at 50 | // the registry list 51 | /* 52 | arr = make([]OsSerialPort, 0) 53 | if len(arr) == 0 { 54 | // assume it failed 55 | arr, sysCallErr = getListViaRegistry() 56 | } 57 | */ 58 | 59 | return arr, sysCallErr 60 | } 61 | 62 | func getListSynchronously() { 63 | 64 | } 65 | 66 | func getListViaWmiPnpEntity() ([]OsSerialPort, os.SyscallError) { 67 | 68 | //log.Println("Doing getListViaWmiPnpEntity()") 69 | 70 | // this method panics a lot and i'm not sure why, just catch 71 | // the panic and return empty list 72 | defer func() { 73 | if e := recover(); e != nil { 74 | // e is the interface{} typed-value we passed to panic() 75 | log.Println("Got panic: ", e) // Prints "Whoops: boom!" 76 | } 77 | }() 78 | 79 | var err os.SyscallError 80 | 81 | //var friendlyName string 82 | 83 | // init COM, oh yeah 84 | ole.CoInitialize(0) 85 | defer ole.CoUninitialize() 86 | 87 | unknown, _ := oleutil.CreateObject("WbemScripting.SWbemLocator") 88 | defer unknown.Release() 89 | 90 | wmi, _ := unknown.QueryInterface(ole.IID_IDispatch) 91 | defer wmi.Release() 92 | 93 | // service is a SWbemServices 94 | serviceRaw, _ := oleutil.CallMethod(wmi, "ConnectServer") 95 | service := serviceRaw.ToIDispatch() 96 | defer service.Release() 97 | 98 | // result is a SWBemObjectSet 99 | //pname := syscall.StringToUTF16("SELECT * FROM Win32_PnPEntity where Name like '%" + "COM35" + "%'") 100 | pname := "SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0 and Name like '%(COM%'" 101 | //pname := "SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0" 102 | resultRaw, err2 := oleutil.CallMethod(service, "ExecQuery", pname) 103 | //log.Println("Got result from oleutil.CallMethod") 104 | if err2 != nil { 105 | // we got back an error or empty list 106 | log.Printf("Got an error back from oleutil.CallMethod. err:%v", err2) 107 | return nil, err 108 | } 109 | 110 | result := resultRaw.ToIDispatch() 111 | defer result.Release() 112 | 113 | countVar, _ := oleutil.GetProperty(result, "Count") 114 | count := int(countVar.Val) 115 | 116 | list := make([]OsSerialPort, count) 117 | 118 | for i := 0; i < count; i++ { 119 | // item is a SWbemObject, but really a Win32_Process 120 | itemRaw, _ := oleutil.CallMethod(result, "ItemIndex", i) 121 | item := itemRaw.ToIDispatch() 122 | defer item.Release() 123 | 124 | asString, _ := oleutil.GetProperty(item, "Name") 125 | 126 | //log.Println(asString.ToString()) 127 | 128 | // get the com port 129 | //if false { 130 | s := strings.Split(asString.ToString(), "(COM")[1] 131 | s = "COM" + s 132 | s = strings.Split(s, ")")[0] 133 | list[i].Name = s 134 | list[i].FriendlyName = asString.ToString() 135 | //} 136 | } 137 | 138 | /* 139 | for index, element := range list { 140 | log.Println("index ", index, " element ", element.Name+ 141 | " friendly ", element.FriendlyName) 142 | } 143 | */ 144 | 145 | return list, err 146 | } 147 | 148 | func getListViaOpen() ([]OsSerialPort, os.SyscallError) { 149 | 150 | log.Println("Doing getListViaOpen(). Will try to open COM1 to COM99.") 151 | var err os.SyscallError 152 | list := make([]OsSerialPort, 100) 153 | var igood int = 0 154 | for i := 0; i < 100; i++ { 155 | prtname := "COM" + strconv.Itoa(i) 156 | conf := &serial.Config{Name: prtname, Baud: 9600} 157 | sp, err := serial.OpenPort(conf) 158 | //log.Println("Just tried to open port", prtname) 159 | if err == nil { 160 | //log.Println("Able to open port", prtname) 161 | list[igood].Name = prtname 162 | sp.Close() 163 | list[igood].FriendlyName = prtname 164 | //list[igood].FriendlyName = getFriendlyName(prtname) 165 | igood++ 166 | } 167 | } 168 | for index, element := range list[:igood] { 169 | log.Println("index ", index, " element ", element.Name+ 170 | " friendly ", element.FriendlyName) 171 | } 172 | return list[:igood], err 173 | } 174 | 175 | /* 176 | func getListViaRegistry() ([]OsSerialPort, os.SyscallError) { 177 | 178 | log.Println("Doing getListViaRegistry()") 179 | var err os.SyscallError 180 | var root win.HKEY 181 | rootpath, _ := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM") 182 | log.Println(win.RegOpenKeyEx(win.HKEY_LOCAL_MACHINE, rootpath, 0, win.KEY_READ, &root)) 183 | 184 | var name_length uint32 = 72 185 | var key_type uint32 186 | var lpDataLength uint32 = 72 187 | var zero_uint uint32 = 0 188 | name := make([]uint16, 72) 189 | lpData := make([]byte, 72) 190 | 191 | var retcode int32 192 | retcode = 0 193 | for retcode == 0 { 194 | retcode = win.RegEnumValue(root, zero_uint, &name[0], &name_length, nil, &key_type, &lpData[0], &lpDataLength) 195 | log.Println("Retcode:", retcode) 196 | log.Println("syscall name: "+syscall.UTF16ToString(name[:name_length-2])+"---- name_length:", name_length) 197 | log.Println("syscall lpdata:"+string(lpData[:lpDataLength-2])+"--- lpDataLength:", lpDataLength) 198 | //log.Println() 199 | zero_uint++ 200 | } 201 | win.RegCloseKey(root) 202 | win.RegOpenKeyEx(win.HKEY_LOCAL_MACHINE, rootpath, 0, win.KEY_READ, &root) 203 | 204 | list := make([]OsSerialPort, zero_uint) 205 | var i uint32 = 0 206 | for i = 0; i < zero_uint; i++ { 207 | win.RegEnumValue(root, i-1, &name[0], &name_length, nil, &key_type, &lpData[0], &lpDataLength) 208 | //name := string(lpData[:lpDataLength]) 209 | //name = name[:strings.Index(name, '\0')] 210 | //nameb := []byte(strings.TrimSpace(string(lpData[:lpDataLength]))) 211 | //list[i].Name = string(nameb) 212 | //list[i].Name = string(name[:strings.Index(name, "\0")]) 213 | //list[i].Name = fmt.Sprintf("%s", string(lpData[:lpDataLength-1])) 214 | pname := make([]uint16, (lpDataLength-2)/2) 215 | pname = convertByteArrayToUint16Array(lpData[:lpDataLength-2], lpDataLength-2) 216 | list[i].Name = syscall.UTF16ToString(pname) 217 | log.Println("The length of the name is:", len(list[i].Name)) 218 | log.Println("list[i].Name=" + list[i].Name + "---") 219 | //list[i].FriendlyName = getFriendlyName(list[i].Name) 220 | list[i].FriendlyName = getFriendlyName("COM34") 221 | } 222 | win.RegCloseKey(root) 223 | return list, err 224 | } 225 | */ 226 | 227 | func convertByteArrayToUint16Array(b []byte, mylen uint32) []uint16 { 228 | 229 | log.Println("converting. len:", mylen) 230 | var i uint32 231 | ret := make([]uint16, mylen/2) 232 | for i = 0; i < mylen; i += 2 { 233 | //ret[i/2] = binary.LittleEndian.Uint16(b[i : i+1]) 234 | ret[i/2] = uint16(b[i]) | uint16(b[i+1])<<8 235 | } 236 | return ret 237 | } 238 | 239 | func getFriendlyName(portname string) string { 240 | 241 | // this method panics a lot and i'm not sure why, just catch 242 | // the panic and return empty list 243 | defer func() { 244 | if e := recover(); e != nil { 245 | // e is the interface{} typed-value we passed to panic() 246 | log.Println("Got panic: ", e) // Prints "Whoops: boom!" 247 | } 248 | }() 249 | 250 | var friendlyName string 251 | 252 | // init COM, oh yeah 253 | ole.CoInitialize(0) 254 | defer ole.CoUninitialize() 255 | 256 | unknown, _ := oleutil.CreateObject("WbemScripting.SWbemLocator") 257 | defer unknown.Release() 258 | 259 | wmi, _ := unknown.QueryInterface(ole.IID_IDispatch) 260 | defer wmi.Release() 261 | 262 | // service is a SWbemServices 263 | serviceRaw, _ := oleutil.CallMethod(wmi, "ConnectServer") 264 | service := serviceRaw.ToIDispatch() 265 | defer service.Release() 266 | 267 | // result is a SWBemObjectSet 268 | //pname := syscall.StringToUTF16("SELECT * FROM Win32_PnPEntity where Name like '%" + "COM35" + "%'") 269 | pname := "SELECT * FROM Win32_PnPEntity where Name like '%" + portname + "%'" 270 | resultRaw, _ := oleutil.CallMethod(service, "ExecQuery", pname) 271 | result := resultRaw.ToIDispatch() 272 | defer result.Release() 273 | 274 | countVar, _ := oleutil.GetProperty(result, "Count") 275 | count := int(countVar.Val) 276 | 277 | for i := 0; i < count; i++ { 278 | // item is a SWbemObject, but really a Win32_Process 279 | itemRaw, _ := oleutil.CallMethod(result, "ItemIndex", i) 280 | item := itemRaw.ToIDispatch() 281 | defer item.Release() 282 | 283 | asString, _ := oleutil.GetProperty(item, "Name") 284 | 285 | println(asString.ToString()) 286 | friendlyName = asString.ToString() 287 | } 288 | 289 | return friendlyName 290 | } 291 | -------------------------------------------------------------------------------- /serialport.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | //"github.com/johnlauer/goserial" 7 | "github.com/facchinm/go-serial" 8 | "io" 9 | "log" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type SerialConfig struct { 15 | Name string 16 | Baud int 17 | 18 | // Size int // 0 get translated to 8 19 | // Parity SomeNewTypeToGetCorrectDefaultOf_None 20 | // StopBits SomeNewTypeToGetCorrectDefaultOf_1 21 | 22 | // RTSFlowControl bool 23 | // DTRFlowControl bool 24 | // XONFlowControl bool 25 | 26 | // CRLFTranslate bool 27 | // TimeoutStuff int 28 | RtsOn bool 29 | DtrOn bool 30 | } 31 | 32 | type serport struct { 33 | // The serial port connection. 34 | portConf *SerialConfig 35 | portIo io.ReadWriteCloser 36 | 37 | done chan bool // signals the end of this request 38 | 39 | // Keep track of whether we're being actively closed 40 | // just so we don't show scary error messages 41 | isClosing bool 42 | 43 | // counter incremented on queue, decremented on write 44 | itemsInBuffer int 45 | 46 | // buffered channel containing up to 25600 outbound messages. 47 | sendBuffered chan Cmd 48 | 49 | // unbuffered channel of outbound messages that bypass internal serial port buffer 50 | sendNoBuf chan Cmd 51 | 52 | // Do we have an extra channel/thread to watch our buffer? 53 | BufferType string 54 | //bufferwatcher *BufferflowDummypause 55 | bufferwatcher Bufferflow 56 | 57 | // Keep track of whether this is the primary serial port, i.e. cnc controller 58 | // or if its secondary, i.e. a backup port or arduino or something tertiary 59 | IsPrimary bool 60 | IsSecondary bool 61 | } 62 | 63 | type Cmd struct { 64 | data string 65 | id string 66 | skippedBuffer bool 67 | willHandleCompleteResponse bool 68 | } 69 | 70 | type CmdComplete struct { 71 | Cmd string 72 | Id string 73 | P string 74 | BufSize int `json:"-"` 75 | D string `json:"-"` 76 | } 77 | 78 | type qwReport struct { 79 | Cmd string 80 | QCnt int 81 | Id string 82 | D string `json:"-"` 83 | Buf string `json:"-"` 84 | P string 85 | } 86 | 87 | type SpPortMessage struct { 88 | P string // the port, i.e. com22 89 | D string // the data, i.e. G0 X0 Y0 90 | } 91 | 92 | func (p *serport) reader() { 93 | 94 | //var buf bytes.Buffer 95 | ch := make([]byte, 1024) 96 | timeCheckOpen := time.Now() 97 | 98 | for { 99 | 100 | n, err := p.portIo.Read(ch) 101 | 102 | //if we detect that port is closing, break out o this for{} loop. 103 | if p.isClosing { 104 | strmsg := "Shutting down reader on " + p.portConf.Name 105 | log.Println(strmsg) 106 | h.broadcastSys <- []byte(strmsg) 107 | break 108 | } 109 | 110 | // read can return legitimate bytes as well as an error 111 | // so process the bytes if n > 0 112 | if n > 0 { 113 | //log.Print("Read " + strconv.Itoa(n) + " bytes ch: " + string(ch)) 114 | data := string(ch[:n]) 115 | //log.Print("The data i will convert to json is:") 116 | //log.Print(data) 117 | 118 | // give the data to our bufferflow so it can do it's work 119 | // to read/translate the data to see if it wants to block 120 | // writes to the serialport. each bufferflow type will decide 121 | // this on its own based on its logic, i.e. tinyg vs grbl vs others 122 | //p.b.bufferwatcher..OnIncomingData(data) 123 | p.bufferwatcher.OnIncomingData(data) 124 | 125 | // see if the OnIncomingData handled the broadcast back 126 | // to the user. this option was added in case the OnIncomingData wanted 127 | // to do something fancier or implementation specific, i.e. TinyG Buffer 128 | // actually sends back data on a perline basis rather than our method 129 | // where we just send the moment we get it. the reason for this is that 130 | // the browser was sometimes getting back packets out of order which 131 | // of course would screw things up when parsing 132 | 133 | if p.bufferwatcher.IsBufferGloballySendingBackIncomingData() == false { 134 | //m := SpPortMessage{"Alice", "Hello"} 135 | m := SpPortMessage{p.portConf.Name, data} 136 | //log.Print("The m obj struct is:") 137 | //log.Print(m) 138 | 139 | //b, err := json.MarshalIndent(m, "", "\t") 140 | b, err := json.Marshal(m) 141 | if err != nil { 142 | log.Println(err) 143 | h.broadcastSys <- []byte("Error creating json on " + p.portConf.Name + " " + 144 | err.Error() + " The data we were trying to convert is: " + string(ch[:n])) 145 | break 146 | } 147 | //log.Print("Printing out json byte data...") 148 | //log.Print(string(b)) 149 | h.broadcastSys <- b 150 | //h.broadcastSys <- []byte("{ \"p\" : \"" + p.portConf.Name + "\", \"d\": \"" + string(ch[:n]) + "\" }\n") 151 | } 152 | } 153 | 154 | // double check that we got characters in the buffer 155 | // before deciding if an EOF is legitimately a reason 156 | // to close the port because we're seeing that on some 157 | // os's like Linux/Ubuntu you get an EOF when you open 158 | // the port. Perhaps the EOF was buffered from a previous 159 | // close and the OS doesn't clear out that buffer on a new 160 | // connect. This means we'll only catch EOF's when there are 161 | // other characters with it, but that seems to work ok 162 | if n <= 0 { 163 | if err == io.EOF || err == io.ErrUnexpectedEOF { 164 | // hit end of file 165 | log.Println("Hit end of file on serial port") 166 | h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got EOF (End of File) on port which usually means another app other than Serial Port JSON Server is locking your port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}") 167 | 168 | } 169 | 170 | if err != nil { 171 | log.Println(err) 172 | h.broadcastSys <- []byte("Error reading on " + p.portConf.Name + " " + 173 | err.Error() + " Closing port.") 174 | h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Got error reading on port. " + err.Error() + "\",\"Port\":\"" + p.portConf.Name + "\",\"Baud\":" + strconv.Itoa(p.portConf.Baud) + "}") 175 | break 176 | } 177 | 178 | // Keep track of time difference between two consecutive read with n == 0 and err == nil 179 | // we get here if the port has been disconnected while open (cpu usage will jump to 100%) 180 | // let's close the port only if the events are extremely fast (<1ms) 181 | if err == nil { 182 | diff := time.Since(timeCheckOpen) 183 | if diff.Nanoseconds() < 1000000 { 184 | p.isClosing = true 185 | } 186 | timeCheckOpen = time.Now() 187 | } 188 | } 189 | } 190 | p.portIo.Close() 191 | } 192 | 193 | // this method runs as its own thread because it's instantiated 194 | // as a "go" method. so if it blocks inside, it is ok 195 | func (p *serport) writerBuffered() { 196 | 197 | // this method can panic if user closes serial port and something is 198 | // in BlockUntilReady() and then a send occurs on p.sendNoBuf 199 | 200 | defer func() { 201 | if e := recover(); e != nil { 202 | // e is the interface{} typed-value we passed to panic() 203 | log.Println("Got panic: ", e) // Prints "Whoops: boom!" 204 | } 205 | }() 206 | 207 | // this for loop blocks on p.sendBuffered until that channel 208 | // sees something come in 209 | for data := range p.sendBuffered { 210 | 211 | //log.Printf("Got p.sendBuffered. data:%v, id:%v\n", string(data.data), string(data.id)) 212 | 213 | // we want to block here if we are being asked 214 | // to pause. 215 | goodToGo, willHandleCompleteResponse := p.bufferwatcher.BlockUntilReady(string(data.data), data.id) 216 | 217 | if goodToGo == false { 218 | log.Println("We got back from BlockUntilReady() but apparently we must cancel this cmd") 219 | // since we won't get a buffer decrement in p.sendNoBuf, we must do it here 220 | p.itemsInBuffer-- 221 | } else { 222 | // send to the non-buffered serial port writer 223 | //log.Println("About to send to p.sendNoBuf channel") 224 | data.willHandleCompleteResponse = willHandleCompleteResponse 225 | p.sendNoBuf <- data 226 | } 227 | } 228 | msgstr := "writerBuffered just got closed. make sure you make a new one. port:" + p.portConf.Name 229 | log.Println(msgstr) 230 | h.broadcastSys <- []byte(msgstr) 231 | } 232 | 233 | // this method runs as its own thread because it's instantiated 234 | // as a "go" method. so if it blocks inside, it is ok 235 | func (p *serport) writerNoBuf() { 236 | // this for loop blocks on p.send until that channel 237 | // sees something come in 238 | for data := range p.sendNoBuf { 239 | 240 | //log.Printf("Got p.sendNoBuf. data:%v, id:%v\n", string(data.data), string(data.id)) 241 | 242 | // if we get here, we were able to write successfully 243 | // to the serial port because it blocks until it can write 244 | 245 | // decrement counter 246 | p.itemsInBuffer-- 247 | log.Printf("itemsInBuffer:%v\n", p.itemsInBuffer) 248 | //h.broadcastSys <- []byte("{\"Cmd\":\"Write\",\"QCnt\":" + strconv.Itoa(p.itemsInBuffer) + ",\"Byte\":" + strconv.Itoa(n2) + ",\"Port\":\"" + p.portConf.Name + "\"}") 249 | 250 | // For reducing load on websocket, stop transmitting write data 251 | buf := "Buf" 252 | if data.skippedBuffer { 253 | buf = "NoBuf" 254 | } 255 | qwr := qwReport{ 256 | Cmd: "Write", 257 | QCnt: p.itemsInBuffer, 258 | Id: string(data.id), 259 | D: string(data.data), 260 | Buf: buf, 261 | P: p.portConf.Name, 262 | } 263 | qwrJson, _ := json.Marshal(qwr) 264 | h.broadcastSys <- qwrJson 265 | 266 | // FINALLY, OF ALL THE CODE IN THIS PROJECT 267 | // WE TRULY/FINALLY GET TO WRITE TO THE SERIAL PORT! 268 | n2, err := p.portIo.Write([]byte(data.data)) 269 | 270 | // see if we need to send back the completeResponse 271 | if data.willHandleCompleteResponse == false { 272 | // we need to send back complete response 273 | // Send fake cmd:"Complete" back 274 | //strCmd := data.data 275 | m := CmdComplete{"CompleteFake", data.id, p.portConf.Name, -1, data.data} 276 | msgJson, err := json.Marshal(m) 277 | if err == nil { 278 | h.broadcastSys <- msgJson 279 | } 280 | 281 | } 282 | 283 | log.Print("Just wrote ", n2, " bytes to serial: ", string(data.data)) 284 | //log.Print(n2) 285 | //log.Print(" bytes to serial: ") 286 | //log.Print(data) 287 | if err != nil { 288 | errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." 289 | log.Print(errstr) 290 | h.broadcastSys <- []byte(errstr) 291 | break 292 | } 293 | } 294 | msgstr := "Shutting down writer on " + p.portConf.Name 295 | log.Println(msgstr) 296 | h.broadcastSys <- []byte(msgstr) 297 | p.portIo.Close() 298 | } 299 | 300 | func spHandlerOpen(portname string, baud int, buftype string, isSecondary bool) { 301 | 302 | log.Print("Inside spHandler") 303 | 304 | var out bytes.Buffer 305 | 306 | out.WriteString("Opening serial port ") 307 | out.WriteString(portname) 308 | out.WriteString(" at ") 309 | out.WriteString(strconv.Itoa(baud)) 310 | out.WriteString(" baud") 311 | log.Print(out.String()) 312 | 313 | //h.broadcast <- []byte("Opened a serial port ") 314 | //h.broadcastSys <- out.Bytes() 315 | 316 | isPrimary := true 317 | if isSecondary { 318 | isPrimary = false 319 | } 320 | 321 | conf := &SerialConfig{Name: portname, Baud: baud, RtsOn: true} 322 | 323 | mode := &serial.Mode{ 324 | BaudRate: baud, 325 | Vmin: 0, 326 | Vtimeout: 10, 327 | } 328 | 329 | sp, err := serial.OpenPort(portname, mode) 330 | log.Print("Just tried to open port") 331 | if err != nil { 332 | //log.Fatal(err) 333 | log.Print("Error opening port " + err.Error()) 334 | //h.broadcastSys <- []byte("Error opening port. " + err.Error()) 335 | h.broadcastSys <- []byte("{\"Cmd\":\"OpenFail\",\"Desc\":\"Error opening port. " + err.Error() + "\",\"Port\":\"" + conf.Name + "\",\"Baud\":" + strconv.Itoa(conf.Baud) + "}") 336 | 337 | return 338 | } 339 | log.Print("Opened port successfully") 340 | //p := &serport{send: make(chan []byte, 256), portConf: conf, portIo: sp} 341 | // we can go up to 256,000 lines of gcode in the buffer 342 | p := &serport{sendBuffered: make(chan Cmd, 256000), sendNoBuf: make(chan Cmd), portConf: conf, portIo: sp, BufferType: buftype, IsPrimary: isPrimary, IsSecondary: isSecondary} 343 | 344 | // if user asked for a buffer watcher, i.e. tinyg/grbl then attach here 345 | if buftype == "tinyg" { 346 | 347 | bw := &BufferflowTinyg{Name: "tinyg", parent_serport: p} 348 | bw.Init() 349 | bw.Port = portname 350 | p.bufferwatcher = bw 351 | } else if buftype == "dummypause" { 352 | 353 | // this is a dummy pause type bufferflow object 354 | // to test artificially a delay on the serial port write 355 | // it just pauses 3 seconds on each serial port write 356 | bw := &BufferflowDummypause{} 357 | bw.Init() 358 | bw.Port = portname 359 | p.bufferwatcher = bw 360 | } else if buftype == "grbl" { 361 | // grbl bufferflow 362 | // store port as parent_serport for use in intializing a status query loop for '?' 363 | bw := &BufferflowGrbl{Name: "grbl", parent_serport: p} 364 | bw.Init() 365 | bw.Port = portname 366 | p.bufferwatcher = bw 367 | } else { 368 | bw := &BufferflowDefault{} 369 | bw.Init() 370 | bw.Port = portname 371 | p.bufferwatcher = bw 372 | } 373 | 374 | sh.register <- p 375 | defer func() { sh.unregister <- p }() 376 | // this is internally buffered thread to not send to serial port if blocked 377 | go p.writerBuffered() 378 | // this is thread to send to serial port regardless of block 379 | go p.writerNoBuf() 380 | p.reader() 381 | //go p.reader() 382 | //p.done = make(chan bool) 383 | //<-p.done 384 | } 385 | 386 | func spHandlerCloseExperimental(p *serport) { 387 | h.broadcastSys <- []byte("Pre-closing serial port " + p.portConf.Name) 388 | p.isClosing = true 389 | //close the port 390 | 391 | p.bufferwatcher.Close() 392 | p.portIo.Close() 393 | h.broadcastSys <- []byte("Bufferwatcher closed") 394 | p.portIo.Close() 395 | //elicit response from hardware to close out p.reader() 396 | //_, _ = p.portIo.Write([]byte("?")) 397 | //p.portIo.Read(nil) 398 | 399 | //close(p.portIo) 400 | h.broadcastSys <- []byte("portIo closed") 401 | close(p.sendBuffered) 402 | h.broadcastSys <- []byte("p.sendBuffered closed") 403 | close(p.sendNoBuf) 404 | h.broadcastSys <- []byte("p.sendNoBuf closed") 405 | 406 | //p.done <- true 407 | 408 | // unregister myself 409 | // we already have a deferred unregister in place from when 410 | // we opened. the only thing holding up that thread is the p.reader() 411 | // so if we close the reader we should get an exit 412 | h.broadcastSys <- []byte("Closing serial port " + p.portConf.Name) 413 | } 414 | 415 | func spHandlerClose(p *serport) { 416 | p.isClosing = true 417 | //close the port 418 | //elicit response from hardware to close out p.reader() 419 | _, _ = p.portIo.Write([]byte("?")) 420 | 421 | p.bufferwatcher.Close() 422 | p.portIo.Close() 423 | // unregister myself 424 | // we already have a deferred unregister in place from when 425 | // we opened. the only thing holding up that thread is the p.reader() 426 | // so if we close the reader we should get an exit 427 | h.broadcastSys <- []byte("Closing serial port " + p.portConf.Name) 428 | } 429 | -------------------------------------------------------------------------------- /snapshot/README.md: -------------------------------------------------------------------------------- 1 | serial-port-json-server 2 | ======================= 3 | Version 1.75 4 | 5 | A serial port JSON websocket & web server that runs from the command line on Windows, Mac, Linux, Raspberry Pi, or Beagle Bone that lets you communicate with your serial port from a web application. This enables web apps to be written that can communicate with your local serial device such as an Arduino, CNC controller, or any device that communicates over the serial port. 6 | 7 | The app is written in Go. It has an embedded web server and websocket server. The server runs on the standard port of localhost:8989. You can connect to it locally with your browser to interact by visiting http://localhost:8989. The websocket is technically running at ws://localhost/ws. You can of course connect to your websocket from any other computer to bind in remotely. For example, just connect to ws://192.168.1.10/ws if you are on a remote host where 192.168.1.10 is your devices actual IP address. 8 | 9 | The app is one executable with everything you need and is available ready-to-go for every major platform. It is a multi-threaded app that uses all of the cool techniques available in Go including extensive use of channels (threads) to create a super-responsive app. 10 | 11 | If you are a web developer and want to write a web application that connects to somebody's local or remote serial port server, then you simply need to create a websocket connection to the localhost or remote host and you will be directly interacting with that user's serial port. 12 | 13 | For example, if you wanted to create a Gcode Sender web app to enable people to send 3D print or milling commands from your site, this would be a perfect use case. Or if you've created an oscilloscope web app that connects to an Arduino, it would be another great use case. Finally you can write web apps that interact with a user's local hardware. 14 | 15 | Thanks go to gary.burd.info for the websocket example in Go. Thanks also go to tarm/goserial for the serial port base implementation. Thanks go to Jarret Luft at well for building the Grbl buffer and helping on global code changes to make everything better. 16 | 17 | Example Use Case 18 | --------- 19 | Here is a screenshot of the Serial Port JSON Server being used inside the ChiliPeppr Serial Port web console app. 20 | http://chilipeppr.com/serialport 21 | 22 | 23 | This is the Serial Port JSON Server being used inside the TinyG workspace in ChiliPeppr. 24 | http://chilipeppr.com/tinyg 25 | 26 | 27 | There is also a JSFiddle you can fork to create your own interface to the Serial Port JSON Server for your own project. 28 | http://jsfiddle.net/chilipeppr/vetj5fvx/ 29 | 30 | 31 | 32 | Running 33 | --------- 34 | From the command line issue the following command: 35 | - Mac/Linux 36 | `./serial-port-json-server` 37 | - Windows 38 | `serial-port-json-server.exe` 39 | 40 | Verbose logging mode: 41 | - Mac/Linux 42 | `./serial-port-json-server -v` 43 | - Windows 44 | `serial-port-json-server.exe -v` 45 | 46 | Running on alternate port: 47 | - Mac/Linux 48 | `./serial-port-json-server -addr :8000` 49 | - Windows 50 | `serial-port-json-server.exe -addr :8000` 51 | 52 | Here's a screenshot of a successful run on Windows x64. Make sure you allow the firewall to give access to Serial Port JSON Server or you'll wonder why it's not working. 53 | 54 | 55 | 56 | How to Build 57 | --------- 58 | Video tutorial of building SPJS on a Mac: https://www.youtube.com/watch?v=4Hou06bOuHc 59 | 60 | 1. Install Go (http://golang.org/doc/install) 61 | 2. If you're on a Mac, install Xcode from the Apple Store because you'll need gcc to compile the native code for a Mac. If you're on Windows, Linux, Raspberry Pi, or Beagle Bone you are all set. 62 | 3. Get go into your path so you can run "go" from any directory: 63 | On Linux, Mac, Raspberry Pi, Beagle Bone Black 64 | export PATH=$PATH:/usr/local/go/bin 65 | On Windows, use the Environment Variables dialog by right-click My Computer 66 | 4. Define your GOPATH variable and create the folder to match. This is your personal working folder for all yourGo code. This is important because you will be retrieving several projects from Github and Go needs to know where to download all the files and where to build the directory structure. On my Windows computer I created a folder called C:\Users\John\go and set GOPATH=C:\Users\John\go 67 | On Mac 68 | export GOPATH=/Users/john/go 69 | On Linux, Raspberry Pi, Beagle Bone Black, Intel Edison 70 | export GOPATH=/home/john/go 71 | On Windows, use the Environment Variables dialog by right-click My Computer to create GOPATH 72 | 5. Change directory into your GOPATH 73 | 6. Type "go get github.com/johnlauer/serial-port-json-server". This will retrieve this Github project and all dependent projects. It takes some time to run this. 74 | 7. Then change direcory into github.com\johnlauer\serial-port-json-server. 75 | 8. Type "go build" when you're inside that directory and it will create a binary called serial-port-json-server 76 | 9. Run it by typing ./serial-port-json-server or on Windows run serial-port-json-server.exe 77 | 10. If you have a firewall on the computer running the serial-port-json-server you must allow port 8989 in the firewall. 78 | 79 | Supported Commands 80 | ------- 81 | 82 | Command | Example | Description 83 | ------- | ------- | ------- 84 | list | | Lists all available serial ports on your device 85 | open portName baudRate [bufferAlgorithm] | open /dev/ttyACM0 115200 tinyg | Opens a serial port. The comPort should be the Name of the port inside the list response such as COM2 or /dev/ttyACM0. The baudrate should be a rate from the baudrates command or a typical baudrate such as 9600 or 115200. A bufferAlgorithm can be optionally specified such as "tinyg" (or in the future "grbl" if somebody writes it) or write your own. 86 | sendjson {} | {"P":"COM22","Data":[{"D":"!~\n","Id":"234"},{"D":"{\"sr\":\"\"}\n","Id":"235"}]} | See Wiki page at https://github.com/johnlauer/serial-port-json-server/wiki 87 | send portName data | send /dev/ttyACM0 G1 X10.5 Y2 F100\n | Send your data to the serial port. Remember to send a newline in your data if your serial port expects it. 88 | sendnobuf portName data | send COM22 {"qv":0}\n | Send your data and bypass the bufferFlowAlgorithm if you specified one. 89 | close portName | close COM1 | Close out your serial port 90 | bufferalgorithms | | List the available bufferAlgorithms on the server. You will get a list such as "default, tinyg" 91 | baudrates | | List common baudrates such as 2400, 9600, 115200 92 | 93 | FAQ 94 | ------- 95 | - Q: There are several Node.js serial port servers. Why not write this in Node.js instead of Go? 96 | 97 | - A: Because Go is a better solution for several reasons. 98 | - Easier to install on your computer. Just download and run binary. (Node requires big install) 99 | - It is multi-threaded which is key for a serial port websocket server (Node is single-threaded) 100 | - It has a tiny memory footprint using about 3MB of RAM 101 | - It is one clean compiled executable with no dependencies 102 | - It makes very efficient use of RAM with amazing garbage collection 103 | - It is super fast when running 104 | - It launches super quick 105 | - It is essentially C code without the pain of C code. Go has insanely amazing threading support called Channels. Node.js is single-threaded, so you can't take full advantage of the CPU's threading capabilities. Go lets you do this easily. A serial port server needs several threads. 1) Websocket thread for each connection. 2) Serial port thread for each serial device. Serial Port JSON Server allows you to bind as many serial port devices in parallel as you want. 3) A writer and reader thread for each serial port. 4) A buffering thread for each incoming message from the browser into the websocket 5) A buffering thread for messages back out from the server to the websocket to the browser. To achieve this in Node requires lots of callbacks. You also end up talking natively anyway to the serial port on each specific platform you're on, so you have to deal with the native code glued to Node. 106 | 107 | Revisions 108 | ------- 109 | Changes in 1.75 110 | - Tweaked the order of operations for pausing/unpausing the buffer in Grbl and TinyG to account for rare cases where a deadlock could occur. This should guarantee no dead-locking. 111 | - Jarret Luft added an artificial % buffer wipe to Grbl buffer to mimic to some degree the buffer wiping available on TinyG. 112 | 113 | Changes in 1.7 114 | - sendjson now supported. Will give back onQueue, onWrite, onComplete 115 | - Moved TinyG buffer to serial byte counting. 116 | 117 | Changes in 1.6 118 | - Logging is now off by default so Raspberry Pi runs cleaner. The immense amount of logging was dragging the Raspi down. Should help on BeagleBone Black as well. Makes SPJS run more efficient on powerful systems too like Windows, Mac, and Linux. You can turn on logging by issuing a -v on the command line. This fix by Jarret Luft. 119 | - Added EOF extra checking for Linux serial ports that seem to return an EOF on a new connect and thus the port was prematurely closing. Thanks to Yiannis Mandravellos for finding the bug and fixing it. 120 | - Added a really nice Grbl bufferAlgorithm which was written by Jarret Luft who is the creator of the Grbl workspace in ChiliPeppr. 121 | - The buffer counts each line of gcode being sent to Grbl up to 127 bytes and then doesn't send anymore data to Grbl until it sees an OK or ERROR response from Grbl indicating the command was processed. For each OK|ERROR the buffer decrements the counter to see how much more room is avaialble. If the next Gcode command can fit it is sent immediately in. 122 | - This new Grbl buffer should mirror the stream.py example code from Sonny Jeon who maintains Grbl. This Serial Port JSON Server should now be able to execute the commands faster than anything out there since it's written in Go (which is C) and is compiled and super-fast. 123 | - Position requests occur inside this buffer where a ? is sent every 250ms to Grbl such that you should see a position just come back on demand non-stop from Grbl. It could be possible in a future version to only queue these position reports up during actual Gcode commands being sent so that when idle there are not a ton of position updates being sent back that aren't necessary. 124 | - Soft resets (Ctrl-x) now wipe the buffer. 125 | - !~? will skip ahead of all other commands now. This is important for jogging or using ! as a quick stop of your controller since you can have 25,000 lines of gcode queued to SPJS now and of course you would want these commands to skip in front of that queue. 126 | - Feedhold pauses the buffer inside SPJS now. 127 | - Cycle resume ~ unpauses the buffer inside SPJS now. 128 | - When using this buffer data is sent back in a per line mode rather than as characters are received so there is more efficiency on the websocket. 129 | - Checks for the grbl init line indicating the arduino is ready to accept commands 130 | 131 | Changes in 1.5 132 | - For TinyG buffer, moved to slot counter approach. The buffer planner approach was causing G2/G3 commands to overflow the buffer because the round-trip time was too off with reading QR responses. So, moved to a 4 slot buffer approach. Jogging is still a bit rough in this approach, but that can get tweaked. The new slot approach is more like counting serial buffer queue items. SPJS sends up to 4 commands and then waits for a r:{} json response. It has intelligence to know if certain commands won't get a response like !~% or newlines, so it doesn't look for slot responses and just blindly sends. The only danger is if there are 4 really long lines of Gcode that surpass the 254 bytes in the serial buffer then we could overflow. Could add trapping for that. 133 | 134 | Changes in 1.4 135 | - Added reporting on Queuing so you know what the state of the Serial Port JSON Server Queue is doing. The reason for this is to ensure your serial port commands don't get out of order you will want to make sure you write to the websocket and then wait for the {"Cmd":"Queued"} response. Then write your next command. This is necessary because when sending different frames across a websocket over the Internet, you can get packet retransmissions, and although you'll never lose your data, your serial commands could arrive at the server out of order. By watching that your command is queued, you are safe to send the next command. However, this can also slow things down, so now you can simply gang up multiple commands into one send and the Serial Port JSON Server will split them into separate sub-commands and tell you that it did in the queue and write reports. 136 | - For example, a typical queue report looks like {"Cmd":"Queued","QCnt":61,"Type":["Buf"],"D":["{\"sr\":\"\"}\n"],"Port":"COM22"}. 137 | - If you send something like: send COM22 {"sr":""}\n{"qr":""}\n{"sr":""}\n{"qr":""}\n. You will get back a queue report like {"Cmd":"Queued","QCnt":4,"Type":["Buf","Buf","Buf","Buf"],"D":["{\"sr\":\"\"}\n","{\"qr\":\"\"}\n","{\"sr\":\"\"}\n","{\"qr\":\"\"}\n"],"Port":"COM22"} 138 | - When two queue items are written to the serial port you will get back something like {"Cmd":"Write","QCnt":1,"D":"{\"qr\":\"\"}\n","Port":"COM22"}{"Cmd":"Write","QCnt":0,"D":"{\"sr\":\"\"}\n","Port":"COM22"} 139 | - Fixed analysis of incoming serial data due to some serial ports sending fragmented data. 140 | - Added bufferalgorithms and baudrates commands 141 | - A new command called sendnobuf was added so you can bypass the bufferflow algorithm. This command only is worth using if you specified a bufflerFlowAlgorithm when you opened the serial port. You use it by sending "sendnobuf com4 G0 X0 Y0" and it will jump ahead of the queue and go diretly to the serial port without hesitation. 142 | - TinyG Bufferflow algorithm. 143 | - Looks for qr responses and if they are too low on the planner buffer will trigger a pause on send. 144 | - Looks for qr responses and if they are high enough to send again the bufferflow is unblocked. 145 | - If you pause with ! then the bufferflow also pauses. 146 | - If you resume with ~ then the bufferflow also resumes. 147 | - If you wipe the buffer with % then the bufferflow also wipes. 148 | - When you send !~% it automatically is sent to TinyG without buffering so it essentially skips ahead of all other buffered commands. This mimics what TinyG does internally. 149 | - If you ask qr reports to be turned off with a $qv=0 or {"qv":0} then bypassmode is entered whereby no blocking occurs on sending serial port commands. 150 | - If you ask qr reports to be turned back on with $qv=1 (or 2 or 3) or {"qv":1} (or 2 or 3) then bypassmode is turned off. 151 | - If a qr reponse is seen from TinyG then BypassMode is turned off automatically. 152 | 153 | Changes in 1.3 154 | - Added ability for buffer flow plugins. There is a new buffer flow plugin 155 | for TinyG that watches the {"qr":NN} response. When it sees the qr value 156 | go below 12 it pauses its own sending and queues up whatever is still coming 157 | in on the Websocket. This is fine because we've got plenty of RAM on the 158 | websocket server. The {"qr":NN} value is still sent back on the websocket as 159 | soon as it was before, so the host application should see no real difference 160 | as to how it worked before. The difference now though is that the serial sending 161 | knows to check if sending is paused to the serial port and queue. This makes 162 | sure no buffer overflows ever occur. The reason this was becoming important is 163 | that the lag time between the qr response and the sending of Gcode was too distant 164 | and this buffer flow needs resolution around 5ms. Normal latency on the Internet 165 | is like 20ms to 200ms, so it just wasn't fast enough. If the Javascript hosting 166 | the websocket was busy processing other events, then this lag time became even 167 | worse. So, now the Serial Port JSON Server simply helps out by lots of extra 168 | buffering. Go ahead and pound it even harder with more serial commands and see 169 | it fly. 170 | 171 | Changes in 1.2 172 | - Added better error handling 173 | - Removed forcibly adding a newline to the serial data being sent to the port. This 174 | means apps must send in a newline if the serial port expects it. 175 | - Embedded the home.html file inside the binary so there is no longer a dependency 176 | on an external file. 177 | - TODO: Closing a port on Beagle Bone seems to hang. Only solution now is to kill 178 | the process and restart. 179 | - TODO: Mac implementation seems to have trouble on writing data after a while. Mac 180 | gray screen of death can appear. Mac version uses CGO, so it is in unsafe mode. 181 | May have to rework Mac serial port to use pure golang code. 182 | -------------------------------------------------------------------------------- /snapshot/downloads.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Downloads 4 | --- 5 | 6 | serial-port-json-server downloads (version snapshot) 7 | 8 | ### Linux 9 | 10 | * [serial-port-json-server\_linux\_386.tar.gz](serial-port-json-server_linux_386.tar.gz) 11 | * [serial-port-json-server\_linux\_amd64.tar.gz](serial-port-json-server_linux_amd64.tar.gz) 12 | * [serial-port-json-server\_linux\_arm.tar.gz](serial-port-json-server_linux_arm.tar.gz) 13 | * [serial-port-json-server\_snapshot\_amd64.deb](serial-port-json-server_snapshot_amd64.deb) 14 | * [serial-port-json-server\_snapshot\_armhf.deb](serial-port-json-server_snapshot_armhf.deb) 15 | * [serial-port-json-server\_snapshot\_i386.deb](serial-port-json-server_snapshot_i386.deb) 16 | 17 | ### MS Windows 18 | 19 | * [serial-port-json-server\_windows\_386.zip](serial-port-json-server_windows_386.zip) 20 | * [serial-port-json-server\_windows\_amd64.zip](serial-port-json-server_windows_amd64.zip) 21 | 22 | ### Other files 23 | 24 | * [README.md](README.md) 25 | 26 | 27 | 28 | Generated by goxc -------------------------------------------------------------------------------- /trayicon.go: -------------------------------------------------------------------------------- 1 | // 2 | // trayicon.go 3 | // 4 | // Created by Martino Facchin 5 | // Copyright (c) 2015 Arduino LLC 6 | // 7 | // Permission is hereby granted, free of charge, to any person 8 | // obtaining a copy of this software and associated documentation 9 | // files (the "Software"), to deal in the Software without 10 | // restriction, including without limitation the rights to use, 11 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the 13 | // Software is furnished to do so, subject to the following 14 | // conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | // OTHER DEALINGS IN THE SOFTWARE. 27 | // 28 | 29 | // +build !arm 30 | 31 | package main 32 | 33 | import ( 34 | "fmt" 35 | "github.com/facchinm/systray" 36 | "github.com/facchinm/systray/example/icon" 37 | "github.com/skratchdot/open-golang/open" 38 | "runtime" 39 | ) 40 | 41 | func setupSysTray() { 42 | runtime.LockOSThread() 43 | systray.Run(setupSysTrayReal) 44 | } 45 | 46 | func setupSysTrayReal() { 47 | 48 | systray.SetIcon(icon.Data) 49 | mUrl := systray.AddMenuItem("Go to create.arduino.cc", "Arduino Create") 50 | mQuit := systray.AddMenuItem("Quit", "Quit the bridge") 51 | 52 | go func() { 53 | <-mQuit.ClickedCh 54 | systray.Quit() 55 | fmt.Println("Quit now...") 56 | exit() 57 | }() 58 | 59 | // We can manipulate the systray in other goroutines 60 | go func() { 61 | <-mUrl.ClickedCh 62 | open.Run("http://create.arduino.cc") 63 | }() 64 | } 65 | -------------------------------------------------------------------------------- /trayicon_linux_arm.go: -------------------------------------------------------------------------------- 1 | // 2 | // trayicon.go 3 | // 4 | // Created by Martino Facchin 5 | // Copyright (c) 2015 Arduino LLC 6 | // 7 | // Permission is hereby granted, free of charge, to any person 8 | // obtaining a copy of this software and associated documentation 9 | // files (the "Software"), to deal in the Software without 10 | // restriction, including without limitation the rights to use, 11 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the 13 | // Software is furnished to do so, subject to the following 14 | // conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | // OTHER DEALINGS IN THE SOFTWARE. 27 | // 28 | 29 | package main 30 | 31 | func setupSysTray() { 32 | //no systray support for arm yet 33 | select {} 34 | } 35 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | // 2 | // update.go 3 | // 4 | // Created by Martino Facchin 5 | // Code from github.com/sanbornm and github.com/sanderhahn 6 | // Copyright (c) 2015 Arduino LLC 7 | // 8 | // Permission is hereby granted, free of charge, to any person 9 | // obtaining a copy of this software and associated documentation 10 | // files (the "Software"), to deal in the Software without 11 | // restriction, including without limitation the rights to use, 12 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the 14 | // Software is furnished to do so, subject to the following 15 | // conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be 18 | // included in all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | // OTHER DEALINGS IN THE SOFTWARE. 28 | // 29 | 30 | package main 31 | 32 | import ( 33 | "archive/zip" 34 | "bytes" 35 | "compress/gzip" 36 | "crypto/sha256" 37 | "encoding/json" 38 | "errors" 39 | "fmt" 40 | "io" 41 | "io/ioutil" 42 | "log" 43 | "math/rand" 44 | "net/http" 45 | "os" 46 | "path" 47 | "path/filepath" 48 | "runtime" 49 | "strings" 50 | "time" 51 | 52 | "github.com/inconshreveable/go-update" 53 | "github.com/kardianos/osext" 54 | "github.com/kr/binarydist" 55 | "github.com/termie/go-shutil" 56 | 57 | patch "github.com/sanderhahn/gozip/patchzip" 58 | ) 59 | 60 | func IsZip(path string) bool { 61 | r, err := zip.OpenReader(path) 62 | if err == nil { 63 | r.Close() 64 | return true 65 | } 66 | return false 67 | } 68 | 69 | func Zip(path string, dirs []string) (err error) { 70 | if IsZip(path) { 71 | return errors.New(path + " is already a zip file") 72 | } 73 | 74 | f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 75 | if err != nil { 76 | return 77 | } 78 | defer f.Close() 79 | 80 | startoffset, err := f.Seek(0, os.SEEK_END) 81 | if err != nil { 82 | return 83 | } 84 | 85 | w := patch.NewWriterAt(f, startoffset) 86 | 87 | for _, dir := range dirs { 88 | err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 89 | if err != nil { 90 | return err 91 | } 92 | 93 | fh, err := patch.FileInfoHeader(info) 94 | if err != nil { 95 | return err 96 | } 97 | fh.Name = path 98 | 99 | p, err := w.CreateHeader(fh) 100 | if err != nil { 101 | return err 102 | } 103 | if !info.IsDir() { 104 | content, err := ioutil.ReadFile(path) 105 | if err != nil { 106 | return err 107 | } 108 | _, err = p.Write(content) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | return err 114 | }) 115 | } 116 | err = w.Close() 117 | return 118 | } 119 | 120 | func Unzip(zippath string, destination string) (err error) { 121 | r, err := zip.OpenReader(zippath) 122 | if err != nil { 123 | return err 124 | } 125 | for _, f := range r.File { 126 | fullname := path.Join(destination, f.Name) 127 | if f.FileInfo().IsDir() { 128 | os.MkdirAll(fullname, f.FileInfo().Mode().Perm()) 129 | } else { 130 | os.MkdirAll(filepath.Dir(fullname), 0755) 131 | perms := f.FileInfo().Mode().Perm() 132 | out, err := os.OpenFile(fullname, os.O_CREATE|os.O_RDWR, perms) 133 | if err != nil { 134 | return err 135 | } 136 | rc, err := f.Open() 137 | if err != nil { 138 | return err 139 | } 140 | _, err = io.CopyN(out, rc, f.FileInfo().Size()) 141 | if err != nil { 142 | return err 143 | } 144 | rc.Close() 145 | out.Close() 146 | 147 | mtime := f.FileInfo().ModTime() 148 | err = os.Chtimes(fullname, mtime, mtime) 149 | if err != nil { 150 | return err 151 | } 152 | } 153 | } 154 | return 155 | } 156 | 157 | func UnzipList(path string) (list []string, err error) { 158 | r, err := zip.OpenReader(path) 159 | if err != nil { 160 | return 161 | } 162 | for _, f := range r.File { 163 | list = append(list, f.Name) 164 | } 165 | return 166 | } 167 | 168 | // Update protocol: 169 | // 170 | // GET hk.heroku.com/hk/linux-amd64.json 171 | // 172 | // 200 ok 173 | // { 174 | // "Version": "2", 175 | // "Sha256": "..." // base64 176 | // } 177 | // 178 | // then 179 | // 180 | // GET hkpatch.s3.amazonaws.com/hk/1/2/linux-amd64 181 | // 182 | // 200 ok 183 | // [bsdiff data] 184 | // 185 | // or 186 | // 187 | // GET hkdist.s3.amazonaws.com/hk/2/linux-amd64.gz 188 | // 189 | // 200 ok 190 | // [gzipped executable data] 191 | // 192 | // 193 | 194 | const ( 195 | plat = runtime.GOOS + "-" + runtime.GOARCH 196 | ) 197 | 198 | const devValidTime = 7 * 24 * time.Hour 199 | 200 | var ErrHashMismatch = errors.New("new file hash mismatch after patch") 201 | var up = update.New() 202 | 203 | // Updater is the configuration and runtime data for doing an update. 204 | // 205 | // Note that ApiURL, BinURL and DiffURL should have the same value if all files are available at the same location. 206 | // 207 | // Example: 208 | // 209 | // updater := &selfupdate.Updater{ 210 | // CurrentVersion: version, 211 | // ApiURL: "http://updates.yourdomain.com/", 212 | // BinURL: "http://updates.yourdownmain.com/", 213 | // DiffURL: "http://updates.yourdomain.com/", 214 | // Dir: "update/", 215 | // CmdName: "myapp", // app name 216 | // } 217 | // if updater != nil { 218 | // go updater.BackgroundRun() 219 | // } 220 | type Updater struct { 221 | CurrentVersion string // Currently running version. 222 | ApiURL string // Base URL for API requests (json files). 223 | CmdName string // Command name is appended to the ApiURL like http://apiurl/CmdName/. This represents one binary. 224 | BinURL string // Base URL for full binary downloads. 225 | DiffURL string // Base URL for diff downloads. 226 | Dir string // Directory to store selfupdate state. 227 | Info struct { 228 | Version string 229 | Sha256 []byte 230 | } 231 | } 232 | 233 | func (u *Updater) getExecRelativeDir(dir string) string { 234 | filename, _ := osext.Executable() 235 | path := filepath.Join(filepath.Dir(filename), dir) 236 | return path 237 | } 238 | 239 | // BackgroundRun starts the update check and apply cycle. 240 | func (u *Updater) BackgroundRun() error { 241 | os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777) 242 | if u.wantUpdate() { 243 | if err := up.CanUpdate(); err != nil { 244 | // fail 245 | return err 246 | } 247 | //self, err := osext.Executable() 248 | //if err != nil { 249 | // fail update, couldn't figure out path to self 250 | //return 251 | //} 252 | // TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports. 253 | if err := u.update(); err != nil { 254 | return err 255 | } 256 | } 257 | return nil 258 | } 259 | 260 | func (u *Updater) wantUpdate() bool { 261 | if strings.Contains(u.CurrentVersion, "dev") { 262 | return false 263 | } else { 264 | return true 265 | } 266 | } 267 | 268 | func (u *Updater) update() error { 269 | path, err := osext.Executable() 270 | if err != nil { 271 | return err 272 | } 273 | old, err := os.Open(path) 274 | if err != nil { 275 | return err 276 | } 277 | defer old.Close() 278 | 279 | err = u.fetchInfo() 280 | if err != nil { 281 | return err 282 | } 283 | if u.Info.Version == u.CurrentVersion { 284 | return nil 285 | } 286 | bin, err := u.fetchAndVerifyPatch(old) 287 | if err != nil { 288 | if err == ErrHashMismatch { 289 | log.Println("update: hash mismatch from patched binary") 290 | } else { 291 | if u.DiffURL != "" { 292 | log.Println("update: patching binary,", err) 293 | } 294 | } 295 | 296 | bin, err = u.fetchAndVerifyFullBin() 297 | if err != nil { 298 | if err == ErrHashMismatch { 299 | log.Println("update: hash mismatch from full binary") 300 | } else { 301 | log.Println("update: fetching full binary,", err) 302 | } 303 | return err 304 | } 305 | } 306 | 307 | // close the old binary before installing because on windows 308 | // it can't be renamed if a handle to the file is still open 309 | old.Close() 310 | 311 | err, errRecover := up.FromStream(bytes.NewBuffer(bin)) 312 | if errRecover != nil { 313 | return fmt.Errorf("update and recovery errors: %q %q", err, errRecover) 314 | } 315 | if err != nil { 316 | return err 317 | } 318 | 319 | // remove config.ini so at restart the package will extract again 320 | shutil.CopyFile(*configIni, *configIni+".bak", false) 321 | os.Remove(*configIni) 322 | 323 | // update done, we should decide if we need to restart ASAP (maybe a field in update json?) 324 | // BIG issue: the file has been renamed in the meantime 325 | 326 | return nil 327 | } 328 | 329 | func (u *Updater) fetchInfo() error { 330 | r, err := fetch(u.ApiURL + u.CmdName + "/" + plat + ".json") 331 | if err != nil { 332 | return err 333 | } 334 | defer r.Close() 335 | err = json.NewDecoder(r).Decode(&u.Info) 336 | if err != nil { 337 | return err 338 | } 339 | if len(u.Info.Sha256) != sha256.Size { 340 | return errors.New("bad cmd hash in info") 341 | } 342 | return nil 343 | } 344 | 345 | func (u *Updater) fetchAndVerifyPatch(old io.Reader) ([]byte, error) { 346 | bin, err := u.fetchAndApplyPatch(old) 347 | if err != nil { 348 | return nil, err 349 | } 350 | if !verifySha(bin, u.Info.Sha256) { 351 | return nil, ErrHashMismatch 352 | } 353 | return bin, nil 354 | } 355 | 356 | func (u *Updater) fetchAndApplyPatch(old io.Reader) ([]byte, error) { 357 | r, err := fetch(u.DiffURL + u.CmdName + "/" + u.CurrentVersion + "/" + u.Info.Version + "/" + plat) 358 | if err != nil { 359 | return nil, err 360 | } 361 | defer r.Close() 362 | var buf bytes.Buffer 363 | err = binarydist.Patch(old, &buf, r) 364 | return buf.Bytes(), err 365 | } 366 | 367 | func (u *Updater) fetchAndVerifyFullBin() ([]byte, error) { 368 | bin, err := u.fetchBin() 369 | if err != nil { 370 | return nil, err 371 | } 372 | verified := verifySha(bin, u.Info.Sha256) 373 | if !verified { 374 | return nil, ErrHashMismatch 375 | } 376 | return bin, nil 377 | } 378 | 379 | func (u *Updater) fetchBin() ([]byte, error) { 380 | r, err := fetch(u.BinURL + u.CmdName + "/" + u.Info.Version + "/" + plat + ".gz") 381 | if err != nil { 382 | return nil, err 383 | } 384 | defer r.Close() 385 | buf := new(bytes.Buffer) 386 | gz, err := gzip.NewReader(r) 387 | if err != nil { 388 | return nil, err 389 | } 390 | if _, err = io.Copy(buf, gz); err != nil { 391 | return nil, err 392 | } 393 | 394 | return buf.Bytes(), nil 395 | } 396 | 397 | // returns a random duration in [0,n). 398 | func randDuration(n time.Duration) time.Duration { 399 | return time.Duration(rand.Int63n(int64(n))) 400 | } 401 | 402 | func fetch(url string) (io.ReadCloser, error) { 403 | resp, err := http.Get(url) 404 | if err != nil { 405 | return nil, err 406 | } 407 | if resp.StatusCode != 200 { 408 | return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) 409 | } 410 | return resp.Body, nil 411 | } 412 | 413 | func readTime(path string) time.Time { 414 | p, err := ioutil.ReadFile(path) 415 | if os.IsNotExist(err) { 416 | return time.Time{} 417 | } 418 | if err != nil { 419 | return time.Now().Add(1000 * time.Hour) 420 | } 421 | t, err := time.Parse(time.RFC3339, string(p)) 422 | if err != nil { 423 | return time.Now().Add(1000 * time.Hour) 424 | } 425 | return t 426 | } 427 | 428 | func verifySha(bin []byte, sha []byte) bool { 429 | h := sha256.New() 430 | h.Write(bin) 431 | return bytes.Equal(h.Sum(nil), sha) 432 | } 433 | 434 | func writeTime(path string, t time.Time) bool { 435 | return ioutil.WriteFile(path, []byte(t.Format(time.RFC3339)), 0644) == nil 436 | } 437 | -------------------------------------------------------------------------------- /utilities.go: -------------------------------------------------------------------------------- 1 | // utilities.go 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "crypto/md5" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/kardianos/osext" 11 | "io" 12 | "io/ioutil" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | type Combo struct { 20 | vendors map[string]Vendor 21 | } 22 | 23 | type Vendor struct { 24 | architectures map[string]Architecture 25 | } 26 | 27 | type Architecture struct { 28 | boards map[string]Board 29 | platform map[string]Platform 30 | } 31 | 32 | type Platform struct { 33 | } 34 | 35 | type Board struct { 36 | name string 37 | vid map[int]int 38 | pid map[int]int 39 | upload Upload 40 | build Build 41 | bootloader Bootloader 42 | } 43 | 44 | type Upload struct { 45 | protocol string 46 | disable_flushing bool 47 | maximum_data_size int 48 | tool string 49 | use_1200bps_touch bool 50 | speed int 51 | maximum_size int 52 | wait_for_upload_port int 53 | } 54 | 55 | type Build struct { 56 | core string 57 | f_cpu int 58 | board string 59 | vid int 60 | pid int 61 | usb_product string 62 | mcu string 63 | extra_flags string 64 | variant string 65 | } 66 | 67 | type Bootloader struct { 68 | extended_fuses int 69 | high_fuses int 70 | file string 71 | low_fuses int 72 | lock_bits int 73 | tool string 74 | unlock_bits int 75 | } 76 | 77 | type ConfigProperty struct { 78 | value string 79 | path string 80 | } 81 | 82 | func computeMd5(filePath string) ([]byte, error) { 83 | var result []byte 84 | file, err := os.Open(filePath) 85 | if err != nil { 86 | return result, err 87 | } 88 | defer file.Close() 89 | 90 | hash := md5.New() 91 | if _, err := io.Copy(hash, file); err != nil { 92 | return result, err 93 | } 94 | 95 | return hash.Sum(result), nil 96 | } 97 | 98 | func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error) { 99 | if stack[0].Process == nil { 100 | if err = stack[0].Start(); err != nil { 101 | return err 102 | } 103 | } 104 | if len(stack) > 1 { 105 | if err = stack[1].Start(); err != nil { 106 | return err 107 | } 108 | defer func() { 109 | pipes[0].Close() 110 | err = call(stack[1:], pipes[1:]) 111 | }() 112 | } 113 | return stack[0].Wait() 114 | } 115 | 116 | // code inspired by https://gist.github.com/tyndyll/89fbb2c2273f83a074dc 117 | func pipe_commands(commands ...*exec.Cmd) ([]byte, error) { 118 | var errorBuffer, outputBuffer bytes.Buffer 119 | pipeStack := make([]*io.PipeWriter, len(commands)-1) 120 | i := 0 121 | for ; i < len(commands)-1; i++ { 122 | stdinPipe, stdoutPipe := io.Pipe() 123 | commands[i].Stdout = stdoutPipe 124 | commands[i].Stderr = &errorBuffer 125 | commands[i+1].Stdin = stdinPipe 126 | pipeStack[i] = stdoutPipe 127 | } 128 | commands[i].Stdout = &outputBuffer 129 | commands[i].Stderr = &errorBuffer 130 | 131 | if err := call(commands, pipeStack); err != nil { 132 | logger.Errorf(string(errorBuffer.Bytes()), err) 133 | return nil, err 134 | } 135 | 136 | return outputBuffer.Bytes(), nil 137 | } 138 | 139 | func getBoardName(pid string) (string, string, error) { 140 | //execPath, _ := osext.Executable() 141 | 142 | //avr := (m["arduino"].(map[string]interface{})["avr"].(map[string]interface{})["boards"]) 143 | //var uno Board 144 | //uno = avr["uno"] 145 | 146 | findAllPIDs(globalConfigMap) 147 | 148 | list, _ := searchFor(globalConfigMap, []string{"pid"}, pid) 149 | 150 | var archBoardNameSlice []string 151 | archBoardName := "" 152 | 153 | if len(list) > 0 { 154 | archBoardNameSlice = strings.Split(list[0].path, ":")[:5] 155 | archBoardName = archBoardNameSlice[1] + ":" + archBoardNameSlice[2] + ":" + archBoardNameSlice[4] 156 | } else { 157 | return "", "", nil 158 | } 159 | 160 | boardPath := append(archBoardNameSlice, "name") 161 | 162 | boardName := getElementFromMapWithList(globalConfigMap, boardPath).(string) 163 | 164 | return archBoardName, boardName, nil 165 | } 166 | 167 | func matchStringWithSlice(str string, match []string) bool { 168 | for _, elem := range match { 169 | if !strings.Contains(str, elem) { 170 | return false 171 | } 172 | } 173 | return true 174 | } 175 | 176 | func recursivelyIterateConfig(m map[string]interface{}, fullpath string, match []string, mapOut *[]ConfigProperty) { 177 | 178 | for k, v := range m { 179 | switch vv := v.(type) { 180 | case string: 181 | if matchStringWithSlice(fullpath+":"+k, match) { 182 | //fmt.Println(k, "is string", vv, "path", fullpath) 183 | if mapOut != nil { 184 | *mapOut = append(*mapOut, ConfigProperty{path: fullpath, value: vv}) 185 | //fmt.Println(getElementFromMapWithList(globalConfigMap, strings.Split(fullpath, ":"))) 186 | } 187 | } 188 | case map[string]interface{}: 189 | //fmt.Println(k, "is a map:", fullpath) 190 | recursivelyIterateConfig(m[k].(map[string]interface{}), fullpath+":"+k, match, mapOut) 191 | default: 192 | //fmt.Println(k, "is of a type I don't know how to handle ", vv) 193 | } 194 | } 195 | } 196 | 197 | func RemoveDuplicates(xs *[]string) { 198 | found := make(map[string]bool) 199 | j := 0 200 | for i, x := range *xs { 201 | if !found[x] { 202 | found[x] = true 203 | (*xs)[j] = (*xs)[i] 204 | j++ 205 | } 206 | } 207 | *xs = (*xs)[:j] 208 | } 209 | 210 | func findAllVIDs(m map[string]interface{}) []ConfigProperty { 211 | var vidList []ConfigProperty 212 | recursivelyIterateConfig(m, "", []string{"vid"}, &vidList) 213 | //fmt.Println(vidList) 214 | return vidList 215 | } 216 | 217 | func findAllPIDs(m map[string]interface{}) []ConfigProperty { 218 | var pidList []ConfigProperty 219 | recursivelyIterateConfig(m, "", []string{"pid"}, &pidList) 220 | //fmt.Println(pidList) 221 | return pidList 222 | } 223 | 224 | func searchFor(m map[string]interface{}, args []string, element string) ([]ConfigProperty, bool) { 225 | var uList []ConfigProperty 226 | var results []ConfigProperty 227 | recursivelyIterateConfig(m, "", args, &uList) 228 | //fmt.Println(uList) 229 | for _, elm := range uList { 230 | if elm.value == element { 231 | results = append(results, elm) 232 | } 233 | } 234 | return results, len(results) != 0 235 | } 236 | 237 | func getElementFromMapWithList(m map[string]interface{}, listStr []string) interface{} { 238 | var k map[string]interface{} 239 | k = m 240 | for _, element := range listStr { 241 | switch k[element].(type) { 242 | case string: 243 | return k[element] 244 | default: 245 | if element != "" { 246 | k = k[element].(map[string]interface{}) 247 | } 248 | } 249 | } 250 | return k 251 | } 252 | 253 | func createGlobalConfigMap(m *map[string]interface{}) { 254 | execPath, _ := osext.Executable() 255 | 256 | file, e := ioutil.ReadFile(filepath.Dir(execPath) + "/arduino/boards.json") 257 | 258 | if e != nil { 259 | fmt.Printf("File error: %v\n", e) 260 | os.Exit(1) 261 | } 262 | 263 | //var config Combo 264 | json.Unmarshal(file, m) 265 | } 266 | 267 | // Filter returns a new slice containing all OsSerialPort in the slice that satisfy the predicate f. 268 | func Filter(vs []OsSerialPort, f func(OsSerialPort) bool) []OsSerialPort { 269 | var vsf []OsSerialPort 270 | for _, v := range vs { 271 | if f(v) { 272 | vsf = append(vsf, v) 273 | } 274 | } 275 | return vsf 276 | } 277 | --------------------------------------------------------------------------------