├── .gitignore ├── .gitmodules ├── .project ├── .travis.yml ├── LICENSE.md ├── README.md ├── altbuild.sh ├── bufferflow.go ├── bufferflow_default.go ├── bufferflow_dummypause.go ├── bufferflow_grbl.go ├── bufferflow_marlin.go ├── bufferflow_nodemcu.go ├── bufferflow_timed.go ├── bufferflow_tinyg.go ├── bufferflow_tinyg_v2.go ├── bufferflow_tinygg2.go ├── bufferflow_tinygpktmode.go ├── bufferflow_tinygtidmode.go ├── compile_go1_5_crosscompile.sh ├── compile_spjs.sh ├── compile_webidebridge.sh ├── conn.go ├── download.go ├── drivers └── windows │ └── TinyGv2.inf ├── dummy.go ├── execprocess.go ├── feedrateoverride.go ├── gpio.go ├── gpio_linux_arm.go ├── home.html ├── hub.go ├── main.go ├── programmer.go ├── queue.go ├── queue_tid.go ├── release.sh ├── serial.go ├── seriallist.go ├── seriallist_darwin.go ├── seriallist_linux.go ├── seriallist_windows.go ├── serialport.go └── snapshot └── downloads.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | bufferflow_tinyg_old.md 3 | 4 | serial-port-json-server 5 | 6 | snapshot/* 7 | 8 | *.exe 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - tip -------------------------------------------------------------------------------- /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 | //"log" 4 | //"time" 5 | 6 | var availableBufferAlgorithms = []string{"default", "timed", "nodemcu", "tinyg", "tinyg_old", "tinyg_linemode", "tinyg_tidmode", "tinygg2", "grbl", "marlin"} 7 | 8 | //var availableBufferAlgorithms = []string{"default", "tinyg", "tinygg2", "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, string) // 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 | GetManualPaused() bool 27 | SetManualPaused(isPaused bool) 28 | SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool // implement this method 29 | SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool // implement this method 30 | SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool // implement this method 31 | SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool // implement this method 32 | SeeIfSpecificCommandsReturnNoResponse(cmd string) bool // implement this method 33 | ReleaseLock() // implement this method 34 | IsBufferGloballySendingBackIncomingData() bool // implement this method 35 | Close() // implement this method 36 | RewriteSerialData(cmd string, id string) string // implement this method 37 | } 38 | 39 | /*data packets returned to client*/ 40 | type DataCmdComplete struct { 41 | Cmd string 42 | Id string 43 | P string 44 | BufSize int `json:"-"` 45 | D string `json:"-"` 46 | } 47 | 48 | type DataPerLine struct { 49 | P string 50 | D string 51 | } 52 | -------------------------------------------------------------------------------- /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) RewriteSerialData(cmd string, id string) string { 22 | return "" 23 | } 24 | 25 | func (b *BufferflowDefault) BlockUntilReady(cmd string, id string) (bool, bool, string) { 26 | //log.Printf("BlockUntilReady() start\n") 27 | return true, false, "" 28 | } 29 | 30 | func (b *BufferflowDefault) OnIncomingData(data string) { 31 | //log.Printf("OnIncomingData() start. data:%v\n", data) 32 | } 33 | 34 | // Clean out b.sem so it can truly block 35 | func (b *BufferflowDefault) ClearOutSemaphore() { 36 | } 37 | 38 | func (b *BufferflowDefault) BreakApartCommands(cmd string) []string { 39 | return []string{cmd} 40 | } 41 | 42 | func (b *BufferflowDefault) Pause() { 43 | return 44 | } 45 | 46 | func (b *BufferflowDefault) Unpause() { 47 | return 48 | } 49 | 50 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 51 | return false 52 | } 53 | 54 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 55 | return false 56 | } 57 | 58 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 59 | return false 60 | } 61 | 62 | func (b *BufferflowDefault) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 63 | return false 64 | } 65 | 66 | func (b *BufferflowDefault) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 67 | return false 68 | } 69 | 70 | func (b *BufferflowDefault) ReleaseLock() { 71 | } 72 | 73 | func (b *BufferflowDefault) IsBufferGloballySendingBackIncomingData() bool { 74 | return false 75 | } 76 | 77 | func (b *BufferflowDefault) Close() { 78 | } 79 | 80 | func (b *BufferflowDefault) GetManualPaused() bool { 81 | return false 82 | } 83 | 84 | func (b *BufferflowDefault) SetManualPaused(isPaused bool) { 85 | } 86 | -------------------------------------------------------------------------------- /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) RewriteSerialData(cmd string, id string) string { 19 | return "" 20 | } 21 | 22 | func (b *BufferflowDummypause) BlockUntilReady(cmd string, id string) (bool, bool, string) { 23 | log.Printf("BlockUntilReady() start. numLines:%v\n", b.NumLines) 24 | log.Printf("buffer:%v\n", b) 25 | //for b.Paused { 26 | log.Println("We are paused for 3 seconds. Yeilding send.") 27 | time.Sleep(3000 * time.Millisecond) 28 | //} 29 | log.Printf("BlockUntilReady() end\n") 30 | return true, false, "" 31 | } 32 | 33 | func (b *BufferflowDummypause) OnIncomingData(data string) { 34 | log.Printf("OnIncomingData() start. data:%v\n", data) 35 | b.NumLines++ 36 | //time.Sleep(3000 * time.Millisecond) 37 | log.Printf("OnIncomingData() end. numLines:%v\n", b.NumLines) 38 | } 39 | 40 | // Clean out b.sem so it can truly block 41 | func (b *BufferflowDummypause) ClearOutSemaphore() { 42 | } 43 | 44 | func (b *BufferflowDummypause) BreakApartCommands(cmd string) []string { 45 | return []string{cmd} 46 | } 47 | 48 | func (b *BufferflowDummypause) Pause() { 49 | return 50 | } 51 | 52 | func (b *BufferflowDummypause) Unpause() { 53 | return 54 | } 55 | 56 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 57 | return false 58 | } 59 | 60 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 61 | return false 62 | } 63 | 64 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 65 | return false 66 | } 67 | 68 | func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 69 | return false 70 | } 71 | 72 | func (b *BufferflowDummypause) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 73 | /* 74 | // remove comments 75 | cmd = b.reComment.ReplaceAllString(cmd, "") 76 | cmd = b.reComment2.ReplaceAllString(cmd, "") 77 | if match := b.reNoResponse.MatchString(cmd); match { 78 | log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) 79 | return true 80 | } 81 | */ 82 | return false 83 | } 84 | 85 | func (b *BufferflowDummypause) ReleaseLock() { 86 | } 87 | 88 | func (b *BufferflowDummypause) IsBufferGloballySendingBackIncomingData() bool { 89 | return false 90 | } 91 | 92 | func (b *BufferflowDummypause) Close() { 93 | } 94 | 95 | func (b *BufferflowDummypause) GetManualPaused() bool { 96 | return false 97 | } 98 | 99 | func (b *BufferflowDummypause) SetManualPaused(isPaused bool) { 100 | } 101 | -------------------------------------------------------------------------------- /bufferflow_grbl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 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) RewriteSerialData(cmd string, id string) string { 63 | return "" 64 | } 65 | 66 | func (b *BufferflowGrbl) BlockUntilReady(cmd string, id string) (bool, bool, string) { 67 | log.Printf("BlockUntilReady() start\n") 68 | 69 | b.q.Push(cmd, id) 70 | 71 | log.Printf("New line length: %v, buffer size increased to:%v\n", len(cmd), b.q.LenOfCmds()) 72 | log.Println(b.q) 73 | 74 | if b.q.LenOfCmds() >= b.BufferMax { 75 | b.SetPaused(true, 0) 76 | log.Printf("Buffer Full - Will send this command when space is available") 77 | } 78 | 79 | if b.GetPaused() { 80 | log.Println("It appears we are being asked to pause, so we will wait on b.sem") 81 | // We are being asked to pause our sending of commands 82 | 83 | // clear all b.sem signals so when we block below, we truly block 84 | b.ClearOutSemaphore() 85 | 86 | log.Println("Blocking on b.sem until told from OnIncomingData to go") 87 | unblockType, ok := <-b.sem // will block until told from OnIncomingData to go 88 | 89 | log.Printf("Done blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) 90 | 91 | // we get an unblockType of 1 for normal unblocks 92 | // we get an unblockType of 2 when we're being asked to wipe the buffer, i.e. from a % cmd 93 | if unblockType == 2 { 94 | log.Println("This was an unblock of type 2, which means we're being asked to wipe internal buffer. so return false.") 95 | // returning false asks the calling method to wipe the serial send once 96 | // this function returns 97 | return false, false, "" 98 | } 99 | 100 | log.Printf("BlockUntilReady(cmd:%v, id:%v) end\n", cmd, id) 101 | } 102 | return true, true, "" 103 | } 104 | 105 | func (b *BufferflowGrbl) OnIncomingData(data string) { 106 | log.Printf("OnIncomingData() start. data:%q\n", data) 107 | 108 | b.LatestData += data 109 | 110 | //it was found ok was only received with status responses until the grbl buffer is full. 111 | //b.LatestData = regexp.MustCompile(">\\r\\nok").ReplaceAllString(b.LatestData, ">") //remove oks from status responses 112 | 113 | arrLines := b.reNewLine.Split(b.LatestData, -1) 114 | log.Printf("arrLines:%v\n", arrLines) 115 | 116 | if len(arrLines) > 1 { 117 | // that means we found a newline and have 2 or greater array values 118 | // so we need to analyze our arrLines[] lines but keep last line 119 | // for next trip into OnIncomingData 120 | log.Printf("We have data lines to analyze. numLines:%v\n", len(arrLines)) 121 | 122 | } else { 123 | // we don't have a newline yet, so just exit and move on 124 | // we don't have to reset b.LatestData because we ended up 125 | // without any newlines so maybe we will next time into this method 126 | log.Printf("Did not find newline yet, so nothing to analyze\n") 127 | return 128 | } 129 | 130 | // if we made it here we have lines to analyze 131 | // so analyze all of them except the last line 132 | for index, element := range arrLines[:len(arrLines)-1] { 133 | log.Printf("Working on element:%v, index:%v", element, index) 134 | 135 | //check for 'ok' or 'error' response indicating a gcode line has been processed 136 | if b.ok.MatchString(element) || b.err.MatchString(element) { 137 | if b.q.Len() > 0 { 138 | doneCmd, id := b.q.Poll() 139 | 140 | if b.ok.MatchString(element) { 141 | // Send cmd:"Complete" back 142 | m := DataCmdComplete{"Complete", id, b.Port, b.q.LenOfCmds(), doneCmd} 143 | bm, err := json.Marshal(m) 144 | if err == nil { 145 | h.broadcastSys <- bm 146 | } 147 | } else if b.err.MatchString(element) { 148 | // Send cmd:"Error" back 149 | log.Printf("Error Response Received:%v, id:%v", doneCmd, id) 150 | m := DataCmdComplete{"Error", id, b.Port, b.q.LenOfCmds(), doneCmd} 151 | bm, err := json.Marshal(m) 152 | if err == nil { 153 | h.broadcastSys <- bm 154 | } 155 | } 156 | 157 | log.Printf("Buffer decreased to itemCnt:%v, lenOfBuf:%v\n", b.q.Len(), b.q.LenOfCmds()) 158 | } else { 159 | 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!!!!") 160 | } 161 | 162 | if b.q.LenOfCmds() < b.BufferMax { 163 | 164 | log.Printf("Grbl just completed a line of gcode\n") 165 | 166 | // if we are paused, tell us to unpause cuz we have clean buffer room now 167 | if b.GetPaused() { 168 | b.SetPaused(false, 1) 169 | } 170 | } 171 | 172 | //check for the grbl init line indicating the arduino is ready to accept commands 173 | //could also pull version from this string, if we find a need for that later 174 | } else if b.initline.MatchString(element) { 175 | //grbl init line received, clear anything from current buffer and unpause 176 | b.LocalBufferWipe(b.parent_serport) 177 | 178 | //unpause buffer but wipe the command in the queue as grbl has restarted. 179 | if b.GetPaused() { 180 | b.SetPaused(false, 2) 181 | } 182 | 183 | b.version = element //save element in version 184 | 185 | //Check for report output, compare to last report output, if different return to client to update status; otherwise ignore status. 186 | } else if b.rpt.MatchString(element) { 187 | if element == b.LastStatus { 188 | log.Println("Grbl status has not changed, not reporting to client") 189 | continue //skip this element as the cnc position has not changed, and move on to the next element. 190 | } 191 | 192 | b.LastStatus = element //if we make it here something has changed with the status string and laststatus needs updating 193 | } 194 | 195 | // handle communication back to client 196 | m := DataPerLine{b.Port, element + "\n"} 197 | bm, err := json.Marshal(m) 198 | if err == nil { 199 | h.broadcastSys <- bm 200 | } 201 | 202 | } // for loop 203 | 204 | // now wipe the LatestData to only have the last line that we did not analyze 205 | // because we didn't know/think that was a full command yet 206 | b.LatestData = arrLines[len(arrLines)-1] 207 | 208 | //time.Sleep(3000 * time.Millisecond) 209 | log.Printf("OnIncomingData() end.\n") 210 | } 211 | 212 | // Clean out b.sem so it can truly block 213 | func (b *BufferflowGrbl) ClearOutSemaphore() { 214 | ctr := 0 215 | 216 | keepLooping := true 217 | for keepLooping { 218 | select { 219 | case d, ok := <-b.sem: 220 | log.Printf("Consuming b.sem queue to clear it before we block. ok:%v, d:%v\n", ok, string(d)) 221 | ctr++ 222 | if ok == false { 223 | keepLooping = false 224 | } 225 | default: 226 | keepLooping = false 227 | log.Println("Hit default in select clause") 228 | } 229 | } 230 | log.Printf("Done consuming b.sem queue so we're good to block on it now. ctr:%v\n", ctr) 231 | // ok, all b.sem signals are now consumed into la-la land 232 | } 233 | 234 | func (b *BufferflowGrbl) BreakApartCommands(cmd string) []string { 235 | 236 | // add newline after !~% 237 | log.Printf("Command Before Break-Apart: %q\n", cmd) 238 | 239 | cmds := strings.Split(cmd, "\n") 240 | finalCmds := []string{} 241 | for _, item := range cmds { 242 | //remove comments and whitespace from item 243 | item = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(item, "") 244 | item = regexp.MustCompile(";.*").ReplaceAllString(item, "") 245 | item = strings.Replace(item, " ", "", -1) 246 | 247 | if item == "*init*" { //return init string to update grbl widget when already connected to grbl 248 | m := DataPerLine{b.Port, b.version + "\n"} 249 | bm, err := json.Marshal(m) 250 | if err == nil { 251 | h.broadcastSys <- bm 252 | } 253 | } else if item == "*status*" { //return status when client first connects to existing open port 254 | m := DataPerLine{b.Port, b.LastStatus + "\n"} 255 | bm, err := json.Marshal(m) 256 | if err == nil { 257 | h.broadcastSys <- bm 258 | } 259 | } else if item == "?" { 260 | log.Printf("Query added without newline: %q\n", item) 261 | finalCmds = append(finalCmds, item) //append query request without newline character 262 | } else if item == "%" { 263 | log.Printf("Wiping Grbl BufferFlow") 264 | b.LocalBufferWipe(b.parent_serport) 265 | //dont add this command to the list of finalCmds 266 | } else if item != "" { 267 | log.Printf("Re-adding newline to item:%v\n", item) 268 | s := item + "\n" 269 | finalCmds = append(finalCmds, s) 270 | log.Printf("New cmd item:%v\n", s) 271 | } 272 | 273 | } 274 | log.Printf("Final array of cmds after BreakApartCommands(). finalCmds:%v\n", finalCmds) 275 | 276 | return finalCmds 277 | //return []string{cmd} //do not process string 278 | } 279 | 280 | func (b *BufferflowGrbl) Pause() { 281 | b.SetPaused(true, 0) 282 | //b.BypassMode = false // turn off bypassmode in case it's on 283 | log.Println("Paused buffer on next BlockUntilReady() call") 284 | } 285 | 286 | func (b *BufferflowGrbl) Unpause() { 287 | //unpause buffer by setting paused to false and passing a 1 to b.sem 288 | b.SetPaused(false, 1) 289 | log.Println("Unpaused buffer inside BlockUntilReady() call") 290 | } 291 | 292 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 293 | // remove comments 294 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 295 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 296 | if match, _ := regexp.MatchString("[!~\\?]|(\u0018)", cmd); match { 297 | log.Printf("Found cmd that should skip buffer. cmd:%v\n", cmd) 298 | return true 299 | } 300 | return false 301 | } 302 | 303 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 304 | // remove comments 305 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 306 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 307 | if match, _ := regexp.MatchString("[!]", cmd); match { 308 | log.Printf("Found cmd that should pause buffer. cmd:%v\n", cmd) 309 | return true 310 | } 311 | return false 312 | } 313 | 314 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 315 | 316 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 317 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 318 | if match, _ := regexp.MatchString("[~]", cmd); match { 319 | log.Printf("Found cmd that should unpause buffer. cmd:%v\n", cmd) 320 | return true 321 | } 322 | return false 323 | } 324 | 325 | func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 326 | 327 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 328 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 329 | if match, _ := regexp.MatchString("(\u0018)", cmd); match { 330 | log.Printf("Found cmd that should wipe out and reset buffer. cmd:%v\n", cmd) 331 | 332 | //b.q.Delete() //delete tracking queue, all buffered commands will be wiped. 333 | 334 | //log.Println("Buffer variables cleared for new input.") 335 | return true 336 | } 337 | return false 338 | } 339 | 340 | func (b *BufferflowGrbl) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 341 | /* 342 | // remove comments 343 | cmd = b.reComment.ReplaceAllString(cmd, "") 344 | cmd = b.reComment2.ReplaceAllString(cmd, "") 345 | if match := b.reNoResponse.MatchString(cmd); match { 346 | log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) 347 | return true 348 | } 349 | */ 350 | return false 351 | } 352 | 353 | func (b *BufferflowGrbl) ReleaseLock() { 354 | log.Println("Lock being released in GRBL buffer") 355 | 356 | b.q.Delete() 357 | 358 | log.Println("ReleaseLock(), so we will send signal of 2 to b.sem to unpause the BlockUntilReady() thread") 359 | 360 | //release lock, send signal 2 to b.sem 361 | b.SetPaused(false, 2) 362 | } 363 | 364 | func (b *BufferflowGrbl) IsBufferGloballySendingBackIncomingData() bool { 365 | //telling json server that we are handling client responses 366 | return true 367 | } 368 | 369 | //Use this function to open a connection, write directly to serial port and close connection. 370 | //This is used for sending query requests outside of the normal buffered operations that will pause to wait for room in the grbl buffer 371 | //'?' is asynchronous to the normal buffer load and does not need to be paused when buffer full 372 | func (b *BufferflowGrbl) rptQueryLoop(p *serport) { 373 | b.parent_serport = p //make note of this port for use in clearing the buffer later, on error. 374 | ticker := time.NewTicker(250 * time.Millisecond) 375 | b.quit = make(chan int) 376 | go func() { 377 | for { 378 | select { 379 | case <-ticker.C: 380 | 381 | n2, err := p.portIo.Write([]byte("?")) 382 | 383 | log.Print("Just wrote ", n2, " bytes to serial: ?") 384 | 385 | if err != nil { 386 | errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." 387 | log.Print(errstr) 388 | h.broadcastSys <- []byte(errstr) 389 | ticker.Stop() //stop query loop if we can't write to the port 390 | break 391 | } 392 | case <-b.quit: 393 | ticker.Stop() 394 | return 395 | } 396 | } 397 | }() 398 | } 399 | 400 | func (b *BufferflowGrbl) Close() { 401 | //stop the status query loop when the serial port is closed off. 402 | log.Println("Stopping the status query loop") 403 | b.quit <- 1 404 | } 405 | 406 | // Gets the paused state of this buffer 407 | // go-routine safe. 408 | func (b *BufferflowGrbl) GetPaused() bool { 409 | b.lock.Lock() 410 | defer b.lock.Unlock() 411 | return b.Paused 412 | } 413 | 414 | // Sets the paused state of this buffer 415 | // go-routine safe. 416 | func (b *BufferflowGrbl) SetPaused(isPaused bool, semRelease int) { 417 | b.lock.Lock() 418 | defer b.lock.Unlock() 419 | b.Paused = isPaused 420 | 421 | //if we are unpausing the buffer, we need to send a signal to release the channel 422 | if isPaused == false { 423 | go func() { 424 | // sending a 2 asks BlockUntilReady() to cancel the send 425 | b.sem <- semRelease 426 | defer func() { 427 | log.Printf("Unpause Semaphore just got consumed by the BlockUntilReady()\n") 428 | }() 429 | }() 430 | } 431 | } 432 | 433 | //local version of buffer wipe loop needed to handle pseudo clear buffer (%) without passing that value on to 434 | func (b *BufferflowGrbl) LocalBufferWipe(p *serport) { 435 | log.Printf("Pseudo command received to wipe grbl buffer but *not* send on to grbl controller.") 436 | 437 | // consume all stuff queued 438 | func() { 439 | ctr := 0 440 | 441 | keepLooping := true 442 | for keepLooping { 443 | select { 444 | case d, ok := <-p.sendBuffered: 445 | log.Printf("Consuming sendBuffered queue. ok:%v, d:%v, id:%v\n", ok, string(d.data), string(d.id)) 446 | ctr++ 447 | 448 | p.itemsInBuffer-- 449 | if ok == false { 450 | keepLooping = false 451 | } 452 | default: 453 | keepLooping = false 454 | log.Println("Hit default in select clause") 455 | } 456 | } 457 | log.Printf("Done consuming sendBuffered cmds. ctr:%v\n", ctr) 458 | }() 459 | 460 | b.ReleaseLock() 461 | 462 | // let user know we wiped queue 463 | log.Printf("itemsInBuffer:%v\n", p.itemsInBuffer) 464 | h.broadcastSys <- []byte("{\"Cmd\":\"WipedQueue\",\"QCnt\":" + strconv.Itoa(p.itemsInBuffer) + ",\"Port\":\"" + p.portConf.Name + "\"}") 465 | } 466 | 467 | func (b *BufferflowGrbl) GetManualPaused() bool { 468 | return false 469 | } 470 | 471 | func (b *BufferflowGrbl) SetManualPaused(isPaused bool) { 472 | } 473 | -------------------------------------------------------------------------------- /bufferflow_marlin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type BufferflowMarlin 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 *BufferflowMarlin) Init() { 39 | b.lock = &sync.Mutex{} 40 | b.SetPaused(false, 1) 41 | 42 | log.Println("Initting MARLIN 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("^echo:Marlin") 55 | b.qry, _ = regexp.Compile("\\?") 56 | b.rpt, _ = regexp.Compile("^X:") 57 | 58 | //initialize query loop 59 | b.rptQueryLoop(b.parent_serport) // Disable the query loop 60 | } 61 | 62 | func (b *BufferflowMarlin) RewriteSerialData(cmd string, id string) string { 63 | return "" 64 | } 65 | 66 | func (b *BufferflowMarlin) BlockUntilReady(cmd string, id string) (bool, bool, string) { 67 | log.Printf("BlockUntilReady() start\n") 68 | 69 | b.q.Push(cmd, id) 70 | 71 | log.Printf("New line length: %v, buffer size increased to:%v\n", len(cmd), b.q.LenOfCmds()) 72 | log.Println(b.q) 73 | 74 | if b.q.LenOfCmds() >= b.BufferMax { 75 | b.SetPaused(true, 0) 76 | log.Printf("Buffer Full - Will send this command when space is available") 77 | } 78 | 79 | if b.GetPaused() { 80 | log.Println("It appears we are being asked to pause, so we will wait on b.sem") 81 | // We are being asked to pause our sending of commands 82 | 83 | // clear all b.sem signals so when we block below, we truly block 84 | b.ClearOutSemaphore() 85 | 86 | log.Println("Blocking on b.sem until told from OnIncomingData to go") 87 | unblockType, ok := <-b.sem // will block until told from OnIncomingData to go 88 | 89 | log.Printf("Done blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) 90 | 91 | // we get an unblockType of 1 for normal unblocks 92 | // we get an unblockType of 2 when we're being asked to wipe the buffer, i.e. from a % cmd 93 | if unblockType == 2 { 94 | log.Println("This was an unblock of type 2, which means we're being asked to wipe internal buffer. so return false.") 95 | // returning false asks the calling method to wipe the serial send once 96 | // this function returns 97 | return false, false, "" 98 | } 99 | 100 | log.Printf("BlockUntilReady(cmd:%v, id:%v) end\n", cmd, id) 101 | } 102 | return true, true, "" 103 | } 104 | 105 | func (b *BufferflowMarlin) OnIncomingData(data string) { 106 | log.Printf("OnIncomingData() start. data:%q\n", data) 107 | 108 | b.LatestData += data 109 | 110 | //it was found ok was only received with status responses until the MARLIN buffer is full. 111 | //b.LatestData = regexp.MustCompile(">\\r\\nok").ReplaceAllString(b.LatestData, ">") //remove oks from status responses 112 | 113 | arrLines := b.reNewLine.Split(b.LatestData, -1) 114 | log.Printf("arrLines:%v\n", arrLines) 115 | 116 | if len(arrLines) > 1 { 117 | // that means we found a newline and have 2 or greater array values 118 | // so we need to analyze our arrLines[] lines but keep last line 119 | // for next trip into OnIncomingData 120 | log.Printf("We have data lines to analyze. numLines:%v\n", len(arrLines)) 121 | 122 | } else { 123 | // we don't have a newline yet, so just exit and move on 124 | // we don't have to reset b.LatestData because we ended up 125 | // without any newlines so maybe we will next time into this method 126 | log.Printf("Did not find newline yet, so nothing to analyze\n") 127 | return 128 | } 129 | 130 | // if we made it here we have lines to analyze 131 | // so analyze all of them except the last line 132 | for index, element := range arrLines[:len(arrLines)-1] { 133 | log.Printf("Working on element:%v, index:%v", element, index) 134 | 135 | //check for 'ok' or 'error' response indicating a gcode line has been processed 136 | if b.ok.MatchString(element) || b.err.MatchString(element) { 137 | if b.q.Len() > 0 { 138 | doneCmd, id := b.q.Poll() 139 | 140 | if b.ok.MatchString(element) { 141 | // Send cmd:"Complete" back 142 | m := DataCmdComplete{"Complete", id, b.Port, b.q.LenOfCmds(), doneCmd} 143 | bm, err := json.Marshal(m) 144 | if err == nil { 145 | h.broadcastSys <- bm 146 | } 147 | } else if b.err.MatchString(element) { 148 | // Send cmd:"Error" back 149 | log.Printf("Error Response Received:%v, id:%v", doneCmd, id) 150 | m := DataCmdComplete{"Error", id, b.Port, b.q.LenOfCmds(), doneCmd} 151 | bm, err := json.Marshal(m) 152 | if err == nil { 153 | h.broadcastSys <- bm 154 | } 155 | } 156 | 157 | log.Printf("Buffer decreased to itemCnt:%v, lenOfBuf:%v\n", b.q.Len(), b.q.LenOfCmds()) 158 | } else { 159 | 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!!!!") 160 | } 161 | 162 | if b.q.LenOfCmds() < b.BufferMax { 163 | 164 | log.Printf("Marlin just completed a line of gcode\n") 165 | 166 | // if we are paused, tell us to unpause cuz we have clean buffer room now 167 | if b.GetPaused() { 168 | b.SetPaused(false, 1) 169 | } 170 | } 171 | 172 | //check for the marlin init line indicating the arduino is ready to accept commands 173 | //could also pull version from this string, if we find a need for that later 174 | } else if b.initline.MatchString(element) { 175 | //marlin init line received, clear anything from current buffer and unpause 176 | b.LocalBufferWipe(b.parent_serport) 177 | 178 | //unpause buffer but wipe the command in the queue as marlin has restarted. 179 | if b.GetPaused() { 180 | b.SetPaused(false, 2) 181 | } 182 | 183 | b.version = element //save element in version 184 | 185 | //Check for report output, compare to last report output, if different return to client to update status; otherwise ignore status. 186 | } else if b.rpt.MatchString(element) { 187 | if element == b.LastStatus { 188 | log.Println("Marlin status has not changed, not reporting to client") 189 | continue //skip this element as the cnc position has not changed, and move on to the next element. 190 | } 191 | 192 | b.LastStatus = element //if we make it here something has changed with the status string and laststatus needs updating 193 | } 194 | 195 | // handle communication back to client 196 | m := DataPerLine{b.Port, element + "\n"} 197 | bm, err := json.Marshal(m) 198 | if err == nil { 199 | h.broadcastSys <- bm 200 | } 201 | 202 | } // for loop 203 | 204 | // now wipe the LatestData to only have the last line that we did not analyze 205 | // because we didn't know/think that was a full command yet 206 | b.LatestData = arrLines[len(arrLines)-1] 207 | 208 | //time.Sleep(3000 * time.Millisecond) 209 | log.Printf("OnIncomingData() end.\n") 210 | } 211 | 212 | // Clean out b.sem so it can truly block 213 | func (b *BufferflowMarlin) ClearOutSemaphore() { 214 | ctr := 0 215 | 216 | keepLooping := true 217 | for keepLooping { 218 | select { 219 | case d, ok := <-b.sem: 220 | log.Printf("Consuming b.sem queue to clear it before we block. ok:%v, d:%v\n", ok, string(d)) 221 | ctr++ 222 | if ok == false { 223 | keepLooping = false 224 | } 225 | default: 226 | keepLooping = false 227 | log.Println("Hit default in select clause") 228 | } 229 | } 230 | log.Printf("Done consuming b.sem queue so we're good to block on it now. ctr:%v\n", ctr) 231 | // ok, all b.sem signals are now consumed into la-la land 232 | } 233 | 234 | func (b *BufferflowMarlin) BreakApartCommands(cmd string) []string { 235 | 236 | // add newline after !~% 237 | log.Printf("Command Before Break-Apart: %q\n", cmd) 238 | 239 | cmds := strings.Split(cmd, "\n") 240 | finalCmds := []string{} 241 | for _, item := range cmds { 242 | //remove comments and whitespace from item 243 | item = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(item, "") 244 | item = regexp.MustCompile(";.*").ReplaceAllString(item, "") 245 | item = strings.Replace(item, " ", "", -1) 246 | 247 | if item == "*init*" { //return init string to update marlin widget when already connected to marlin 248 | m := DataPerLine{b.Port, b.version + "\n"} 249 | bm, err := json.Marshal(m) 250 | if err == nil { 251 | h.broadcastSys <- bm 252 | } 253 | } else if item == "*status*" { //return status when client first connects to existing open port 254 | m := DataPerLine{b.Port, b.LastStatus + "\n"} 255 | bm, err := json.Marshal(m) 256 | if err == nil { 257 | h.broadcastSys <- bm 258 | } 259 | } else if item == "?" { 260 | log.Printf("Query added without newline: %q\n", item) 261 | finalCmds = append(finalCmds, item) //append query request without newline character 262 | } else if item == "%" { 263 | log.Printf("Wiping Marlin BufferFlow") 264 | b.LocalBufferWipe(b.parent_serport) 265 | //dont add this command to the list of finalCmds 266 | } else if item != "" { 267 | log.Printf("Re-adding newline to item:%v\n", item) 268 | s := item + "\n" 269 | finalCmds = append(finalCmds, s) 270 | log.Printf("New cmd item:%v\n", s) 271 | } 272 | 273 | } 274 | log.Printf("Final array of cmds after BreakApartCommands(). finalCmds:%v\n", finalCmds) 275 | 276 | return finalCmds 277 | //return []string{cmd} //do not process string 278 | } 279 | 280 | func (b *BufferflowMarlin) Pause() { 281 | b.SetPaused(true, 0) 282 | //b.BypassMode = false // turn off bypassmode in case it's on 283 | log.Println("Paused buffer on next BlockUntilReady() call") 284 | } 285 | 286 | func (b *BufferflowMarlin) Unpause() { 287 | //unpause buffer by setting paused to false and passing a 1 to b.sem 288 | b.SetPaused(false, 1) 289 | log.Println("Unpaused buffer inside BlockUntilReady() call") 290 | } 291 | 292 | func (b *BufferflowMarlin) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 293 | // remove comments 294 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 295 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 296 | if match, _ := regexp.MatchString("[!~\\?]|(\u0018)", cmd); match { 297 | log.Printf("Found cmd that should skip buffer. cmd:%v\n", cmd) 298 | return true 299 | } 300 | return false 301 | } 302 | 303 | func (b *BufferflowMarlin) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 304 | // remove comments 305 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 306 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 307 | if match, _ := regexp.MatchString("[!]", cmd); match { 308 | log.Printf("Found cmd that should pause buffer. cmd:%v\n", cmd) 309 | return true 310 | } 311 | return false 312 | } 313 | 314 | func (b *BufferflowMarlin) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 315 | 316 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 317 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 318 | if match, _ := regexp.MatchString("[~]", cmd); match { 319 | log.Printf("Found cmd that should unpause buffer. cmd:%v\n", cmd) 320 | return true 321 | } 322 | return false 323 | } 324 | 325 | func (b *BufferflowMarlin) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 326 | 327 | //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") 328 | //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") 329 | if match, _ := regexp.MatchString("(\u0018)", cmd); match { 330 | log.Printf("Found cmd that should wipe out and reset buffer. cmd:%v\n", cmd) 331 | 332 | //b.q.Delete() //delete tracking queue, all buffered commands will be wiped. 333 | 334 | //log.Println("Buffer variables cleared for new input.") 335 | return true 336 | } 337 | return false 338 | } 339 | 340 | func (b *BufferflowMarlin) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 341 | /* 342 | // remove comments 343 | cmd = b.reComment.ReplaceAllString(cmd, "") 344 | cmd = b.reComment2.ReplaceAllString(cmd, "") 345 | if match := b.reNoResponse.MatchString(cmd); match { 346 | log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) 347 | return true 348 | } 349 | */ 350 | return false 351 | } 352 | 353 | func (b *BufferflowMarlin) ReleaseLock() { 354 | log.Println("Lock being released in Marlin buffer") 355 | 356 | b.q.Delete() 357 | 358 | log.Println("ReleaseLock(), so we will send signal of 2 to b.sem to unpause the BlockUntilReady() thread") 359 | 360 | //release lock, send signal 2 to b.sem 361 | b.SetPaused(false, 2) 362 | } 363 | 364 | func (b *BufferflowMarlin) IsBufferGloballySendingBackIncomingData() bool { 365 | //telling json server that we are handling client responses 366 | return true 367 | } 368 | 369 | //Use this function to open a connection, write directly to serial port and close connection. 370 | //This is used for sending query requests outside of the normal buffered operations that will pause to wait for room in the marlin buffer 371 | //'?' is asynchronous to the normal buffer load and does not need to be paused when buffer full 372 | func (b *BufferflowMarlin) rptQueryLoop(p *serport) { 373 | b.parent_serport = p //make note of this port for use in clearing the buffer later, on error. 374 | ticker := time.NewTicker(2000 * time.Millisecond) 375 | b.quit = make(chan int) 376 | go func() { 377 | for { 378 | select { 379 | case <-ticker.C: 380 | 381 | n2, err := p.portIo.Write([]byte("M114")) 382 | 383 | log.Print("Just wrote ", n2, " bytes to serial: M114") 384 | 385 | if err != nil { 386 | errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." 387 | log.Print(errstr) 388 | h.broadcastSys <- []byte(errstr) 389 | ticker.Stop() //stop query loop if we can't write to the port 390 | break 391 | } 392 | case <-b.quit: 393 | ticker.Stop() 394 | return 395 | } 396 | } 397 | }() 398 | } 399 | 400 | func (b *BufferflowMarlin) Close() { 401 | //stop the status query loop when the serial port is closed off. 402 | log.Println("Stopping the status query loop") 403 | b.quit <- 1 404 | } 405 | 406 | // Gets the paused state of this buffer 407 | // go-routine safe. 408 | func (b *BufferflowMarlin) GetPaused() bool { 409 | b.lock.Lock() 410 | defer b.lock.Unlock() 411 | return b.Paused 412 | } 413 | 414 | // Sets the paused state of this buffer 415 | // go-routine safe. 416 | func (b *BufferflowMarlin) SetPaused(isPaused bool, semRelease int) { 417 | b.lock.Lock() 418 | defer b.lock.Unlock() 419 | b.Paused = isPaused 420 | 421 | //if we are unpausing the buffer, we need to send a signal to release the channel 422 | if isPaused == false { 423 | go func() { 424 | // sending a 2 asks BlockUntilReady() to cancel the send 425 | b.sem <- semRelease 426 | defer func() { 427 | log.Printf("Unpause Semaphore just got consumed by the BlockUntilReady()\n") 428 | }() 429 | }() 430 | } 431 | } 432 | 433 | //local version of buffer wipe loop needed to handle pseudo clear buffer (%) without passing that value on to 434 | func (b *BufferflowMarlin) LocalBufferWipe(p *serport) { 435 | log.Printf("Pseudo command received to wipe marlin buffer but *not* send on to marlin controller.") 436 | 437 | // consume all stuff queued 438 | func() { 439 | ctr := 0 440 | 441 | keepLooping := true 442 | for keepLooping { 443 | select { 444 | case d, ok := <-p.sendBuffered: 445 | log.Printf("Consuming sendBuffered queue. ok:%v, d:%v, id:%v\n", ok, string(d.data), string(d.id)) 446 | ctr++ 447 | 448 | p.itemsInBuffer-- 449 | if ok == false { 450 | keepLooping = false 451 | } 452 | default: 453 | keepLooping = false 454 | log.Println("Hit default in select clause") 455 | } 456 | } 457 | log.Printf("Done consuming sendBuffered cmds. ctr:%v\n", ctr) 458 | }() 459 | 460 | b.ReleaseLock() 461 | 462 | // let user know we wiped queue 463 | log.Printf("itemsInBuffer:%v\n", p.itemsInBuffer) 464 | h.broadcastSys <- []byte("{\"Cmd\":\"WipedQueue\",\"QCnt\":" + strconv.Itoa(p.itemsInBuffer) + ",\"Port\":\"" + p.portConf.Name + "\"}") 465 | } 466 | 467 | func (b *BufferflowMarlin) GetManualPaused() bool { 468 | return false 469 | } 470 | 471 | func (b *BufferflowMarlin) SetManualPaused(isPaused bool) { 472 | } 473 | -------------------------------------------------------------------------------- /bufferflow_nodemcu.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "regexp" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type BufferflowNodeMcu struct { 13 | Name string 14 | Port string 15 | //Output chan []byte 16 | Input chan string 17 | ticker *time.Ticker 18 | IsOpen bool 19 | bufferedOutput string 20 | reNewLine *regexp.Regexp 21 | reCmdDone *regexp.Regexp 22 | // additional lock for BlockUntilReady vs OnIncomingData method 23 | inOutLock *sync.Mutex 24 | q *Queue 25 | sem chan int // semaphore to wait on until given release 26 | Paused bool 27 | ManualPaused bool 28 | lock *sync.Mutex 29 | manualLock *sync.Mutex 30 | BufferMax int 31 | } 32 | 33 | func (b *BufferflowNodeMcu) Init() { 34 | log.Println("Initting timed buffer flow (output once every 16ms)") 35 | b.bufferedOutput = "" 36 | b.IsOpen = true 37 | b.reNewLine, _ = regexp.Compile("\\r{0,1}\\n") 38 | b.inOutLock = &sync.Mutex{} 39 | 40 | b.q = NewQueue() 41 | // when we get a > response we know a line was processed 42 | b.reCmdDone, _ = regexp.Compile("^(>|stdin:|=)") 43 | b.sem = make(chan int, 1000) 44 | b.Paused = false 45 | b.ManualPaused = false 46 | b.lock = &sync.Mutex{} 47 | b.manualLock = &sync.Mutex{} 48 | b.Input = make(chan string) 49 | b.BufferMax = 2 50 | 51 | go func() { 52 | for data := range b.Input { 53 | 54 | //log.Printf("Got to b.Input chan loop. data:%v\n", data) 55 | 56 | // Lock the packet ctr at start and then end 57 | b.inOutLock.Lock() 58 | 59 | b.bufferedOutput = b.bufferedOutput + data 60 | arrLines := b.reNewLine.Split(b.bufferedOutput, -1) 61 | if len(arrLines) > 1 { 62 | // that means we found a newline and have 2 or greater array values 63 | // so we need to analyze our arrLines[] lines but keep last line 64 | // for next trip into OnIncomingData 65 | //log.Printf("We have data lines to analyze. numLines:%v\n", len(arrLines)) 66 | 67 | } else { 68 | // we don't have a newline yet, so just exit and move on 69 | // we don't have to reset b.LatestData because we ended up 70 | // without any newlines so maybe we will next time into this method 71 | //log.Printf("Did not find newline yet, so nothing to analyze\n") 72 | b.inOutLock.Unlock() 73 | continue 74 | } 75 | 76 | log.Printf("Analyzing incoming data. Start.") 77 | 78 | // if we made it here we have lines to analyze 79 | // so analyze all of them except the last line 80 | for _, element := range arrLines[:len(arrLines)-1] { 81 | //log.Printf("Working on element:%v, index:%v", element, index) 82 | //log.Printf("Working on element:%v, index:%v", element) 83 | log.Printf("\t\tData:%v", element) 84 | 85 | // check if there was a reset cuz we need to wipe our buffer if there was 86 | if len(element) > 4 { 87 | bTxt := []byte(element)[len(element)-4:] 88 | bTest := []byte{14, 219, 200, 244} 89 | log.Printf("\t\ttesting two arrays\n\tbTxt :%v\n\tbTest:%v\n", bTxt, bTest) 90 | //reWasItReset := regexp.MustCompile("fffd") 91 | //if reWasItReset.MatchString(element) { 92 | if ByteArrayEquals(bTxt, bTest) { 93 | // it was reset, wipe buffer 94 | b.q.Delete() 95 | log.Printf("\t\tLooks like it was reset. We should wipe buffer.") 96 | b.SetPaused(false, 2) 97 | } 98 | } 99 | 100 | // see if it just got restarted 101 | reIsRestart := regexp.MustCompile("NodeMCU custom build by frightanic.com") 102 | if reIsRestart.MatchString(element) { 103 | // it was reset, wipe buffer 104 | b.q.Delete() 105 | log.Printf("\t\tLooks like it was reset. We should wipe buffer.") 106 | b.SetPaused(false, 2) 107 | } 108 | 109 | //check for >|stdin:|= response indicating a line has been processed 110 | if b.reCmdDone.MatchString(element) { 111 | 112 | // ok, a line has been processed, the if statement below better 113 | // be guaranteed to be true, cuz if its not we did something wrong 114 | if b.q.Len() > 0 { 115 | //b.BufferSize -= b.BufferSizeArray[0] 116 | doneCmd, id := b.q.Poll() 117 | 118 | // Send cmd:"Complete" back 119 | m := DataCmdComplete{"Complete", id, b.Port, b.q.Len(), doneCmd} 120 | bm, err := json.Marshal(m) 121 | if err == nil { 122 | h.broadcastSys <- bm 123 | } 124 | 125 | log.Printf("\tBuffer decreased to b.q.Len:%v\n", b.q.Len()) 126 | } else { 127 | log.Printf("\tWe should RARELY get here cuz we should have a command in the queue to dequeue when we get the >|stdin:|= response. If you see this debug stmt this is one of those few instances where NodeMCU sent us a >|stdin:|= not in response to a command we sent.") 128 | } 129 | 130 | if b.q.Len() < b.BufferMax { 131 | 132 | // if we are paused, tell us to unpause cuz we have clean buffer room now 133 | if b.GetPaused() { 134 | 135 | // we are paused, but we can't just go unpause ourself, because we may 136 | // be manually paused. this means we have to do a double-check here 137 | if b.GetManualPaused() == false { 138 | 139 | // we are not in a manual pause state, that means we can go ahead 140 | // and unpause ourselves 141 | b.SetPaused(false, 1) //set paused to false first, then release the hold on the buffer 142 | } else { 143 | log.Println("\tWe just got incoming >|stdin:|= so we could unpause, but since manual paused we will ignore until next time a >|stdin:|= comes in to unpause") 144 | } 145 | } 146 | 147 | } 148 | 149 | } 150 | 151 | // handle communication back to client 152 | // for base serial data (this is not the cmd:"Write" or cmd:"Complete") 153 | m := DataPerLine{b.Port, element + "\n"} 154 | bm, err := json.Marshal(m) 155 | if err == nil { 156 | h.broadcastSys <- bm 157 | } 158 | 159 | } // for loop 160 | 161 | b.bufferedOutput = arrLines[len(arrLines)-1] 162 | 163 | b.inOutLock.Unlock() 164 | log.Printf("Done with analyzing incoming data.") 165 | 166 | } 167 | }() 168 | 169 | /* 170 | go func() { 171 | b.ticker = time.NewTicker(16 * time.Millisecond) 172 | for _ = range b.ticker.C { 173 | if b.bufferedOutput != "" { 174 | m := SpPortMessage{b.Port, b.bufferedOutput} 175 | buf, _ := json.Marshal(m) 176 | b.Output <- []byte(buf) 177 | //log.Println(buf) 178 | b.bufferedOutput = "" 179 | } 180 | } 181 | }() 182 | */ 183 | 184 | } 185 | 186 | func IntArrayEquals(a []int, b []int) bool { 187 | if len(a) != len(b) { 188 | return false 189 | } 190 | for i, v := range a { 191 | if v != b[i] { 192 | return false 193 | } 194 | } 195 | return true 196 | } 197 | 198 | func ByteArrayEquals(a []byte, b []byte) bool { 199 | if len(a) != len(b) { 200 | return false 201 | } 202 | for i, v := range a { 203 | if v != b[i] { 204 | return false 205 | } 206 | } 207 | return true 208 | } 209 | 210 | func (b *BufferflowNodeMcu) BlockUntilReady(cmd string, id string) (bool, bool, string) { 211 | 212 | // Lock for this ENTIRE method 213 | b.inOutLock.Lock() 214 | 215 | log.Printf("BlockUntilReady() Start\n") 216 | log.Printf("\tid:%v, txt:%v\n", id, strings.Replace(cmd, "\n", "\\n", -1)) 217 | 218 | // keep track of whether we need to unlock at end of method or not 219 | // i.e. we unlock if we have to pause, thus we won't have to doubly unlock at end of method 220 | isNeedToUnlock := true 221 | 222 | b.q.Push(cmd, id) 223 | 224 | if b.q.Len() >= b.BufferMax { 225 | b.SetPaused(true, 0) // b.Paused = true 226 | log.Printf("\tIt looks like the local queue at Len: %v is over the allowed size of BufferMax: %v, so we are going to pause. Then when some incoming responses come in a check will occur to see if there's room to send this command. Pausing...", b.q.Len(), b.BufferMax) 227 | } 228 | 229 | if b.GetPaused() { 230 | //log.Println("It appears we are being asked to pause, so we will wait on b.sem") 231 | // We are being asked to pause our sending of commands 232 | 233 | // clear all b.sem signals so when we block below, we truly block 234 | b.ClearOutSemaphore() 235 | 236 | // since we need other code to run while we're blocking, we better release the packet ctr lock 237 | b.inOutLock.Unlock() 238 | // since we already unlocked this thread, note it so we don't doubly unlock 239 | isNeedToUnlock = false 240 | 241 | log.Println("\tBlocking on b.sem until told from OnIncomingData to go") 242 | unblockType, ok := <-b.sem // will block until told from OnIncomingData to go 243 | 244 | log.Printf("\tDone blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) 245 | 246 | log.Printf("\tDone blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) 247 | 248 | // we get an unblockType of 1 for normal unblocks 249 | // we get an unblockType of 2 when we're being asked to wipe the buffer, i.e. from a % cmd 250 | if unblockType == 2 { 251 | log.Println("\tThis was an unblock of type 2, which means we're being asked to wipe internal buffer. so return false.") 252 | // returning false asks the calling method to wipe the serial send once 253 | // this function returns 254 | return false, false, "" 255 | } 256 | } 257 | 258 | log.Printf("BlockUntilReady() end\n") 259 | 260 | time.Sleep(10 * time.Millisecond) 261 | 262 | if isNeedToUnlock { 263 | b.inOutLock.Unlock() 264 | } 265 | 266 | //return true, willHandleCompleteResponse, newCmd 267 | 268 | return true, true, "" 269 | } 270 | 271 | func (b *BufferflowNodeMcu) OnIncomingData(data string) { 272 | b.Input <- data 273 | } 274 | 275 | // Clean out b.sem so it can truly block 276 | func (b *BufferflowNodeMcu) ClearOutSemaphore() { 277 | keepLooping := true 278 | for keepLooping { 279 | select { 280 | case _, ok := <-b.sem: // case d, ok := 281 | //log.Printf("Consuming b.sem queue to clear it before we block. ok:%v, d:%v\n", ok, string(d)) 282 | //ctr++ 283 | if ok == false { 284 | keepLooping = false 285 | } 286 | default: 287 | keepLooping = false 288 | //log.Println("Hit default in select clause") 289 | } 290 | } 291 | } 292 | 293 | func (b *BufferflowNodeMcu) BreakApartCommands(cmd string) []string { 294 | return []string{cmd} 295 | } 296 | 297 | func (b *BufferflowNodeMcu) Pause() { 298 | return 299 | } 300 | 301 | func (b *BufferflowNodeMcu) Unpause() { 302 | return 303 | } 304 | 305 | func (b *BufferflowNodeMcu) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 306 | reRestart := regexp.MustCompile("node.restart\\(\\)") 307 | if reRestart.MatchString(cmd) { 308 | return true 309 | } else { 310 | return false 311 | } 312 | } 313 | 314 | func (b *BufferflowNodeMcu) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 315 | return false 316 | } 317 | 318 | func (b *BufferflowNodeMcu) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 319 | return false 320 | } 321 | 322 | func (b *BufferflowNodeMcu) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 323 | reRestart := regexp.MustCompile("^\\s*node.restart\\(\\)") 324 | if reRestart.MatchString(cmd) { 325 | log.Printf("\t\tWe found a node.restart() and thus we will wipe buffer") 326 | //b.ReleaseLock() 327 | return true 328 | } else { 329 | return false 330 | } 331 | } 332 | 333 | func (b *BufferflowNodeMcu) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 334 | 335 | reWhiteSpace := regexp.MustCompile("^\\s*$") 336 | if reWhiteSpace.MatchString(cmd) { 337 | log.Println("Found a whitespace only command") 338 | return true 339 | } else { 340 | return false 341 | } 342 | 343 | //return false 344 | } 345 | 346 | func (b *BufferflowNodeMcu) ReleaseLock() { 347 | log.Println("Wiping NodeMCU buffer") 348 | 349 | b.q.Delete() 350 | b.SetPaused(false, 2) 351 | } 352 | 353 | func (b *BufferflowNodeMcu) IsBufferGloballySendingBackIncomingData() bool { 354 | return true 355 | } 356 | 357 | func (b *BufferflowNodeMcu) Close() { 358 | if b.IsOpen == false { 359 | // we are being asked a 2nd time to close when we already have 360 | // that will cause a panic 361 | log.Println("We got called a 2nd time to close, but already closed") 362 | return 363 | } 364 | b.IsOpen = false 365 | 366 | //b.ticker.Stop() 367 | close(b.Input) 368 | } 369 | 370 | func (b *BufferflowNodeMcu) RewriteSerialData(cmd string, id string) string { 371 | return "" 372 | } 373 | 374 | // Gets the paused state of this buffer 375 | // go-routine safe. 376 | func (b *BufferflowNodeMcu) GetPaused() bool { 377 | b.lock.Lock() 378 | defer b.lock.Unlock() 379 | return b.Paused 380 | } 381 | 382 | // Sets the paused state of this buffer 383 | // go-routine safe. 384 | func (b *BufferflowNodeMcu) SetPaused(isPaused bool, semRelease int) { 385 | b.lock.Lock() 386 | defer b.lock.Unlock() 387 | b.Paused = isPaused 388 | 389 | // only release semaphore if we are being told to unpause 390 | if b.Paused == false { 391 | // the BlockUntilReady thread should be sitting waiting 392 | // so when we send this should trigger it 393 | b.sem <- semRelease 394 | log.Printf("\tJust sent release to b.sem with val:%v, so we will not block the sending to serial port anymore.", semRelease) 395 | 396 | } 397 | } 398 | 399 | func (b *BufferflowNodeMcu) GetManualPaused() bool { 400 | b.manualLock.Lock() 401 | defer b.manualLock.Unlock() 402 | return b.ManualPaused 403 | } 404 | 405 | func (b *BufferflowNodeMcu) SetManualPaused(isPaused bool) { 406 | b.manualLock.Lock() 407 | defer b.manualLock.Unlock() 408 | b.ManualPaused = isPaused 409 | } 410 | -------------------------------------------------------------------------------- /bufferflow_timed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type BufferflowTimed struct { 10 | Name string 11 | Port string 12 | Output chan []byte 13 | Input chan string 14 | ticker *time.Ticker 15 | IsOpen bool 16 | bufferedOutput string 17 | } 18 | 19 | /* 20 | var ( 21 | bufferedOutput string 22 | ) 23 | */ 24 | 25 | func (b *BufferflowTimed) Init() { 26 | log.Println("Initting timed buffer flow (output once every 16ms)") 27 | b.bufferedOutput = "" 28 | b.IsOpen = true 29 | 30 | go func() { 31 | for data := range b.Input { 32 | b.bufferedOutput = b.bufferedOutput + data 33 | 34 | } 35 | }() 36 | 37 | go func() { 38 | b.ticker = time.NewTicker(16 * time.Millisecond) 39 | for _ = range b.ticker.C { 40 | if b.bufferedOutput != "" { 41 | m := SpPortMessage{b.Port, b.bufferedOutput} 42 | buf, _ := json.Marshal(m) 43 | b.Output <- []byte(buf) 44 | //log.Println(buf) 45 | b.bufferedOutput = "" 46 | } 47 | } 48 | }() 49 | 50 | } 51 | 52 | func (b *BufferflowTimed) BlockUntilReady(cmd string, id string) (bool, bool, string) { 53 | //log.Printf("BlockUntilReady() start\n") 54 | return true, false, "" 55 | } 56 | 57 | func (b *BufferflowTimed) OnIncomingData(data string) { 58 | b.Input <- data 59 | } 60 | 61 | // Clean out b.sem so it can truly block 62 | func (b *BufferflowTimed) ClearOutSemaphore() { 63 | } 64 | 65 | func (b *BufferflowTimed) BreakApartCommands(cmd string) []string { 66 | return []string{cmd} 67 | } 68 | 69 | func (b *BufferflowTimed) Pause() { 70 | return 71 | } 72 | 73 | func (b *BufferflowTimed) Unpause() { 74 | return 75 | } 76 | 77 | func (b *BufferflowTimed) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { 78 | return false 79 | } 80 | 81 | func (b *BufferflowTimed) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { 82 | return false 83 | } 84 | 85 | func (b *BufferflowTimed) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { 86 | return false 87 | } 88 | 89 | func (b *BufferflowTimed) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { 90 | return false 91 | } 92 | 93 | func (b *BufferflowTimed) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { 94 | return false 95 | } 96 | 97 | func (b *BufferflowTimed) ReleaseLock() { 98 | } 99 | 100 | func (b *BufferflowTimed) IsBufferGloballySendingBackIncomingData() bool { 101 | return true 102 | } 103 | 104 | func (b *BufferflowTimed) Close() { 105 | if b.IsOpen == false { 106 | // we are being asked a 2nd time to close when we already have 107 | // that will cause a panic 108 | log.Println("We got called a 2nd time to close, but already closed") 109 | return 110 | } 111 | b.IsOpen = false 112 | 113 | b.ticker.Stop() 114 | close(b.Input) 115 | } 116 | 117 | func (b *BufferflowTimed) GetManualPaused() bool { 118 | return false 119 | } 120 | 121 | func (b *BufferflowTimed) RewriteSerialData(cmd string, id string) string { 122 | return "" 123 | } 124 | 125 | func (b *BufferflowTimed) SetManualPaused(isPaused bool) { 126 | } 127 | -------------------------------------------------------------------------------- /compile_go1_5_crosscompile.sh: -------------------------------------------------------------------------------- 1 | # git submodule init 2 | # git submodule update 3 | 4 | echo "About to cross-compile Serial Port JSON Server with Go 1.5" 5 | if [ "$1" = "" ]; then 6 | echo "You need to pass in the version number as the first parameter like ./compile_go1_5_crosscompile 1.87" 7 | exit 8 | fi 9 | 10 | rm -rf snapshot/* 11 | 12 | cp README.md snapshot/ 13 | 14 | echo "Building Linux amd64" 15 | mkdir snapshot/serial-port-json-server-$1_linux_amd64 16 | mkdir snapshot/serial-port-json-server-$1_linux_amd64/arduino 17 | cp -r arduino/hardware snapshot/serial-port-json-server-$1_linux_amd64/arduino/hardware 18 | cp -r arduino/tools_linux_64 snapshot/serial-port-json-server-$1_linux_amd64/arduino/tools 19 | env GOOS=linux GOARCH=amd64 go build -v -o snapshot/serial-port-json-server-$1_linux_amd64/serial-port-json-server 20 | tar -zcvf snapshot/serial-port-json-server-$1_linux_amd64.tar.gz snapshot/serial-port-json-server-$1_linux_amd64 21 | 22 | echo "" 23 | echo "Building Linux 386" 24 | mkdir snapshot/serial-port-json-server-$1_linux_386 25 | mkdir snapshot/serial-port-json-server-$1_linux_386/arduino 26 | cp -r arduino/hardware snapshot/serial-port-json-server-$1_linux_386/arduino/hardware 27 | cp -r arduino/tools_linux_32 snapshot/serial-port-json-server-$1_linux_386/arduino/tools 28 | env GOOS=linux GOARCH=386 go build -v -o snapshot/serial-port-json-server-$1_linux_386/serial-port-json-server 29 | tar -zcvf snapshot/serial-port-json-server-$1_linux_386.tar.gz snapshot/serial-port-json-server-$1_linux_386 30 | 31 | echo "" 32 | echo "Building Linux ARM (Raspi)" 33 | mkdir snapshot/serial-port-json-server-$1_linux_arm 34 | mkdir snapshot/serial-port-json-server-$1_linux_arm/arduino 35 | cp -r arduino/hardware snapshot/serial-port-json-server-$1_linux_arm/arduino/hardware 36 | cp -r arduino/tools_linux_arm snapshot/serial-port-json-server-$1_linux_arm/arduino/tools 37 | env GOOS=linux GOARCH=arm go build -v -o snapshot/serial-port-json-server-$1_linux_arm/serial-port-json-server 38 | tar -zcvf snapshot/serial-port-json-server-$1_linux_arm.tar.gz snapshot/serial-port-json-server-$1_linux_arm 39 | 40 | echo "" 41 | echo "Building Windows x32" 42 | mkdir snapshot/serial-port-json-server-$1_windows_386 43 | mkdir snapshot/serial-port-json-server-$1_windows_386/arduino 44 | cp -r arduino/hardware snapshot/serial-port-json-server-$1_windows_386/arduino/hardware 45 | cp -r arduino/tools_windows snapshot/serial-port-json-server-$1_windows_386/arduino/tools 46 | env GOOS=windows GOARCH=386 go build -v -o snapshot/serial-port-json-server-$1_windows_386/serial-port-json-server.exe 47 | cd snapshot/serial-port-json-server-$1_windows_386 48 | zip -r ../serial-port-json-server-$1_windows_386.zip * 49 | cd ../.. 50 | 51 | echo "" 52 | echo "Building Windows x64" 53 | mkdir snapshot/serial-port-json-server-$1_windows_amd64 54 | mkdir snapshot/serial-port-json-server-$1_windows_amd64/arduino 55 | cp -r arduino/hardware snapshot/serial-port-json-server-$1_windows_amd64/arduino/hardware 56 | cp -r arduino/tools_windows snapshot/serial-port-json-server-$1_windows_amd64/arduino/tools 57 | env GOOS=windows GOARCH=amd64 go build -v -o snapshot/serial-port-json-server-$1_windows_amd64/serial-port-json-server.exe 58 | cd snapshot/serial-port-json-server-$1_windows_amd64 59 | zip -r ../serial-port-json-server-$1_windows_amd64.zip * 60 | cd ../.. 61 | 62 | echo "" 63 | echo "Building Darwin x64" 64 | mkdir snapshot/serial-port-json-server-$1_darwin_amd64 65 | mkdir snapshot/serial-port-json-server-$1_darwin_amd64/arduino 66 | cp -r arduino/hardware snapshot/serial-port-json-server-$1_darwin_amd64/arduino/hardware 67 | cp -r arduino/tools_darwin snapshot/serial-port-json-server-$1_darwin_amd64/arduino/tools 68 | env GOOS=darwin GOARCH=amd64 go build -v -o snapshot/serial-port-json-server-$1_darwin_amd64/serial-port-json-server 69 | cd snapshot/serial-port-json-server-$1_darwin_amd64 70 | zip -r ../serial-port-json-server-$1_darwin_amd64.zip * 71 | cd ../.. 72 | -------------------------------------------------------------------------------- /compile_spjs.sh: -------------------------------------------------------------------------------- 1 | # git submodule init 2 | # git submodule update 3 | 4 | echo "About to cross-compile Serial Port JSON Server" 5 | if [ "$1" = "" ]; then 6 | echo "You need to pass in the version number as the first parameter." 7 | exit 8 | fi 9 | 10 | rm -rf snapshot/* 11 | 12 | cp README.md snapshot/ 13 | 14 | cp -r arduino/tools_linux_64 arduino/tools 15 | goxc -os="linux" -arch="amd64" --include="arduino/hardware,arduino/tools" -n="serial-port-json-server" -d=. 16 | rm -rf arduino/tools 17 | mv snapshot/serial-port-json-server_linux_amd64.tar.gz snapshot/serial-port-json-server-$1_linux_amd64.tar.gz 18 | 19 | cp -r arduino/tools_linux_32 arduino/tools 20 | goxc -os="linux" -arch="386" --include="arduino/hardware,arduino/tools" -n="serial-port-json-server" -d=. 21 | rm -rf arduino/tools 22 | mv snapshot/serial-port-json-server_linux_386.tar.gz snapshot/serial-port-json-server-$1_linux_386.tar.gz 23 | 24 | cp -r arduino/tools_linux_arm arduino/tools 25 | goxc -os="linux" -arch="arm" --include="arduino/hardware,arduino/tools" -n="serial-port-json-server" -d=. 26 | rm -rf arduino/tools 27 | mv snapshot/serial-port-json-server_linux_arm.tar.gz snapshot/serial-port-json-server-$1_linux_arm.tar.gz 28 | 29 | cp -r arduino/tools_windows arduino/tools 30 | goxc -os="windows" --include="arduino/hardware,arduino/tools,drivers/windows" -n="serial-port-json-server" -d=. 31 | rm -rf arduino/tools 32 | mv snapshot/serial-port-json-server_windows_386.zip snapshot/serial-port-json-server-$1_windows_386.zip 33 | mv snapshot/serial-port-json-server_windows_amd64.zip snapshot/serial-port-json-server-$1_windows_amd64.zip 34 | 35 | cp -r arduino/tools_darwin arduino/tools 36 | goxc -os="darwin" --include="arduino/hardware,arduino/tools" -n="serial-port-json-server" -d=. 37 | rm -rf arduino/tools 38 | mv snapshot/serial-port-json-server_darwin_386.zip snapshot/serial-port-json-server-$1_darwin_386.zip 39 | mv snapshot/serial-port-json-server_darwin_amd64.zip snapshot/serial-port-json-server-$1_darwin_amd64.zip 40 | 41 | // remove snapshot files 42 | rm snapshot/*snapshot* 43 | 44 | sudo mkdir "/media/sf_downloads/v$1" 45 | sudo cp snapshot/*.zip snapshot/*.gz snapshot/*.md snapshot/*.deb "/media/sf_downloads/v$1/" 46 | -------------------------------------------------------------------------------- /compile_webidebridge.sh: -------------------------------------------------------------------------------- 1 | # git submodule init 2 | # git submodule update 3 | 4 | cp -r arduino/tools_linux_64 arduino/tools 5 | goxc -os="linux" -arch="amd64" --include="arduino/hardware,arduino/tools" -n="Arduino_WebIDE_Bridge" -d=. 6 | rm -rf arduino/tools 7 | 8 | cp -r arduino/tools_linux_32 arduino/tools 9 | goxc -os="linux" -arch="386" --include="arduino/hardware,arduino/tools" -n="Arduino_WebIDE_Bridge" -d=. 10 | rm -rf arduino/tools 11 | 12 | cp -r arduino/tools_linux_arm arduino/tools 13 | goxc -os="linux" -arch="arm" --include="arduino/hardware,arduino/tools" -n="Arduino_WebIDE_Bridge" -d=. 14 | rm -rf arduino/tools 15 | 16 | cp -r arduino/tools_windows arduino/tools 17 | goxc -os="windows" --include="arduino/hardware,arduino/tools" -n="Arduino_WebIDE_Bridge" -d=. 18 | rm -rf arduino/tools 19 | 20 | cp -r arduino/tools_darwin arduino/tools 21 | goxc -os="darwin" --include="arduino/hardware,arduino/tools" -n="Arduino_WebIDE_Bridge" -d=. 22 | rm -rf arduino/tools 23 | 24 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // Supports Windows, Linux, Mac, and Raspberry Pi 2 | 3 | // Going to add SSL 4 | 5 | package main 6 | 7 | import ( 8 | "log" 9 | "net/http" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | type connection struct { 15 | // The websocket connection. 16 | ws *websocket.Conn 17 | 18 | // Buffered channel of outbound messages. 19 | send chan []byte 20 | } 21 | 22 | func (c *connection) reader() { 23 | for { 24 | _, message, err := c.ws.ReadMessage() 25 | if err != nil { 26 | break 27 | } 28 | 29 | h.broadcast <- message 30 | } 31 | c.ws.Close() 32 | } 33 | 34 | func (c *connection) writer() { 35 | for message := range c.send { 36 | err := c.ws.WriteMessage(websocket.TextMessage, message) 37 | if err != nil { 38 | break 39 | } 40 | } 41 | c.ws.Close() 42 | } 43 | 44 | func wsHandler(w http.ResponseWriter, r *http.Request) { 45 | log.Print("Started a new websocket handler") 46 | ws, err := websocket.Upgrade(w, r, nil, 1024, 1024) 47 | if _, ok := err.(websocket.HandshakeError); ok { 48 | http.Error(w, "Not a websocket handshake", 400) 49 | return 50 | } else if err != nil { 51 | return 52 | } 53 | //c := &connection{send: make(chan []byte, 256), ws: ws} 54 | c := &connection{send: make(chan []byte, 256*10), ws: ws} 55 | h.register <- c 56 | defer func() { h.unregister <- c }() 57 | go c.writer() 58 | c.reader() 59 | } 60 | -------------------------------------------------------------------------------- /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 downloadFromUrl(url string) (filename string, err error) { 16 | 17 | // clean up url 18 | // remove newlines and space at end 19 | url = strings.TrimSpace(url) 20 | 21 | // create tmp dir 22 | tmpdir, err := ioutil.TempDir("", "serial-port-json-server") 23 | if err != nil { 24 | return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?") 25 | } 26 | tokens := strings.Split(url, "/") 27 | filePrefix := tokens[len(tokens)-1] 28 | log.Println("The filePrefix is", filePrefix) 29 | 30 | fileName, _ := filepath.Abs(tmpdir + "/" + filePrefix) 31 | log.Println("Downloading", url, "to", fileName) 32 | 33 | // TODO: check file existence first with io.IsExist 34 | output, err := os.Create(fileName) 35 | if err != nil { 36 | log.Println("Error while creating", fileName, "-", err) 37 | return fileName, err 38 | } 39 | defer output.Close() 40 | 41 | response, err := http.Get(url) 42 | if err != nil { 43 | log.Println("Error while downloading", url, "-", err) 44 | return fileName, err 45 | } 46 | defer response.Body.Close() 47 | 48 | n, err := io.Copy(output, response.Body) 49 | if err != nil { 50 | log.Println("Error while downloading", url, "-", err) 51 | return fileName, err 52 | } 53 | 54 | log.Println(n, "bytes downloaded.") 55 | 56 | return fileName, nil 57 | } 58 | -------------------------------------------------------------------------------- /drivers/windows/TinyGv2.inf: -------------------------------------------------------------------------------- 1 | [Strings] 2 | DriverPackageDisplayName="TinyG USB Driver" 3 | ManufacturerName="Synthetos (www.synthetos.com)" 4 | ServiceName="USB RS-232 Emulation Driver" 5 | due.bossa.name="Bossa Program Port" 6 | due.programming_port.name="Arduino Due Programming Port" 7 | due.sketch01.name="TinyG v2 (Control Channel)" 8 | due.sketch02.name="TinyG v2 (Data Channel)" 9 | due.original.name="Arduino (broken TinyGv2)" 10 | 11 | [DefaultInstall] 12 | CopyINF=arduino.inf 13 | 14 | [Version] 15 | Class=Ports 16 | ClassGuid={4D36E978-E325-11CE-BFC1-08002BE10318} 17 | Signature="$Windows NT$" 18 | Provider=%ManufacturerName% 19 | DriverPackageDisplayName=%DriverPackageDisplayName% 20 | CatalogFile=arduino.cat 21 | DriverVer=08/27/2014,1.0.1.0 22 | 23 | [Manufacturer] 24 | %ManufacturerName%=DeviceList, NTamd64, NTia64 25 | 26 | [DestinationDirs] 27 | FakeModemCopyFileSection=12 28 | DefaultDestDir=12 29 | 30 | [DeviceList] 31 | %due.bossa.name%=DriverInstall, USB\VID_03EB&PID_6124 32 | %due.programming_port.name%=DriverInstall, USB\VID_2341&PID_003D 33 | %due.sketch01.name%=DriverInstall, USB\VID_1D50&PID_606D&MI_00 34 | %due.sketch02.name%=DriverInstall, USB\VID_1D50&PID_606D&MI_01 35 | %due.original.name%=DriverInstall, USB\VID_2341&PID_003E&MI_00 36 | 37 | [DeviceList.NTamd64] 38 | %due.bossa.name%=DriverInstall, USB\VID_03EB&PID_6124 39 | %due.programming_port.name%=DriverInstall, USB\VID_2341&PID_003D 40 | %due.sketch01.name%=DriverInstall, USB\VID_1D50&PID_606D&MI_00 41 | %due.sketch02.name%=DriverInstall, USB\VID_1D50&PID_606D&MI_01 42 | %due.original.name%=DriverInstall, USB\VID_2341&PID_003E&MI_00 43 | 44 | [DeviceList.NTia64] 45 | %due.bossa.name%=DriverInstall, USB\VID_03EB&PID_6124 46 | %due.programming_port.name%=DriverInstall, USB\VID_2341&PID_003D 47 | %due.sketch01.name%=DriverInstall, USB\VID_1D50&PID_606D&MI_00 48 | %due.sketch02.name%=DriverInstall, USB\VID_1D50&PID_606D&MI_01 49 | %due.original.name%=DriverInstall, USB\VID_2341&PID_003E&MI_00 50 | 51 | [DriverInstall] 52 | include=mdmcpq.inf,usb.inf 53 | CopyFiles = FakeModemCopyFileSection 54 | AddReg=DriverAddReg 55 | 56 | [DriverAddReg] 57 | HKR,,DevLoader,,*ntkern 58 | HKR,,NTMPDriver,,usbser.sys 59 | HKR,,EnumPropPages32,,"MsPorts.dll,SerialPortPropPageProvider" 60 | 61 | [DriverInstall.Services] 62 | include=mdmcpq.inf 63 | AddService=usbser, 0x00000002, DriverService 64 | 65 | [DriverService] 66 | DisplayName=%ServiceName% 67 | ServiceType=1 68 | StartType=3 69 | ErrorControl=1 70 | ServiceBinary=%12%\usbser.sys 71 | LoadOrderGroup=Base 72 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /execprocess.go: -------------------------------------------------------------------------------- 1 | // The execprocess feature lets SPJS run anything on the command line as a pass-thru 2 | // scenario. Obviously there are security concerns here if somebody opens up their 3 | // SPJS to the Internet, however if a user opens SPJS to the Internet they are 4 | // exposing a lot of things, so we will trust that users implement their own 5 | // layer of security at their firewall, rather than SPJS managing it. 6 | 7 | package main 8 | 9 | import ( 10 | "runtime" 11 | "strings" 12 | //"fmt" 13 | "encoding/json" 14 | "log" 15 | "os/exec" 16 | "regexp" 17 | ) 18 | 19 | type ExecCmd struct { 20 | ExecStatus string 21 | Id string 22 | Cmd string 23 | Args []string 24 | Output string 25 | //Stderr string 26 | } 27 | 28 | func execRun(command string) { 29 | log.Printf("About to execute command:%s\n", command) 30 | 31 | // we have to remove the word "exec " from the front 32 | re, _ := regexp.Compile("^exec\\s+") 33 | cleanCmd := re.ReplaceAllString(command, "") 34 | 35 | // see if there's an id, and if so, yank it out 36 | // grab any word after id: and do case insensitive (?i) 37 | reId := regexp.MustCompile("(?i)^id:[a-zA-z0-9_\\-]+") 38 | id := reId.FindString(cleanCmd) 39 | if len(id) > 0 { 40 | // we found an id at the start of the exec command, use it 41 | cleanCmd = reId.ReplaceAllString(cleanCmd, "") 42 | id = regexp.MustCompile("^id:").ReplaceAllString(id, "") 43 | } 44 | 45 | // trim it 46 | cleanCmd = regexp.MustCompile("^\\s*").ReplaceAllString(cleanCmd, "") 47 | cleanCmd = regexp.MustCompile("\\s*$").ReplaceAllString(cleanCmd, "") 48 | 49 | // now we have to split off the first command and pass the rest as args 50 | cmdArr := strings.Split(cleanCmd, " ") 51 | cmd := cmdArr[0] 52 | argArr := cmdArr[1:] 53 | oscmd := exec.Command(cmd, argArr...) 54 | 55 | // will block here until results are done 56 | cmdOutput, err := oscmd.CombinedOutput() 57 | 58 | endProgress() 59 | 60 | if err != nil { 61 | log.Printf("Command finished with error: %v "+string(cmdOutput), err) 62 | //h.broadcastSys <- []byte("Could not program the board") 63 | //mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board. It is also possible your serial port is locked by another app and thus we can't grab it to use for programming. Make sure all other apps that may be trying to access this serial port are disconnected or exited.", "Output": string(cmdOutput)} 64 | mapD := ExecCmd{ExecStatus: "Error", Id: id, Cmd: cmd, Args: argArr, Output: string(cmdOutput) + err.Error()} 65 | mapB, _ := json.Marshal(mapD) 66 | h.broadcastSys <- mapB 67 | } else { 68 | log.Printf("Finished without error. Good stuff. stdout: " + string(cmdOutput)) 69 | //h.broadcastSys <- []byte("Flash OK!") 70 | mapD := ExecCmd{ExecStatus: "Done", Id: id, Cmd: cmd, Args: argArr, Output: string(cmdOutput)} 71 | mapB, _ := json.Marshal(mapD) 72 | h.broadcastSys <- mapB 73 | // analyze stdin 74 | 75 | } 76 | 77 | } 78 | 79 | type ExecRuntime struct { 80 | ExecRuntimeStatus string 81 | OS string 82 | Arch string 83 | Goroot string 84 | NumCpu int 85 | } 86 | 87 | // Since SPJS runs on any OS, you will need to query to figure out 88 | // what OS we're on so you know the style of commands to send 89 | func execRuntime() { 90 | // create the struct and send data back 91 | info := ExecRuntime{"Done", runtime.GOOS, runtime.GOARCH, runtime.GOROOT(), runtime.NumCPU()} 92 | bm, err := json.Marshal(info) 93 | if err == nil { 94 | h.broadcastSys <- bm 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /feedrateoverride.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | //"fmt" 5 | "encoding/json" 6 | "log" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | reFeedrate = regexp.MustCompile("(?i)F(\\d+\\.{0,1}\\d*)") 14 | isFroNeedTriggered = false 15 | isFroOn = false 16 | //fro = 0.0 17 | currentFeedrate = -1.0 18 | lastFeedrateSeen = -1.0 19 | portsWithFrOverrideOn = make(map[string]bool) 20 | ) 21 | 22 | type froRequestJson struct { 23 | Cmd string 24 | Desc string 25 | Port string 26 | FeedRateOverride float32 27 | IsOn bool 28 | } 29 | 30 | // This is called from hub.go to actually parse the "fro COM7 1.5" command sent by the user 31 | func spFeedRateOverride(arg string) { 32 | 33 | // we will get a string of "fro COM9 2.4" or "fro /dev/ttyUSB0 0.1" 34 | log.Printf("Inside spFeedRateOverride arg: %v\n", strings.Replace(arg, "\n", "\\n", -1)) 35 | arg = strings.TrimSpace(arg) 36 | arg = strings.TrimPrefix(arg, " ") 37 | 38 | args := strings.Split(arg, " ") 39 | log.Println(args) 40 | 41 | if len(args) != 2 && len(args) != 3 { 42 | errstr := "Could not parse feedrate override command: " + arg 43 | log.Println(errstr) 44 | spErr(errstr) 45 | return 46 | } 47 | portname := strings.Trim(args[1], " ") 48 | log.Println("The port to write to is:" + portname + "---") 49 | 50 | //log.Println("The data is:" + args[2] + "---") 51 | 52 | // see if we have this port open 53 | myport, isFound := findPortByName(portname) 54 | myport.isFeedRateOverrideOn = true 55 | 56 | if !isFound { 57 | // we couldn't find the port, so send err 58 | //isFroOn = false 59 | spErr("We could not find the serial port " + portname + " that you were trying to apply the feedrate override to.") 60 | return 61 | } 62 | 63 | // see if they are just querying status 64 | if len(args) == 2 { 65 | sendStatusOnFeedrateOverride(myport) 66 | return 67 | } 68 | 69 | // we found our port, so now parse our multiplier 70 | fro, err := strconv.ParseFloat(strings.TrimSpace(args[2]), 32) 71 | if err != nil { 72 | errstr := "Could not parse feedrate override multiplier value: " + args[2] 73 | log.Println(errstr) 74 | spErr(errstr) 75 | return 76 | } 77 | 78 | myport.feedRateOverride = float32(fro) 79 | 80 | var frj froRequestJson 81 | frj.Cmd = "FeedRateOverride" 82 | frj.FeedRateOverride = myport.feedRateOverride 83 | frj.Port = myport.portConf.Name 84 | frj.Desc = "Successfully set the feedrate override." 85 | 86 | if frj.FeedRateOverride <= 0.0 { 87 | isFroOn = false 88 | log.Println("User turned off feedrate override by setting it to 0") 89 | frj.IsOn = false 90 | } else { 91 | isFroOn = true 92 | frj.IsOn = true 93 | } 94 | 95 | //ls, err := json.MarshalIndent(frj, "", "\t") 96 | ls, err := json.Marshal(frj) 97 | if err != nil { 98 | log.Println(err) 99 | h.broadcastSys <- []byte("Error creating json on feedrate override report " + 100 | err.Error()) 101 | } else { 102 | //log.Print("Printing out json byte data...") 103 | //log.Print(ls) 104 | h.broadcastSys <- ls 105 | } 106 | 107 | // if we made it this far we truly have a feedrate override in play 108 | // so set boolean that we need to inject it into the next line 109 | isFroNeedTriggered = true 110 | 111 | } 112 | 113 | func sendStatusOnFeedrateOverride(myport *serport) { 114 | // they just want a status 115 | var frj froRequestJson 116 | frj.Cmd = "FeedRateOverride" 117 | frj.FeedRateOverride = myport.feedRateOverride 118 | frj.Port = myport.portConf.Name 119 | frj.Desc = "Providing you status of feed rate override." 120 | 121 | if frj.FeedRateOverride <= 0.0 { 122 | frj.IsOn = false 123 | } else { 124 | frj.IsOn = true 125 | } 126 | 127 | ls, err := json.Marshal(frj) 128 | if err != nil { 129 | log.Println(err) 130 | h.broadcastSys <- []byte("Error creating json on feedrate override report " + 131 | err.Error()) 132 | } else { 133 | //log.Print("Printing out json byte data...") 134 | //log.Print(ls) 135 | h.broadcastSys <- ls 136 | } 137 | return 138 | } 139 | 140 | // Here is where we actually apply the feedrate override on a line of gcode 141 | func doFeedRateOverride(str string, feedrateoverride float32) (bool, string) { 142 | 143 | // myport, isFound := findPortByName(portname) 144 | // if myport == nil || myport.isFeedRateOverrideOn == false { 145 | // log.Println("This port has no feed rate override on. So returning...") 146 | // return false, "" 147 | // } 148 | 149 | //log.Println("Feed Rate Override Start") 150 | // any way we cut this, we MUST extract the feedrate from every line whether 151 | // fro is on or not because we need the currentFeedrate the moment the user asks 152 | // us to turn this on 153 | strArrFsSeen := reFeedrate.FindAllStringSubmatch(str, -1) 154 | if len(strArrFsSeen) > 0 { 155 | // we found some feedrate F values, so let's store it 156 | log.Printf("\tFRO: F's found:%v", strArrFsSeen) 157 | justFoundFeedrate := strArrFsSeen[len(strArrFsSeen)-1][1] 158 | lastFeedrateSeen, _ = strconv.ParseFloat(justFoundFeedrate, 64) 159 | currentFeedrate = lastFeedrateSeen 160 | log.Printf("\tFRO: Found an F so storing it for reference. lastFeedrateSeen:%v", lastFeedrateSeen) 161 | } 162 | 163 | if feedrateoverride == 0.0 && !isFroNeedTriggered { 164 | //log.Println("\tFRO: Feed Rate override is 0.0 so returning") 165 | return false, "" 166 | } 167 | 168 | // Typical line of gcode 169 | // N15 G2 F800.0 X39.0719 Y-3.7614 I-2.0806 J1.2144 170 | // Which, if the feedrate override is 2.6 we want to make look like 171 | // N15 G2 F2080.0 X39.0719 Y-3.7614 I-2.0806 J1.2144 172 | 173 | //str := "N15 G2 f800.0 X39.0719 Y-3.7614 F30 I-2.0806 J1.2144" 174 | //re := regexp.MustCompile("(?i)F(\\d+\\.{0,1}\\d*)") 175 | //strArr := re.FindAllString(str, -1) 176 | //fmt.Println(strArr) 177 | strArr2 := reFeedrate.FindAllStringSubmatch(str, -1) 178 | //log.Println(strArr2) 179 | if len(strArr2) == 0 { 180 | 181 | log.Println("\tFRO: No match found for feedrateoverride.") 182 | 183 | // see if the user asked for a feedrate override though 184 | // if they did, we need to inject one because we didn't find one to adjust 185 | if isFroNeedTriggered { 186 | 187 | log.Printf("\tFRO: We need to inject a feedrate...\n") 188 | 189 | if currentFeedrate == -1.0 { 190 | 191 | // this means we have no idea what the current feedrate is. that means 192 | // the gcode before us never specified it ever so we are stuck and can't 193 | // create the override 194 | log.Println("\tFRO: We have no idea what the current feedrate is, so giving up") 195 | return false, "" 196 | 197 | } else { 198 | 199 | myFro := feedrateoverride 200 | // since a value of 0 means turn off, we need to make it multiply like a 1, but leave it zero to mean turn off 201 | if myFro == 0.0 { 202 | myFro = 1.0 203 | } 204 | 205 | // if we get here we need to inject an F at the end of the line 206 | injectFr := currentFeedrate * float64(myFro) 207 | log.Printf("\tFRO: We do know the current feedrate: %v, so we will inject: F%v\n", currentFeedrate, injectFr) 208 | 209 | str = str + "F" + FloatToString(injectFr) 210 | log.Printf("\tFRO: New gcode line: %v\n", str) 211 | 212 | // set to false so next time through we don't inject again 213 | isFroNeedTriggered = false 214 | 215 | return true, str 216 | } 217 | 218 | } 219 | 220 | // no match found for feedrate, but also there is no need for an injection 221 | // so returning 222 | log.Printf("\tFRO: No need for injection of feedrate either cuz user never asked. currentFeedrate:%v. Returning.", currentFeedrate) 223 | return false, "" 224 | } 225 | 226 | // set to false so next time through we don't override again 227 | isFroNeedTriggered = false 228 | 229 | indxArr := reFeedrate.FindAllStringSubmatchIndex(str, -1) 230 | //log.Println(indxArr) 231 | 232 | fro := float64(feedrateoverride) 233 | //fro := float64(2.6) 234 | //fro := 235 | 236 | // keep track of whether we set the override yet in this method 237 | // this only matters if there are 2 or more F's in one gcode line 238 | // which should almost never happen, but just in case, since we iterate 239 | // in reverse, only use the first time through 240 | isAlreadySetCurrentFeedrate := false 241 | 242 | // loop in reverse so we can inject the new feedrate string at end and not have 243 | // our indexes thrown off 244 | for i := len(strArr2) - 1; i >= 0; i-- { 245 | 246 | itemArr := strArr2[i] 247 | //log.Println(itemArr) 248 | 249 | fr, err := strconv.ParseFloat(itemArr[1], 32) 250 | if err != nil { 251 | log.Println("\tFRO: Error parsing feedrate val", err) 252 | } else { 253 | 254 | // set this as current feedrate 255 | if !isAlreadySetCurrentFeedrate { 256 | currentFeedrate = fr 257 | isAlreadySetCurrentFeedrate = true 258 | log.Printf("\tFRO: Just set current feedrate: %v\n", currentFeedrate) 259 | } 260 | 261 | // only if fro is on should we proceed with the actual swap 262 | if isFroOn == true { 263 | 264 | newFr := fr * fro 265 | //log.Println(newFr) 266 | 267 | // swap out the string for our new string 268 | // because we are looping in reverse, these indexes are valid 269 | str = str[:indxArr[i][2]] + FloatToString(newFr) + str[indxArr[i][3]:] 270 | log.Println("\tFRO: " + strings.Replace(str, "\n", "\\n", -1)) 271 | } 272 | } 273 | 274 | } 275 | 276 | return true, str 277 | 278 | } 279 | 280 | func FloatToString(input_num float64) string { 281 | // to convert a float number to a string 282 | return strconv.FormatFloat(input_num, 'f', 3, 64) 283 | } 284 | -------------------------------------------------------------------------------- /gpio.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | //+build !linux,!arm 3 | // Ignore this file for now, but it would be nice to get GPIO going natively 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "os/signal" 13 | ) 14 | 15 | var ( 16 | gpio GPIO 17 | ) 18 | 19 | type GPIO struct { 20 | pinStates map[string]PinState 21 | pinStateChanged chan PinState 22 | pinAdded chan PinState 23 | pinRemoved chan string 24 | } 25 | 26 | type Direction int 27 | type PullUp int 28 | 29 | type PinState struct { 30 | Pin interface{} `json:"-"` 31 | PinId string 32 | Dir Direction 33 | State byte 34 | Pullup PullUp 35 | Name string 36 | } 37 | 38 | type PinDef struct { 39 | ID string 40 | Aliases []string 41 | Capabilities []string 42 | DigitalLogical int 43 | AnalogLogical int 44 | } 45 | 46 | const STATE_FILE = "pinstates.json" 47 | 48 | const ( 49 | In Direction = 0 50 | Out Direction = 1 51 | PWM Direction = 2 52 | 53 | Pull_None PullUp = 0 54 | Pull_Up PullUp = 1 55 | Pull_Down PullUp = 2 56 | ) 57 | 58 | type GPIOInterface interface { 59 | PreInit() 60 | Init(chan PinState, chan PinState, chan string, map[string]PinState) error 61 | Close() error 62 | PinMap() ([]PinDef, error) 63 | Host() (string, error) 64 | PinStates() (map[string]PinState, error) 65 | PinInit(string, Direction, PullUp, string) error 66 | PinSet(string, byte) error 67 | PinRemove(string) error 68 | } 69 | 70 | func (g *GPIO) CleanupGpio() { 71 | pinStates, err := gpio.PinStates() 72 | if err != nil { 73 | log.Println("Error getting pinstates on cleanup: " + err.Error()) 74 | } else { 75 | data, err := json.Marshal(pinStates) 76 | if err != nil { 77 | log.Println("Error marshalling pin states : " + err.Error()) 78 | } 79 | ioutil.WriteFile(STATE_FILE, data, 0644) 80 | } 81 | gpio.Close() 82 | os.Exit(1) 83 | } 84 | 85 | // I took what Ben had in his main.go file and moved it here 86 | func (g *GPIO) PreInit() { 87 | 88 | c := make(chan os.Signal, 1) 89 | signal.Notify(c, os.Interrupt) 90 | 91 | go func() { 92 | for sig := range c { 93 | // sig is a ^C, handle it 94 | log.Printf("captured %v, cleaning up gpio and exiting..", sig) 95 | gpio.CleanupGpio() 96 | } 97 | }() 98 | 99 | stateChanged := make(chan PinState) 100 | pinRemoved := make(chan string) 101 | pinAdded := make(chan PinState) 102 | go func() { 103 | for { 104 | // start listening on stateChanged and pinRemoved channels and update hub as appropriate 105 | select { 106 | case pinState := <-stateChanged: 107 | go h.sendMsg("PinState", pinState) 108 | case pinName := <-pinRemoved: 109 | go h.sendMsg("PinRemoved", pinName) 110 | case pinState := <-pinAdded: 111 | go h.sendMsg("PinAdded", pinState) 112 | } 113 | } 114 | }() 115 | 116 | pinStates := make(map[string]PinState) 117 | 118 | // read existing pin states 119 | if _, err := os.Stat(STATE_FILE); err == nil { 120 | log.Println("Reading prexisting pinstate file : " + STATE_FILE) 121 | dat, err := ioutil.ReadFile(STATE_FILE) 122 | if err != nil { 123 | log.Println("Failed to read state file : " + STATE_FILE + " : " + err.Error()) 124 | return 125 | } 126 | err = json.Unmarshal(dat, &pinStates) 127 | if err != nil { 128 | log.Println("Failed to unmarshal json : " + err.Error()) 129 | return 130 | } 131 | } 132 | 133 | gpio.Init(stateChanged, pinAdded, pinRemoved, pinStates) 134 | 135 | } 136 | 137 | func (g *GPIO) Init(pinStateChanged chan PinState, pinAdded chan PinState, pinRemoved chan string, states map[string]PinState) error { 138 | g.pinStateChanged = pinStateChanged 139 | g.pinRemoved = pinRemoved 140 | g.pinAdded = pinAdded 141 | g.pinStates = states 142 | 143 | // now init pins 144 | for key, pinState := range g.pinStates { 145 | if pinState.Name == "" { 146 | pinState.Name = pinState.PinId 147 | } 148 | g.PinInit(key, pinState.Dir, pinState.Pullup, pinState.Name) 149 | g.PinSet(key, pinState.State) 150 | } 151 | return nil 152 | } 153 | 154 | func (g *GPIO) Close() error { 155 | return nil 156 | } 157 | func (g *GPIO) PinMap() ([]PinDef, error) { 158 | // return a mock pinmap for this mock interface 159 | pinmap := []PinDef{ 160 | { 161 | "P8_07", 162 | []string{"66", "GPIO_66", "TIMER4"}, 163 | []string{"analog", "digital", "pwm"}, 164 | 66, 165 | 0, 166 | }, { 167 | "P8_08", 168 | []string{"67", "GPIO_67", "TIMER7"}, 169 | []string{"analog", "digital", "pwm"}, 170 | 67, 171 | 0, 172 | }, { 173 | "P8_09", 174 | []string{"69", "GPIO_69", "TIMER5"}, 175 | []string{"analog", "digital", "pwm"}, 176 | 69, 177 | 0, 178 | }, { 179 | "P8_10", 180 | []string{"68", "GPIO_68", "TIMER6"}, 181 | []string{"analog", "digital", "pwm"}, 182 | 68, 183 | 0, 184 | }, { 185 | "P8_11", 186 | []string{"45", "GPIO_45"}, 187 | []string{"analog", "digital", "pwm"}, 188 | 45, 189 | 0, 190 | }, 191 | } 192 | return pinmap, nil 193 | } 194 | func (g *GPIO) Host() (string, error) { 195 | return "fake", nil 196 | } 197 | func (g *GPIO) PinStates() (map[string]PinState, error) { 198 | return g.pinStates, nil 199 | } 200 | func (g *GPIO) PinInit(pinId string, dir Direction, pullup PullUp, name string) error { 201 | // add a pin 202 | 203 | // look up internal ID (we're going to assume its correct already) 204 | 205 | // make a pinstate object 206 | pinState := PinState{ 207 | nil, 208 | pinId, 209 | dir, 210 | 0, 211 | pullup, 212 | name, 213 | } 214 | 215 | g.pinStates[pinId] = pinState 216 | 217 | g.pinAdded <- pinState 218 | return nil 219 | } 220 | func (g *GPIO) PinSet(pinId string, val byte) error { 221 | // change pin state 222 | if pin, ok := g.pinStates[pinId]; ok { 223 | // we have a value.... 224 | pin.State = val 225 | g.pinStates[pinId] = pin 226 | // notify channel of new pinstate 227 | g.pinStateChanged <- pin 228 | } 229 | return nil 230 | } 231 | func (g *GPIO) PinRemove(pinId string) error { 232 | // remove a pin 233 | if _, ok := g.pinStates[pinId]; ok { 234 | // normally you would close the pin here 235 | delete(g.pinStates, pinId) 236 | g.pinRemoved <- pinId 237 | } 238 | return nil 239 | } 240 | -------------------------------------------------------------------------------- /gpio_linux_arm.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | // Ignore this file for now, but it would be nice to get GPIO going natively 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "errors" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "os/signal" 13 | "strconv" 14 | 15 | "github.com/kidoman/embd" 16 | _ "github.com/kidoman/embd/host/all" 17 | ) 18 | 19 | var ( 20 | gpio GPIO 21 | ) 22 | 23 | type GPIO struct { 24 | pinStates map[string]PinState 25 | pinStateChanged chan PinState 26 | pinAdded chan PinState 27 | pinRemoved chan string 28 | } 29 | 30 | type Direction int 31 | type PullUp int 32 | 33 | type PinState struct { 34 | Pin interface{} `json:"-"` 35 | PinId string 36 | Dir Direction 37 | State byte 38 | Pullup PullUp 39 | Name string 40 | } 41 | 42 | type PinDef struct { 43 | ID string 44 | Aliases []string 45 | Capabilities []string 46 | DigitalLogical int 47 | AnalogLogical int 48 | } 49 | 50 | const STATE_FILE = "pinstates.json" 51 | 52 | const ( 53 | In Direction = 0 54 | Out Direction = 1 55 | PWM Direction = 2 56 | 57 | Pull_None PullUp = 0 58 | Pull_Up PullUp = 1 59 | Pull_Down PullUp = 2 60 | ) 61 | 62 | type GPIOInterface interface { 63 | PreInit() 64 | CleanupGpio() 65 | Init(chan PinState, chan PinState, chan string, map[string]PinState) error 66 | Close() error 67 | PinMap() ([]PinDef, error) 68 | Host() (string, error) 69 | PinStates() (map[string]PinState, error) 70 | PinInit(string, Direction, PullUp, string) error 71 | PinSet(string, byte) error 72 | PinRemove(string) error 73 | } 74 | 75 | func (g *GPIO) CleanupGpio() { 76 | pinStates, err := gpio.PinStates() 77 | if err != nil { 78 | log.Println("Error getting pinstates on cleanup: " + err.Error()) 79 | } else { 80 | data, err := json.Marshal(pinStates) 81 | if err != nil { 82 | log.Println("Error marshalling pin states : " + err.Error()) 83 | } 84 | ioutil.WriteFile(STATE_FILE, data, 0644) 85 | } 86 | gpio.Close() 87 | os.Exit(1) 88 | } 89 | 90 | // I took what Ben had in his main.go file and moved it here 91 | func (g *GPIO) PreInit() { 92 | 93 | c := make(chan os.Signal, 1) 94 | signal.Notify(c, os.Interrupt) 95 | 96 | go func() { 97 | for sig := range c { 98 | // sig is a ^C, handle it 99 | log.Printf("captured %v, cleaning up gpio and exiting..", sig) 100 | gpio.CleanupGpio() 101 | } 102 | }() 103 | 104 | stateChanged := make(chan PinState) 105 | pinRemoved := make(chan string) 106 | pinAdded := make(chan PinState) 107 | go func() { 108 | for { 109 | // start listening on stateChanged and pinRemoved channels and update hub as appropriate 110 | select { 111 | case pinState := <-stateChanged: 112 | go h.sendMsg("PinState", pinState) 113 | case pinName := <-pinRemoved: 114 | go h.sendMsg("PinRemoved", pinName) 115 | case pinState := <-pinAdded: 116 | go h.sendMsg("PinAdded", pinState) 117 | } 118 | } 119 | }() 120 | 121 | pinStates := make(map[string]PinState) 122 | 123 | // read existing pin states 124 | if _, err := os.Stat(STATE_FILE); err == nil { 125 | log.Println("Reading prexisting pinstate file : " + STATE_FILE) 126 | dat, err := ioutil.ReadFile(STATE_FILE) 127 | if err != nil { 128 | log.Println("Failed to read state file : " + STATE_FILE + " : " + err.Error()) 129 | return 130 | } 131 | err = json.Unmarshal(dat, &pinStates) 132 | if err != nil { 133 | log.Println("Failed to unmarshal json : " + err.Error()) 134 | return 135 | } 136 | } 137 | 138 | gpio.Init(stateChanged, pinAdded, pinRemoved, pinStates) 139 | 140 | } 141 | 142 | func (g *GPIO) Init(pinStateChanged chan PinState, pinAdded chan PinState, pinRemoved chan string, states map[string]PinState) error { 143 | g.pinStateChanged = pinStateChanged 144 | g.pinRemoved = pinRemoved 145 | g.pinAdded = pinAdded 146 | g.pinStates = states 147 | 148 | // if its a raspberry pi initialize pi-blaster too 149 | host, _, err := embd.DetectHost() 150 | if err != nil { 151 | return err 152 | } 153 | if host == embd.HostRPi { 154 | InitBlaster() 155 | } 156 | 157 | err = embd.InitGPIO() 158 | if err != nil { 159 | return err 160 | } 161 | // now init pins 162 | for key, pinState := range g.pinStates { 163 | if pinState.Name == "" { 164 | pinState.Name = pinState.PinId 165 | } 166 | g.PinInit(key, pinState.Dir, pinState.Pullup, pinState.Name) 167 | g.PinSet(key, pinState.State) 168 | } 169 | return nil 170 | } 171 | 172 | func (g *GPIO) Close() error { 173 | // close all the pins we have open if any 174 | for _, pinState := range g.pinStates { 175 | if pinState.Pin != nil { 176 | switch pinObj := pinState.Pin.(type) { 177 | case embd.DigitalPin: 178 | pinObj.Close() 179 | case embd.PWMPin: 180 | pinObj.Close() 181 | case BlasterPin: 182 | pinObj.Close() 183 | } 184 | } 185 | } 186 | 187 | // if its a raspberry pi close pi-blaster too 188 | host, _, err := embd.DetectHost() 189 | if err != nil { 190 | return err 191 | } 192 | if host == embd.HostRPi { 193 | CloseBlaster() 194 | } 195 | 196 | err = embd.CloseGPIO() 197 | if err != nil { 198 | return err 199 | } 200 | return nil 201 | } 202 | func (g *GPIO) PinMap() ([]PinDef, error) { 203 | desc, err := embd.DescribeHost() 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | // wrap pinmap in a struct to make the json easier to parse on the other end 209 | embdMap := desc.GPIODriver().PinMap() 210 | pinMap := make([]PinDef, len(embdMap)) 211 | // convert to PinDef format 212 | for i := 0; i < len(embdMap); i++ { 213 | pinDesc := embdMap[i] 214 | caps := make([]string, 0) 215 | 216 | if pinDesc.Caps&embd.CapDigital != 0 { 217 | caps = append(caps, "Digital") 218 | } 219 | if pinDesc.Caps&embd.CapAnalog != 0 { 220 | caps = append(caps, "Analog") 221 | } 222 | if pinDesc.Caps&embd.CapPWM != 0 { 223 | caps = append(caps, "PWM") 224 | } 225 | if pinDesc.Caps&embd.CapI2C != 0 { 226 | caps = append(caps, "I2C") 227 | } 228 | if pinDesc.Caps&embd.CapUART != 0 { 229 | caps = append(caps, "UART") 230 | } 231 | if pinDesc.Caps&embd.CapSPI != 0 { 232 | caps = append(caps, "SPI") 233 | } 234 | if pinDesc.Caps&embd.CapGPMC != 0 { 235 | caps = append(caps, "GPMC") 236 | } 237 | if pinDesc.Caps&embd.CapLCD != 0 { 238 | caps = append(caps, "LCD") 239 | } 240 | 241 | pinMap[i] = PinDef{ 242 | pinDesc.ID, 243 | pinDesc.Aliases, 244 | caps, 245 | pinDesc.DigitalLogical, 246 | pinDesc.AnalogLogical, 247 | } 248 | } 249 | return pinMap, nil 250 | } 251 | func (g *GPIO) Host() (string, error) { 252 | host, _, err := embd.DetectHost() 253 | if err != nil { 254 | return "", err 255 | } 256 | return string(host), nil 257 | } 258 | func (g *GPIO) PinStates() (map[string]PinState, error) { 259 | return g.pinStates, nil 260 | } 261 | func (g *GPIO) PinInit(pinId string, dir Direction, pullup PullUp, name string) error { 262 | var pin interface{} 263 | state := byte(0) 264 | 265 | if dir == PWM { 266 | 267 | host, _, err := embd.DetectHost() 268 | if err != nil { 269 | return err 270 | } 271 | if host == embd.HostRPi { 272 | // use pi blaster pin 273 | log.Println("Creating PWM pin on Pi") 274 | 275 | // get the host descriptor 276 | desc, err := embd.DescribeHost() 277 | if err != nil { 278 | return err 279 | } 280 | // get the pinmap 281 | embdMap := desc.GPIODriver().PinMap() 282 | // lookup the pinId in the map 283 | var pinDesc *embd.PinDesc 284 | for i := range embdMap { 285 | pd := embdMap[i] 286 | 287 | if pd.ID == pinId { 288 | pinDesc = pd 289 | break 290 | } 291 | 292 | for j := range pd.Aliases { 293 | if pd.Aliases[j] == pinId { 294 | pinDesc = pd 295 | break 296 | } 297 | } 298 | } 299 | if pinDesc != nil { 300 | // we found a pin with that name....what is its first Alias? 301 | pinIdInt, err := strconv.Atoi(pinDesc.Aliases[0]) 302 | if err != nil { 303 | log.Println("Failed to parse int from alias : ", pinDesc.Aliases[0]) 304 | return err 305 | } 306 | p := NewBlasterPin(pinIdInt) 307 | pin = p 308 | } else { 309 | log.Println("Failed to find Pin ", pinId) 310 | return errors.New("Failed to find pin " + pinId) 311 | } 312 | } else { 313 | // bbb, so use embd since pwm pins work there 314 | p, err := embd.NewPWMPin(pinId) 315 | if err != nil { 316 | log.Println("Failed to create PWM Pin using key ", pinId, " : ", err.Error()) 317 | return err 318 | } 319 | pin = p 320 | } 321 | } else { 322 | // add a pin 323 | p, err := embd.NewDigitalPin(pinId) 324 | if err != nil { 325 | return err 326 | } 327 | pin = p 328 | 329 | err = p.SetDirection(embd.Direction(dir)) 330 | if err != nil { 331 | return err 332 | } 333 | 334 | if pullup == Pull_Up { 335 | err = p.PullUp() 336 | 337 | // pullup and down not implemented on rpi host so we need to manually set initial states 338 | // not ideal as a pullup really isn't the same thing but it works for most use cases 339 | 340 | if err != nil { 341 | log.Println("Failed to set pullup on " + pinId + " setting high state instead : " + err.Error()) 342 | // we failed to set pullup, so lets set initial state high instead 343 | err = p.Write(1) 344 | state = 1 345 | if err != nil { 346 | return err 347 | } 348 | } 349 | } else if pullup == Pull_Down { 350 | err = p.PullDown() 351 | 352 | if err != nil { 353 | 354 | log.Println("Failed to set pulldown on " + pinId + " setting low state instead : " + err.Error()) 355 | 356 | err = p.Write(0) 357 | state = 1 358 | if err != nil { 359 | return err 360 | } 361 | } 362 | } 363 | } 364 | 365 | // test to see if we already have a state for this pin 366 | existingPin, exists := g.pinStates[pinId] 367 | if exists { 368 | existingPin.Pin = pin 369 | existingPin.Name = name 370 | existingPin.Dir = dir 371 | existingPin.State = state 372 | existingPin.Pullup = pullup 373 | g.pinStates[pinId] = existingPin 374 | 375 | g.pinStateChanged <- existingPin 376 | g.pinRemoved <- pinId 377 | g.pinAdded <- g.pinStates[pinId] 378 | } else { 379 | g.pinStates[pinId] = PinState{pin, pinId, dir, state, pullup, name} 380 | g.pinAdded <- g.pinStates[pinId] 381 | } 382 | 383 | return nil 384 | } 385 | func (g *GPIO) PinSet(pinId string, val byte) error { 386 | // change pin state 387 | if pin, ok := g.pinStates[pinId]; ok { 388 | // we have a value.... 389 | switch pinObj := pin.Pin.(type) { 390 | case embd.DigitalPin: 391 | err := pinObj.Write(int(val)) 392 | if err != nil { 393 | return err 394 | } 395 | case embd.PWMPin: 396 | if err := pinObj.SetAnalog(val); err != nil { 397 | return err 398 | } 399 | case BlasterPin: 400 | err := pinObj.Write(val) 401 | if err != nil { 402 | return err 403 | } 404 | } 405 | pin.State = val 406 | g.pinStates[pinId] = pin 407 | // notify channel of new pinstate 408 | g.pinStateChanged <- pin 409 | } 410 | return nil 411 | } 412 | func (g *GPIO) PinRemove(pinId string) error { 413 | // remove a pin 414 | if pin, ok := g.pinStates[pinId]; ok { 415 | var err error 416 | switch pinObj := pin.Pin.(type) { 417 | case embd.DigitalPin: 418 | err = pinObj.Close() 419 | if err != nil { 420 | return err 421 | } 422 | case embd.PWMPin: 423 | err = pinObj.Close() 424 | if err != nil { 425 | return err 426 | } 427 | case BlasterPin: 428 | err = pinObj.Close() 429 | if err != nil { 430 | return err 431 | } 432 | } 433 | delete(g.pinStates, pinId) 434 | g.pinRemoved <- pinId 435 | } 436 | return nil 437 | } 438 | 439 | type BlasterPin struct { 440 | id int 441 | value float64 442 | } 443 | 444 | func InitBlaster() error { 445 | // check the file actually exists, throw error if not so Pi can bail out 446 | if _, err := os.Stat("/dev/pi-blaster"); os.IsNotExist(err) { 447 | return errors.New("/dev/pi-blaster does not exists, is pi-blaster correctly installed?") 448 | } 449 | return nil 450 | } 451 | func CloseBlaster() error { 452 | return nil 453 | } 454 | func NewBlasterPin(pinId int) BlasterPin { 455 | log.Println("Creating pi blaster pin on ", string(pinId)) 456 | return BlasterPin{ 457 | pinId, 458 | 0.0, 459 | } 460 | } 461 | func (b *BlasterPin) Close() error { 462 | f, err := os.Create("/dev/pi-blaster") 463 | if err != nil { 464 | return err 465 | } 466 | defer f.Close() 467 | _, err = f.WriteString("release " + strconv.Itoa(b.id)) 468 | if err != nil { 469 | return err 470 | } 471 | f.Sync() 472 | return nil 473 | } 474 | func (b *BlasterPin) Write(value byte) error { 475 | f, err := os.Create("/dev/pi-blaster") 476 | if err != nil { 477 | return err 478 | } 479 | defer f.Close() 480 | 481 | v := (float64(value) / 255.0) 482 | if v > 1.0 { 483 | v = 1.0 484 | } else if v < 0.0 { 485 | v = 0.0 486 | } 487 | toVal := strconv.FormatFloat(v, 'f', 2, 64) 488 | msg := strconv.Itoa(b.id) + "=" + string(toVal) 489 | _, err = f.WriteString(msg + "\n") 490 | if err != nil { 491 | log.Println("PiBlaster: Failed to write :", err.Error()) 492 | return err 493 | } 494 | b.value = v 495 | f.Sync() 496 | return nil 497 | } 498 | -------------------------------------------------------------------------------- /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 | "log" 6 | 7 | "github.com/kardianos/osext" 8 | //"os" 9 | "os/exec" 10 | //"path" 11 | //"path/filepath" 12 | //"runtime" 13 | //"debug" 14 | "encoding/json" 15 | "runtime" 16 | "runtime/debug" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | type hub struct { 22 | // Registered connections. 23 | connections map[*connection]bool 24 | 25 | // Inbound messages from the connections. 26 | broadcast chan []byte 27 | 28 | // Inbound messages from the system 29 | broadcastSys chan []byte 30 | 31 | // Register requests from the connections. 32 | register chan *connection 33 | 34 | // Unregister requests from connections. 35 | unregister chan *connection 36 | } 37 | 38 | var h = hub{ 39 | // buffered. go with 1000 cuz should never surpass that 40 | broadcast: make(chan []byte, 1000), 41 | broadcastSys: make(chan []byte, 1000), 42 | // non-buffered 43 | //broadcast: make(chan []byte), 44 | //broadcastSys: make(chan []byte), 45 | register: make(chan *connection), 46 | unregister: make(chan *connection), 47 | connections: make(map[*connection]bool), 48 | } 49 | 50 | func (h *hub) run() { 51 | for { 52 | select { 53 | case c := <-h.register: 54 | h.connections[c] = true 55 | // send supported commands 56 | c.send <- []byte("{\"Version\" : \"" + version + "\"} ") 57 | c.send <- []byte("{\"Commands\" : [\"list\", \"open [portName] [baud] [bufferAlgorithm (optional)]\", \"send [portName] [cmd]\", \"sendnobuf [portName] [cmd]\", \"sendjson {P:portName, Data:[{D:cmdStr, Id:idStr}]}\", \"close [portName]\", \"bufferalgorithms\", \"baudrates\", \"restart\", \"exit\", \"broadcast [anythingToRegurgitate]\", \"hostname\", \"version\", \"program [portName] [core:architecture:name] [path/to/binOrHexFile]\", \"programfromurl [portName] [core:architecture:name] [urlToBinOrHexFile]\", \"execruntime\", \"exec [command] [arg1] [arg2] [...]\"]} ") 58 | c.send <- []byte("{\"Hostname\" : \"" + *hostname + "\"} ") 59 | case c := <-h.unregister: 60 | delete(h.connections, c) 61 | // put close in func cuz it was creating panics and want 62 | // to isolate 63 | func() { 64 | // this method can panic if websocket gets disconnected 65 | // from users browser and we see we need to unregister a couple 66 | // of times, i.e. perhaps from incoming data from serial triggering 67 | // an unregister. (NOT 100% sure why seeing c.send be closed twice here) 68 | defer func() { 69 | if e := recover(); e != nil { 70 | log.Println("Got panic: ", e) 71 | } 72 | }() 73 | close(c.send) 74 | }() 75 | case m := <-h.broadcast: 76 | //log.Print("Got a broadcast") 77 | //log.Print(m) 78 | //log.Print(len(m)) 79 | if len(m) > 0 { 80 | //log.Print(string(m)) 81 | //log.Print(h.broadcast) 82 | checkCmd(m) 83 | //log.Print("-----") 84 | 85 | for c := range h.connections { 86 | select { 87 | case c.send <- m: 88 | //log.Print("did broadcast to ") 89 | //log.Print(c.ws.RemoteAddr()) 90 | //c.send <- []byte("hello world") 91 | default: 92 | delete(h.connections, c) 93 | close(c.send) 94 | go c.ws.Close() 95 | } 96 | } 97 | } 98 | case m := <-h.broadcastSys: 99 | //log.Printf("Got a system broadcast: %v\n", string(m)) 100 | //log.Print(string(m)) 101 | //log.Print("-----") 102 | 103 | for c := range h.connections { 104 | select { 105 | case c.send <- m: 106 | //log.Print("did broadcast to ") 107 | //log.Print(c.ws.RemoteAddr()) 108 | //c.send <- []byte("hello world") 109 | default: 110 | delete(h.connections, c) 111 | close(c.send) 112 | go c.ws.Close() 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | func checkCmd(m []byte) { 120 | //log.Print("Inside checkCmd") 121 | s := string(m[:]) 122 | log.Print(s) 123 | 124 | sl := strings.ToLower(s) 125 | 126 | if strings.HasPrefix(sl, "open") { 127 | 128 | // check if user wants to open this port as a secondary port 129 | // this doesn't mean much other than allowing the UI to show 130 | // a port as primary and make other ports sort of act less important 131 | isSecondary := false 132 | if strings.HasPrefix(s, "open secondary") { 133 | isSecondary = true 134 | // swap out the word secondary 135 | s = strings.Replace(s, "open secondary", "open", 1) 136 | } 137 | 138 | // remove newline 139 | args := strings.Split(strings.TrimSpace(s), " ") 140 | if len(args) < 3 { 141 | go spErr("You did not specify a port and baud rate in your open cmd") 142 | return 143 | } 144 | if len(args[1]) < 1 { 145 | go spErr("You did not specify a serial port") 146 | return 147 | } 148 | 149 | baudStr := strings.Replace(args[2], "\n", "", -1) 150 | baud, err := strconv.Atoi(baudStr) 151 | if err != nil { 152 | go spErr("Problem converting baud rate " + args[2]) 153 | return 154 | } 155 | // pass in buffer type now as string. if user does not 156 | // ask for a buffer type pass in empty string 157 | bufferAlgorithm := "" 158 | if len(args) > 3 { 159 | // cool. we got a buffer type request 160 | buftype := strings.Replace(args[3], "\n", "", -1) 161 | bufferAlgorithm = buftype 162 | } 163 | go spHandlerOpen(args[1], baud, bufferAlgorithm, isSecondary) 164 | 165 | } else if strings.HasPrefix(sl, "close") { 166 | 167 | log.Printf("About to split close commands. cmd:\"%v\"", s) 168 | // remove newline 169 | args := strings.Split(strings.TrimSpace(s), " ") 170 | //args := strings.Split(s, " ") 171 | log.Printf("The split args for close:%v", args) 172 | if len(args) > 1 { 173 | go spClose(args[1]) 174 | } else { 175 | go spErr("You did not specify a port to close") 176 | } 177 | 178 | } else if strings.HasPrefix(sl, "programkill") { 179 | 180 | // kill the running process (assumes singleton for now) 181 | go spHandlerProgramKill() 182 | 183 | } else if strings.HasPrefix(sl, "programfromurl") { 184 | 185 | args := strings.Split(s, " ") 186 | if len(args) == 4 { 187 | go spProgramFromUrl(args[1], args[2], args[3]) 188 | } else { 189 | go spErr("You did not specify a port, a board to program and/or a URL") 190 | } 191 | 192 | } else if strings.HasPrefix(sl, "program") { 193 | 194 | args := strings.Split(s, " ") 195 | if len(args) > 3 { 196 | var slice []string = args[3:len(args)] 197 | go spProgram(args[1], args[2], strings.Join(slice, " ")) 198 | } else { 199 | go spErr("You did not specify a port, a board to program and/or a filename") 200 | } 201 | 202 | } else if strings.HasPrefix(sl, "sendjson") { 203 | // will catch sendjson 204 | 205 | go spWriteJson(s) 206 | 207 | } else if strings.HasPrefix(sl, "send") { 208 | // will catch send and sendnobuf 209 | 210 | //args := strings.Split(s, "send ") 211 | go spWrite(s) 212 | 213 | } else if strings.HasPrefix(sl, "list") { 214 | go spList() 215 | //go getListViaWmiPnpEntity() 216 | } else if strings.HasPrefix(sl, "fro") { 217 | // User is wanting us to tweak the feedrate on-the-fly 218 | go spFeedRateOverride(s) 219 | 220 | } else if strings.HasPrefix(sl, "bufferalgorithm") { 221 | go spBufferAlgorithms() 222 | } else if strings.HasPrefix(sl, "baudrate") { 223 | go spBaudRates() 224 | } else if strings.HasPrefix(sl, "broadcast") { 225 | go broadcast(s) 226 | } else if strings.HasPrefix(sl, "restart") { 227 | restart() 228 | } else if strings.HasPrefix(sl, "exit") { 229 | exit() 230 | } else if strings.HasPrefix(sl, "memstats") { 231 | memoryStats() 232 | } else if strings.HasPrefix(sl, "gc") { 233 | garbageCollection() 234 | } else if strings.HasPrefix(sl, "bufflowdebug") { 235 | bufflowdebug(sl) 236 | } else if strings.HasPrefix(sl, "hostname") { 237 | getHostname() 238 | } else if strings.HasPrefix(sl, "version") { 239 | getVersion() 240 | } else if strings.HasPrefix(sl, "execruntime") { 241 | execRuntime() 242 | } else if strings.HasPrefix(sl, "exec") { 243 | go execRun(s) 244 | 245 | /* 246 | } else if strings.HasPrefix(sl, "gethost") { 247 | hostname, err := gpio.Host() 248 | if err != nil { 249 | go h.sendErr(err.Error()) 250 | } 251 | go h.sendMsg("Host", hostname) 252 | 253 | } else if strings.HasPrefix(sl, "getpinmap") { 254 | pinMap, err := gpio.PinMap() 255 | if err != nil { 256 | go h.sendErr(err.Error()) 257 | } 258 | go h.sendMsg("PinMap", pinMap) 259 | } else if strings.HasPrefix(sl, "getpinstates") { 260 | pinStates, err := gpio.PinStates() 261 | if err != nil { 262 | go h.sendErr(err.Error()) 263 | } 264 | go h.sendMsg("PinStates", pinStates) 265 | 266 | } else if strings.HasPrefix(sl, "initpin") { 267 | // format : setpin pinId dir pullup 268 | args := strings.Split(s, " ") 269 | if len(args) < 4 { 270 | go h.sendErr("You did not specify a pin and a direction [0|1|low|high] and a name") 271 | return 272 | } 273 | if len(args[1]) < 1 { 274 | go h.sendErr("You did not specify a pin") 275 | return 276 | } 277 | pin := args[1] 278 | dirStr := args[2] 279 | name := args[4] 280 | dir := In 281 | switch { 282 | case dirStr == "1" || dirStr == "out" || dirStr == "output": 283 | dir = Out 284 | case dirStr == "0" || dirStr == "in" || dirStr == "input": 285 | dir = In 286 | case dirStr == "pwm": 287 | dir = PWM 288 | } 289 | pullup := Pull_None 290 | switch { 291 | case args[3] == "1" || args[3] == "up": 292 | pullup = Pull_Up 293 | case args[3] == "0" || args[3] == "down": 294 | pullup = Pull_Down 295 | } 296 | err := gpio.PinInit(pin, dir, pullup, name) 297 | if err != nil { 298 | go h.sendErr(err.Error()) 299 | } 300 | } else if strings.HasPrefix(sl, "removepin") { 301 | // format : removepin pinId 302 | args := strings.Split(s, " ") 303 | if len(args) < 2 { 304 | go h.sendErr("You did not specify a pin id") 305 | return 306 | } 307 | err := gpio.PinRemove(args[1]) 308 | if err != nil { 309 | go h.sendErr(err.Error()) 310 | } 311 | } else if strings.HasPrefix(sl, "setpin") { 312 | // format : setpin pinId high/low/1/0 313 | args := strings.Split(s, " ") 314 | if len(args) < 3 { 315 | go h.sendErr("You did not specify a pin and a state [0|1|low|high]") 316 | return 317 | } 318 | if len(args[1]) < 1 { 319 | go h.sendErr("You did not specify a pin") 320 | return 321 | } 322 | pin := args[1] 323 | stateStr := args[2] 324 | state := 0 325 | switch { 326 | case stateStr == "1" || stateStr == "high": 327 | state = 1 328 | case stateStr == "0" || stateStr == "low": 329 | state = 0 330 | default: 331 | // assume its a pwm value...if it converts to integer in 0-255 range 332 | s, err := strconv.Atoi(stateStr) 333 | if err != nil { 334 | go h.sendErr("Invalid value, must be between 0 and 255 : " + stateStr) 335 | return 336 | } 337 | if s < 0 || s > 255 { 338 | go h.sendErr("Invalid value, must be between 0 and 255 : " + stateStr) 339 | return 340 | } 341 | state = s 342 | } 343 | 344 | err := gpio.PinSet(pin, byte(state)) 345 | if err != nil { 346 | go h.sendErr(err.Error()) 347 | } 348 | */ 349 | } else { 350 | go spErr("Could not understand command.") 351 | } 352 | 353 | //log.Print("Done with checkCmd") 354 | } 355 | 356 | func bufflowdebug(sl string) { 357 | log.Println("bufflowdebug start") 358 | if strings.HasPrefix(sl, "bufflowdebug on") { 359 | *bufFlowDebugType = "on" 360 | } else if strings.HasPrefix(sl, "bufflowdebug off") { 361 | *bufFlowDebugType = "off" 362 | } 363 | h.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *bufFlowDebugType + "\"}") 364 | log.Println("bufflowdebug end") 365 | } 366 | 367 | func memoryStats() { 368 | var memStats runtime.MemStats 369 | runtime.ReadMemStats(&memStats) 370 | json, _ := json.Marshal(memStats) 371 | log.Printf("memStats:%v\n", string(json)) 372 | h.broadcastSys <- json 373 | } 374 | 375 | func getHostname() { 376 | h.broadcastSys <- []byte("{\"Hostname\" : \"" + *hostname + "\"}") 377 | } 378 | 379 | func getVersion() { 380 | h.broadcastSys <- []byte("{\"Version\" : \"" + version + "\"}") 381 | } 382 | 383 | func garbageCollection() { 384 | log.Printf("Starting garbageCollection()\n") 385 | h.broadcastSys <- []byte("{\"gc\":\"starting\"}") 386 | memoryStats() 387 | debug.SetGCPercent(100) 388 | debug.FreeOSMemory() 389 | debug.SetGCPercent(-1) 390 | log.Printf("Done with garbageCollection()\n") 391 | h.broadcastSys <- []byte("{\"gc\":\"done\"}") 392 | memoryStats() 393 | } 394 | 395 | func exit() { 396 | log.Println("Starting new spjs process") 397 | h.broadcastSys <- []byte("{\"Exiting\" : true}") 398 | log.Fatal("Exited current spjs cuz asked to") 399 | 400 | } 401 | 402 | func restart() { 403 | // relaunch ourself and exit 404 | // the relaunch works because we pass a cmdline in 405 | // that has serial-port-json-server only initialize 5 seconds later 406 | // which gives us time to exit and unbind from serial ports and TCP/IP 407 | // sockets like :8989 408 | log.Println("Starting new spjs process") 409 | h.broadcastSys <- []byte("{\"Restarting\" : true}") 410 | 411 | // figure out current path of executable so we know how to restart 412 | // this process 413 | /* 414 | dir, err2 := filepath.Abs(filepath.Dir(os.Args[0])) 415 | if err2 != nil { 416 | //log.Fatal(err2) 417 | fmt.Printf("Error getting executable file path. err: %v\n", err2) 418 | } 419 | fmt.Printf("The path to this exe is: %v\n", dir) 420 | 421 | // alternate approach 422 | _, filename, _, _ := runtime.Caller(1) 423 | f, _ := os.Open(path.Join(path.Dir(filename), "serial-port-json-server")) 424 | fmt.Println(f) 425 | */ 426 | 427 | // using osext 428 | exePath, err3 := osext.Executable() 429 | if err3 != nil { 430 | fmt.Printf("Error getting exe path using osext lib. err: %v\n", err3) 431 | } 432 | fmt.Printf("exePath using osext: %v\n", exePath) 433 | 434 | // figure out garbageCollection flag 435 | //isGcFlag := "false" 436 | 437 | var cmd *exec.Cmd 438 | /*if *isGC { 439 | //isGcFlag = "true" 440 | cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc") 441 | } else { 442 | cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter) 443 | 444 | }*/ 445 | cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc", *gcType) 446 | 447 | //cmd := exec.Command("./serial-port-json-server", "ls") 448 | err := cmd.Start() 449 | if err != nil { 450 | log.Printf("Got err restarting spjs: %v\n", err) 451 | h.broadcastSys <- []byte("{\"Error\" : \"" + fmt.Sprintf("%v", err) + "\"}") 452 | } else { 453 | h.broadcastSys <- []byte("{\"Restarted\" : true}") 454 | } 455 | log.Fatal("Exited current spjs for restart") 456 | //log.Printf("Waiting for command to finish...") 457 | //err = cmd.Wait() 458 | //log.Printf("Command finished with error: %v", err) 459 | } 460 | 461 | type CmdBroadcast struct { 462 | Cmd string 463 | Msg string 464 | } 465 | 466 | func broadcast(arg string) { 467 | // we will get a string of broadcast asdf asdf asdf 468 | log.Println("Inside broadcast arg: " + arg) 469 | arg = strings.TrimPrefix(arg, " ") 470 | //log.Println("arg after trim: " + arg) 471 | args := strings.SplitN(arg, " ", 2) 472 | if len(args) != 2 { 473 | errstr := "Could not parse broadcast command: " + arg 474 | log.Println(errstr) 475 | spErr(errstr) 476 | return 477 | } 478 | broadcastcmd := strings.Trim(args[1], " ") 479 | log.Println("The broadcast cmd is:" + broadcastcmd + "---") 480 | 481 | bcmd := CmdBroadcast{ 482 | Cmd: "Broadcast", 483 | Msg: broadcastcmd, 484 | } 485 | json, _ := json.Marshal(bcmd) 486 | log.Printf("bcmd:%v\n", string(json)) 487 | h.broadcastSys <- json 488 | 489 | } 490 | 491 | func (h *hub) sendErr(msg string) { 492 | msgMap := map[string]string{"error": msg} 493 | log.Println("Error: " + msg) 494 | bytes, err := json.Marshal(msgMap) 495 | if err != nil { 496 | log.Println("Failed to marshal data!") 497 | return 498 | } 499 | h.broadcastSys <- bytes 500 | } 501 | 502 | func (h *hub) sendMsg(name string, msg interface{}) { 503 | msgMap := make(map[string]interface{}) 504 | msgMap[name] = msg 505 | msgMap["Type"] = name 506 | //log.Println("Sent: " + name) 507 | bytes, err := json.Marshal(msgMap) 508 | if err != nil { 509 | log.Println("Failed to marshal data!") 510 | return 511 | } 512 | h.broadcastSys <- bytes 513 | } 514 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Version 1.86 2 | // Supports Windows, Linux, Mac, and Raspberry Pi, Beagle Bone Black 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "go/build" 9 | "log" 10 | "net/http" 11 | //"path/filepath" 12 | "errors" 13 | "fmt" 14 | "net" 15 | "os" 16 | //"net/http/pprof" 17 | //"runtime" 18 | "io" 19 | "runtime/debug" 20 | "text/template" 21 | "time" 22 | ) 23 | 24 | var ( 25 | version = "1.87" 26 | versionFloat = float32(1.87) 27 | addr = flag.String("addr", ":8989", "http service address") 28 | //assets = flag.String("assets", defaultAssetPath(), "path to assets") 29 | //verbose = flag.Bool("v", true, "show debug logging") 30 | verbose = flag.Bool("v", false, "show debug logging") 31 | //homeTempl *template.Template 32 | isLaunchSelf = flag.Bool("ls", false, "launch self 5 seconds later") 33 | 34 | // regular expression to sort the serial port list 35 | // typically this wouldn't be provided, but if the user wants to clean 36 | // up their list with a regexp so it's cleaner inside their end-user interface 37 | // such as ChiliPeppr, this can make the massive list that Linux gives back 38 | // to you be a bit more manageable 39 | regExpFilter = flag.String("regex", "", "Regular expression to filter serial port list") 40 | 41 | // allow garbageCollection() 42 | //isGC = flag.Bool("gc", false, "Is garbage collection on? Off by default.") 43 | //isGC = flag.Bool("gc", true, "Is garbage collection on? Off by default.") 44 | 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)") 45 | 46 | // whether to do buffer flow debugging 47 | 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") 48 | 49 | // hostname. allow user to override, otherwise we look it up 50 | hostname = flag.String("hostname", "unknown-hostname", "Override the hostname we get from the OS") 51 | ) 52 | 53 | type NullWriter int 54 | 55 | func (NullWriter) Write([]byte) (int, error) { return 0, nil } 56 | 57 | func defaultAssetPath() string { 58 | //p, err := build.Default.Import("gary.burd.info/go-websocket-chat", "", build.FindOnly) 59 | p, err := build.Default.Import("github.com/johnlauer/serial-port-json-server", "", build.FindOnly) 60 | if err != nil { 61 | return "." 62 | } 63 | return p.Dir 64 | } 65 | 66 | func homeHandler(c http.ResponseWriter, req *http.Request) { 67 | homeTemplate.Execute(c, req.Host) 68 | } 69 | 70 | func launchSelfLater() { 71 | log.Println("Going to launch myself 5 seconds later.") 72 | time.Sleep(2 * 1000 * time.Millisecond) 73 | log.Println("Done waiting 5 secs. Now launching...") 74 | } 75 | 76 | func main() { 77 | 78 | flag.Parse() 79 | // setup logging 80 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) 81 | 82 | // see if we are supposed to wait 5 seconds 83 | if *isLaunchSelf { 84 | launchSelfLater() 85 | } 86 | 87 | //getList() 88 | f := flag.Lookup("addr") 89 | log.Println("Version:" + version) 90 | 91 | // hostname 92 | hn, _ := os.Hostname() 93 | if *hostname == "unknown-hostname" { 94 | *hostname = hn 95 | } 96 | log.Println("Hostname:", *hostname) 97 | 98 | // turn off garbage collection 99 | // this is dangerous, as u could overflow memory 100 | //if *isGC { 101 | if *gcType == "std" { 102 | log.Println("Garbage collection is on using Standard mode, meaning we just let Golang determine when to garbage collect.") 103 | } else if *gcType == "max" { 104 | 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.") 105 | } else { 106 | 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.") 107 | debug.SetGCPercent(-1) 108 | } 109 | 110 | ip, err := externalIP() 111 | if err != nil { 112 | log.Println(err) 113 | } 114 | 115 | log.Print("Starting server and websocket on " + ip + "" + f.Value.String()) 116 | //homeTempl = template.Must(template.ParseFiles(filepath.Join(*assets, "home.html"))) 117 | 118 | log.Println("The Serial Port JSON Server is now running.") 119 | log.Println("If you are using ChiliPeppr, you may go back to it and connect to this server.") 120 | 121 | // see if they provided a regex filter 122 | if len(*regExpFilter) > 0 { 123 | log.Printf("You specified a serial port regular expression filter: %v\n", *regExpFilter) 124 | } 125 | 126 | //GetDarwinMeta() 127 | 128 | if !*verbose { 129 | log.Println("You can enter verbose mode to see all logging by starting with the -v command line switch.") 130 | log.SetOutput(new(NullWriter)) //route all logging to nullwriter 131 | } 132 | 133 | // list serial ports 134 | portList, _ := GetList() 135 | metaports, _ := GetMetaList() 136 | 137 | /*if errSys != nil { 138 | log.Printf("Got system error trying to retrieve serial port list. Err:%v\n", errSys) 139 | log.Fatal("Exiting") 140 | }*/ 141 | go func() { 142 | time.Sleep(300 * time.Millisecond) 143 | log.SetOutput(io.Writer(os.Stdout)) 144 | log.Println("Your serial ports:") 145 | if len(portList) == 0 { 146 | log.Println("\tThere are no serial ports to list.") 147 | } 148 | for _, element := range portList { 149 | // if we have meta data for this port, use it 150 | setMetaDataForOsSerialPort(&element, metaports) 151 | log.Printf("\t%v\n", element) 152 | 153 | } 154 | if !*verbose { 155 | //log.Println("You can enter verbose mode to see all logging by starting with the -v command line switch.") 156 | log.SetOutput(new(NullWriter)) //route all logging to nullwriter 157 | } 158 | }() 159 | 160 | // launch the hub routine which is the singleton for the websocket server 161 | go h.run() 162 | // launch our serial port routine 163 | go sh.run() 164 | // launch our dummy data routine 165 | //go d.run() 166 | 167 | // Setup GPIO server 168 | // Ignore GPIO for now, but it would be nice to get GPIO going natively 169 | //gpio.PreInit() 170 | // when the app exits, clean up our gpio ports 171 | //defer gpio.CleanupGpio() 172 | 173 | http.HandleFunc("/", homeHandler) 174 | http.HandleFunc("/ws", wsHandler) 175 | if err := http.ListenAndServe(*addr, nil); err != nil { 176 | fmt.Printf("Error trying to bind to port: %v, so exiting...", err) 177 | log.Fatal("Error ListenAndServe:", err) 178 | } 179 | 180 | } 181 | 182 | func externalIP() (string, error) { 183 | //log.Println("Getting external IP") 184 | ifaces, err := net.Interfaces() 185 | if err != nil { 186 | log.Println("Got err getting external IP addr") 187 | return "", err 188 | } 189 | for _, iface := range ifaces { 190 | if iface.Flags&net.FlagUp == 0 { 191 | //log.Println("Iface down") 192 | continue // interface down 193 | } 194 | if iface.Flags&net.FlagLoopback != 0 { 195 | //log.Println("Loopback") 196 | continue // loopback interface 197 | } 198 | addrs, err := iface.Addrs() 199 | if err != nil { 200 | log.Println("Got err on iface.Addrs()") 201 | return "", err 202 | } 203 | for _, addr := range addrs { 204 | var ip net.IP 205 | switch v := addr.(type) { 206 | case *net.IPNet: 207 | ip = v.IP 208 | case *net.IPAddr: 209 | ip = v.IP 210 | } 211 | if ip == nil || ip.IsLoopback() { 212 | //log.Println("Ip was nil or loopback") 213 | continue 214 | } 215 | ip = ip.To4() 216 | if ip == nil { 217 | //log.Println("Was not ipv4 addr") 218 | continue // not an ipv4 address 219 | } 220 | //log.Println("IP is ", ip.String()) 221 | return ip.String(), nil 222 | } 223 | } 224 | return "", errors.New("are you connected to the network?") 225 | } 226 | 227 | var homeTemplate = template.Must(template.New("home").Parse(homeTemplateHtml)) 228 | 229 | // If you navigate to this server's homepage, you'll get this HTML 230 | // so you can directly interact with the serial port server 231 | const homeTemplateHtml = ` 232 | 233 | 234 | Serial Port Example 235 | 236 | 277 | 314 | 315 | 316 |
317 |
318 | 319 | 320 |
321 | 322 | 323 | ` 324 | -------------------------------------------------------------------------------- /programmer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | //"fmt" 6 | "encoding/json" 7 | "github.com/facchinm/go-serial" 8 | "github.com/kardianos/osext" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Download the file from URL first, store in tmp folder, then pass to spProgram 19 | func spProgramFromUrl(portname string, boardname string, url string) { 20 | mapB, _ := json.Marshal(map[string]string{"ProgrammerStatus": "DownloadStart", "Url": url}) 21 | h.broadcastSys <- mapB 22 | 23 | startDownloadProgress() 24 | 25 | filename, err := downloadFromUrl(url) 26 | 27 | endDownloadProgress() 28 | 29 | mapB, _ = json.Marshal(map[string]string{"Filename": filename, "Url": url, "ProgrammerStatus": "DownloadDone"}) 30 | h.broadcastSys <- mapB 31 | 32 | if err != nil { 33 | spErr(err.Error()) 34 | } else { 35 | spProgram(portname, boardname, filename) 36 | } 37 | 38 | // delete file 39 | 40 | } 41 | 42 | func spProgram(portname string, boardname string, filePath string) { 43 | 44 | isFound, flasher, mycmd := assembleCompilerCommand(boardname, portname, filePath) 45 | mapD := map[string]string{"ProgrammerStatus": "CommandReady", "IsFound": strconv.FormatBool(isFound), "Flasher": flasher, "Cmd": strings.Join(mycmd, " ")} 46 | mapB, _ := json.Marshal(mapD) 47 | h.broadcastSys <- mapB 48 | 49 | if isFound { 50 | spHandlerProgram(flasher, mycmd) 51 | } else { 52 | spErr("We could not find the serial port " + portname + " or the board " + boardname + " that you were trying to program. It is also possible your serial port is locked by another app and thus we can't grab it to use for programming. Make sure all other apps that may be trying to access this serial port are disconnected or exited.") 53 | } 54 | } 55 | 56 | var oscmd *exec.Cmd 57 | var isRunning = false 58 | 59 | func spHandlerProgram(flasher string, cmdString []string) { 60 | 61 | // Extra protection code to ensure we aren't getting called from multiple threads 62 | if isRunning { 63 | mapD := map[string]string{"ProgrammerStatus": "ThreadError", "Msg": "You tried to run a 2nd (or further) program command while the 1st one was already running. Only 1 program cmd can run at once."} 64 | mapB, _ := json.Marshal(mapD) 65 | h.broadcastSys <- mapB 66 | return 67 | } 68 | 69 | isRunning = true 70 | 71 | //h.broadcastSys <- []byte("Start flashing with command " + cmdString) 72 | log.Printf("Flashing with command:" + strings.Join(cmdString, " ")) 73 | mapD := map[string]string{"ProgrammerStatus": "Starting", "Cmd": strings.Join(cmdString, " ")} 74 | mapB, _ := json.Marshal(mapD) 75 | h.broadcastSys <- mapB 76 | 77 | // if runtime.GOOS == "darwin" { 78 | // sh, _ := exec.LookPath("sh") 79 | // // prepend the flasher to run it via sh 80 | // cmdString = append([]string{flasher}, cmdString...) 81 | // oscmd = exec.Command(sh, cmdString...) 82 | // } else { 83 | oscmd = exec.Command(flasher, cmdString...) 84 | // } 85 | 86 | // Stdout buffer 87 | //var cmdOutput []byte 88 | 89 | // start sending back signals to the browser as the programmer runs 90 | // just so user sees that things are chugging along 91 | startProgress() 92 | 93 | // will block here until results are done 94 | cmdOutput, err := oscmd.CombinedOutput() 95 | 96 | endProgress() 97 | 98 | if err != nil { 99 | log.Printf("Command finished with error: %v "+string(cmdOutput), err) 100 | h.broadcastSys <- []byte("Could not program the board") 101 | //mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board. It is also possible your serial port is locked by another app and thus we can't grab it to use for programming. Make sure all other apps that may be trying to access this serial port are disconnected or exited.", "Output": string(cmdOutput)} 102 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board.", "Output": string(cmdOutput)} 103 | mapB, _ := json.Marshal(mapD) 104 | h.broadcastSys <- mapB 105 | } else { 106 | log.Printf("Finished without error. Good stuff. stdout: " + string(cmdOutput)) 107 | h.broadcastSys <- []byte("Flash OK!") 108 | mapD := map[string]string{"ProgrammerStatus": "Done", "Flash": "Ok", "Output": string(cmdOutput)} 109 | mapB, _ := json.Marshal(mapD) 110 | h.broadcastSys <- mapB 111 | // analyze stdin 112 | 113 | } 114 | 115 | isRunning = false 116 | } 117 | 118 | func spHandlerProgramKill() { 119 | 120 | // Kill the process if there is one running 121 | if oscmd != nil && oscmd.Process.Pid > 0 { 122 | h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"PreKilled\", \"Pid\": " + strconv.Itoa(oscmd.Process.Pid) + ", \"ProcessState\": \"" + oscmd.ProcessState.String() + "\"}") 123 | oscmd.Process.Kill() 124 | h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"Killed\", \"Pid\": " + strconv.Itoa(oscmd.Process.Pid) + ", \"ProcessState\": \"" + oscmd.ProcessState.String() + "\"}") 125 | 126 | } else { 127 | if oscmd != nil { 128 | h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"KilledError\", \"Msg\": \"No current process\", \"Pid\": " + strconv.Itoa(oscmd.Process.Pid) + ", \"ProcessState\": \"" + oscmd.ProcessState.String() + "\"}") 129 | } else { 130 | h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"KilledError\", \"Msg\": \"No current process\"}") 131 | } 132 | } 133 | } 134 | 135 | // send back pseudo-status to browser while programming in progress 136 | // so user doesn't think it's dead 137 | // this is not multi-threaded, but will work for now since 138 | // this is just a nice-to-have informational progress 139 | var inProgress bool 140 | 141 | type ProgressState struct { 142 | ProgrammerStatus string 143 | Duration int 144 | Pid int 145 | //ProcessState string 146 | } 147 | 148 | func startProgress() { 149 | inProgress = true 150 | go func() { 151 | duration := 0 152 | for { 153 | time.Sleep(1 * time.Second) 154 | duration++ 155 | if inProgress == false { 156 | break 157 | } 158 | // break after 5 minutes max 159 | if duration > 60*5 { 160 | break 161 | } 162 | 163 | progmsg := ProgressState{ 164 | "Progress", 165 | duration, 166 | oscmd.Process.Pid, 167 | //oscmd.ProcessState.String(), 168 | } 169 | bm, _ := json.Marshal(progmsg) 170 | h.broadcastSys <- []byte(bm) 171 | } 172 | }() 173 | } 174 | 175 | func endProgress() { 176 | inProgress = false 177 | } 178 | 179 | // send back pseudo-status to browser while downloading in progress 180 | // so user doesn't think it's dead 181 | // this is not multi-threaded, but will work for now since 182 | // this is just a nice-to-have informational progress 183 | var inDownloadProgress bool 184 | 185 | func startDownloadProgress() { 186 | inDownloadProgress = true 187 | go func() { 188 | duration := 0 189 | for { 190 | time.Sleep(1 * time.Second) 191 | duration++ 192 | h.broadcastSys <- []byte("{\"ProgrammerStatus\": \"DownloadProgress\", \"Duration\": " + strconv.Itoa(duration) + "}") 193 | if inDownloadProgress == false { 194 | break 195 | } 196 | // break after 5 minutes max 197 | if duration > 60*5 { 198 | break 199 | } 200 | } 201 | }() 202 | } 203 | 204 | func endDownloadProgress() { 205 | inDownloadProgress = false 206 | } 207 | 208 | func formatCmdline(cmdline string, boardOptions map[string]string) (string, bool) { 209 | 210 | list := strings.Split(cmdline, "{") 211 | if len(list) == 1 { 212 | return cmdline, false 213 | } 214 | cmdline = "" 215 | for _, item := range list { 216 | item_s := strings.Split(item, "}") 217 | item = boardOptions[item_s[0]] 218 | if len(item_s) == 2 { 219 | cmdline += item + item_s[1] 220 | } else { 221 | if item != "" { 222 | cmdline += item 223 | } else { 224 | cmdline += item_s[0] 225 | } 226 | } 227 | } 228 | log.Println(cmdline) 229 | return cmdline, true 230 | } 231 | 232 | func containsStr(s []string, e string) bool { 233 | for _, a := range s { 234 | if strings.ToLower(a) == strings.ToLower(e) { 235 | return true 236 | } 237 | } 238 | return false 239 | } 240 | 241 | func findNewPortName(slice1 []string, slice2 []string) string { 242 | m := map[string]int{} 243 | 244 | for _, s1Val := range slice1 { 245 | m[s1Val] = 1 246 | } 247 | for _, s2Val := range slice2 { 248 | m[s2Val] = m[s2Val] + 1 249 | } 250 | 251 | for mKey, mVal := range m { 252 | if mVal == 1 { 253 | return mKey 254 | } 255 | } 256 | 257 | return "" 258 | } 259 | 260 | func assembleCompilerCommand(boardname string, portname string, filePath string) (bool, string, []string) { 261 | 262 | // get executable (self)path and use it as base for all other paths 263 | execPath, _ := osext.Executable() 264 | 265 | boardFields := strings.Split(boardname, ":") 266 | if len(boardFields) != 3 { 267 | h.broadcastSys <- []byte("Board need to be specified in core:architecture:name format") 268 | return false, "", nil 269 | } 270 | tempPath := (filepath.Dir(execPath) + "/" + boardFields[0] + "/hardware/" + boardFields[1] + "/boards.txt") 271 | file, err := os.Open(tempPath) 272 | if err != nil { 273 | //h.broadcastSys <- []byte("Could not find board: " + boardname) 274 | log.Println("Error:", err) 275 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not find board: " + boardname} 276 | mapB, _ := json.Marshal(mapD) 277 | h.broadcastSys <- mapB 278 | 279 | return false, "", nil 280 | } 281 | scanner := bufio.NewScanner(file) 282 | 283 | boardOptions := make(map[string]string) 284 | uploadOptions := make(map[string]string) 285 | 286 | for scanner.Scan() { 287 | // map everything matching with boardname 288 | if strings.Contains(scanner.Text(), boardFields[2]) { 289 | arr := strings.Split(scanner.Text(), "=") 290 | arr[0] = strings.Replace(arr[0], boardFields[2]+".", "", 1) 291 | boardOptions[arr[0]] = arr[1] 292 | } 293 | } 294 | 295 | if len(boardOptions) == 0 { 296 | errmsg := "Board " + boardFields[2] + " is not part of " + boardFields[0] + ":" + boardFields[1] 297 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": errmsg} 298 | mapB, _ := json.Marshal(mapD) 299 | h.broadcastSys <- mapB 300 | 301 | return false, "", nil 302 | } 303 | 304 | boardOptions["serial.port"] = portname 305 | boardOptions["serial.port.file"] = filepath.Base(portname) 306 | 307 | // filepath need special care; the project_name var is the filename minus its extension (hex or bin) 308 | // if we are going to modify standard IDE files we also could pass ALL filename 309 | filePath = strings.Trim(filePath, "\n") 310 | boardOptions["build.path"] = filepath.Dir(filePath) 311 | boardOptions["build.project_name"] = strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filepath.Base(filePath))) 312 | 313 | file.Close() 314 | 315 | // get infos about the programmer 316 | tempPath = (filepath.Dir(execPath) + "/" + boardFields[0] + "/hardware/" + boardFields[1] + "/platform.txt") 317 | file, err = os.Open(tempPath) 318 | if err != nil { 319 | errmsg := "Could not find board: " + boardname 320 | log.Println("Error:", err) 321 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": errmsg} 322 | mapB, _ := json.Marshal(mapD) 323 | h.broadcastSys <- mapB 324 | return false, "", nil 325 | } 326 | scanner = bufio.NewScanner(file) 327 | 328 | tool := boardOptions["upload.tool"] 329 | 330 | for scanner.Scan() { 331 | // map everything matching with upload 332 | if strings.Contains(scanner.Text(), tool) { 333 | arr := strings.Split(scanner.Text(), "=") 334 | uploadOptions[arr[0]] = arr[1] 335 | arr[0] = strings.Replace(arr[0], "tools."+tool+".", "", 1) 336 | boardOptions[arr[0]] = arr[1] 337 | // we have a "=" in command line 338 | if len(arr) > 2 { 339 | boardOptions[arr[0]] = arr[1] + "=" + arr[2] 340 | } 341 | } 342 | } 343 | file.Close() 344 | 345 | // multiple verisons of the same programmer can be handled if "version" is specified 346 | version := uploadOptions["runtime.tools."+tool+".version"] 347 | path := (filepath.Dir(execPath) + "/" + boardFields[0] + "/tools/" + tool + "/" + version) 348 | if err != nil { 349 | errmsg := "Could not find board: " + boardname 350 | log.Println("Error:", err) 351 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": errmsg} 352 | mapB, _ := json.Marshal(mapD) 353 | h.broadcastSys <- mapB 354 | return false, "", nil 355 | } 356 | 357 | boardOptions["runtime.tools."+tool+".path"] = path 358 | 359 | cmdline := boardOptions["upload.pattern"] 360 | // remove cmd.path as it is handled differently 361 | cmdline = strings.Replace(cmdline, "\"{cmd.path}\"", " ", 1) 362 | cmdline = strings.Replace(cmdline, "\"{path}/{cmd}\"", " ", 1) 363 | cmdline = strings.Replace(cmdline, "\"", "", -1) 364 | 365 | initialPortName := portname 366 | 367 | // some boards (eg. Leonardo, Yun) need a special procedure to enter bootloader 368 | if boardOptions["upload.use_1200bps_touch"] == "true" { 369 | // triggers bootloader mode 370 | // the portname could change in this occasion, so fail gently 371 | log.Println("Restarting in bootloader mode") 372 | 373 | mode := &serial.Mode{ 374 | BaudRate: 1200, 375 | Vmin: 1, 376 | Vtimeout: 0, 377 | } 378 | port, err := serial.OpenPort(portname, mode) 379 | if err != nil { 380 | log.Println(err) 381 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": err.Error()} 382 | mapB, _ := json.Marshal(mapD) 383 | h.broadcastSys <- mapB 384 | return false, "", nil 385 | } 386 | log.Println("Was able to open port in 1200 baud mode") 387 | //port.SetDTR(false) 388 | port.Close() 389 | time.Sleep(time.Second / 2.0) 390 | 391 | timeout := false 392 | go func() { 393 | time.Sleep(2 * time.Second) 394 | timeout = true 395 | }() 396 | 397 | // time.Sleep(time.Second / 4) 398 | // wait for port to reappear 399 | if boardOptions["upload.wait_for_upload_port"] == "true" { 400 | after_reset_ports, _ := serial.GetPortsList() 401 | log.Println(after_reset_ports) 402 | var ports []string 403 | for { 404 | ports, _ = serial.GetPortsList() 405 | log.Println(ports) 406 | time.Sleep(time.Millisecond * 200) 407 | portname = findNewPortName(ports, after_reset_ports) 408 | if portname != "" { 409 | break 410 | } 411 | if timeout { 412 | break 413 | } 414 | } 415 | } 416 | } 417 | 418 | if portname == "" { 419 | portname = initialPortName 420 | } 421 | 422 | // some boards (eg. TinyG) need a ctrl+x to enter bootloader 423 | if boardOptions["upload.send_ctrl_x_to_enter_bootloader"] == "true" { 424 | // triggers bootloader mode 425 | // the portname could change in this occasion, so fail gently 426 | log.Println("Sending ctrl+x to enter bootloader mode") 427 | 428 | // Extra protection code to ensure we aren't getting called from multiple threads 429 | if isRunning { 430 | mapD := map[string]string{"AssembleStatus": "ThreadError", "Msg": "You tried to run a 2nd (or further) Ctrl+x prep command while the 1st one was already running."} 431 | mapB, _ := json.Marshal(mapD) 432 | h.broadcastSys <- mapB 433 | return false, "", nil 434 | } 435 | 436 | isRunning = true 437 | 438 | mode := &serial.Mode{ 439 | BaudRate: 115200, 440 | Vmin: 1, 441 | Vtimeout: 0, 442 | } 443 | port, err := serial.OpenPort(portname, mode) 444 | if err != nil { 445 | log.Println(err) 446 | mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": err.Error()} 447 | mapB, _ := json.Marshal(mapD) 448 | h.broadcastSys <- mapB 449 | isRunning = false 450 | return false, "", nil 451 | } 452 | log.Println("Was able to open port in 115200 baud mode for ctrl+x send") 453 | 454 | port.Write([]byte(string(24))) // byte value which is ascii 24, ctrl+x 455 | port.Close() 456 | log.Println("Sent ctrl+x and then closed port. Go ahead and upload cuz we are in bootloader mode.") 457 | } 458 | isRunning = false 459 | 460 | boardOptions["serial.port"] = portname 461 | boardOptions["serial.port.file"] = filepath.Base(portname) 462 | 463 | // split the commandline in substrings and recursively replace mapped strings 464 | cmdlineSlice := strings.Split(cmdline, " ") 465 | var winded = true 466 | for index, _ := range cmdlineSlice { 467 | winded = true 468 | for winded != false { 469 | cmdlineSlice[index], winded = formatCmdline(cmdlineSlice[index], boardOptions) 470 | } 471 | } 472 | 473 | tool = (filepath.Dir(execPath) + "/" + boardFields[0] + "/tools/" + tool + "/bin/" + tool) 474 | // the file doesn't exist, we are on windows 475 | if _, err := os.Stat(tool); err != nil { 476 | tool = tool + ".exe" 477 | // convert all "/" to "\" 478 | tool = strings.Replace(tool, "/", "\\", -1) 479 | } 480 | 481 | // remove blanks from cmdlineSlice 482 | var cmdlineSliceOut []string 483 | for _, element := range cmdlineSlice { 484 | if element != "" { 485 | cmdlineSliceOut = append(cmdlineSliceOut, element) 486 | } 487 | } 488 | 489 | // add verbose 490 | cmdlineSliceOut = append(cmdlineSliceOut, "-v") 491 | 492 | log.Printf("Tool:%v, cmdline:%v\n", tool, cmdlineSliceOut) 493 | return (tool != ""), tool, cmdlineSliceOut 494 | } 495 | -------------------------------------------------------------------------------- /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 ( 32 | "strings" 33 | "sync" 34 | ) 35 | 36 | type queuenode struct { 37 | data string 38 | id string 39 | next *queuenode 40 | } 41 | 42 | // A go-routine safe FIFO (first in first out) data stucture. 43 | type Queue struct { 44 | head *queuenode 45 | tail *queuenode 46 | count int 47 | lock *sync.Mutex 48 | lenOfCmds int 49 | } 50 | 51 | // Creates a new pointer to a new queue. 52 | func NewQueue() *Queue { 53 | q := &Queue{} 54 | q.lock = &sync.Mutex{} 55 | return q 56 | } 57 | 58 | // Returns the number of elements in the queue (i.e. size/length) 59 | // go-routine safe. 60 | func (q *Queue) Len() int { 61 | q.lock.Lock() 62 | defer q.lock.Unlock() 63 | return q.count 64 | } 65 | 66 | // Returns the length of the data (gcode cmd) in the queue (i.e. size/length) 67 | // go-routine safe. 68 | func (q *Queue) LenOfCmds() int { 69 | q.lock.Lock() 70 | defer q.lock.Unlock() 71 | return q.lenOfCmds 72 | } 73 | 74 | // Pushes/inserts a value at the end/tail of the queue. 75 | // Note: this function does mutate the queue. 76 | // go-routine safe. 77 | func (q *Queue) Push(item string, id string) { 78 | q.lock.Lock() 79 | defer q.lock.Unlock() 80 | 81 | n := &queuenode{data: item, id: id} 82 | 83 | if q.tail == nil { 84 | q.tail = n 85 | q.head = n 86 | } else { 87 | q.tail.next = n 88 | q.tail = n 89 | } 90 | q.count++ 91 | q.lenOfCmds += len(item) 92 | } 93 | 94 | // Returns the value at the front of the queue. 95 | // i.e. the oldest value in the queue. 96 | // Note: this function does mutate the queue. 97 | // go-routine safe. 98 | func (q *Queue) Poll() (string, string) { 99 | q.lock.Lock() 100 | defer q.lock.Unlock() 101 | 102 | if q.head == nil { 103 | return "", "" 104 | } 105 | 106 | n := q.head 107 | q.head = n.next 108 | 109 | if q.head == nil { 110 | q.tail = nil 111 | } 112 | q.count-- 113 | q.lenOfCmds -= len(n.data) 114 | 115 | return n.data, n.id 116 | } 117 | 118 | // Returns a read value at the front of the queue. 119 | // i.e. the oldest value in the queue. 120 | // Note: this function does NOT mutate the queue. 121 | // go-routine safe. 122 | func (q *Queue) Peek() (string, string) { 123 | q.lock.Lock() 124 | defer q.lock.Unlock() 125 | 126 | n := q.head 127 | if n == nil || n.data == "" { 128 | return "", "" 129 | } 130 | 131 | return n.data, n.id 132 | } 133 | 134 | // Returns a read value at the front of the queue. 135 | // i.e. the oldest value in the queue. 136 | // Note: this function does NOT mutate the queue. 137 | // go-routine safe. 138 | func (q *Queue) Delete() { 139 | q.lock.Lock() 140 | defer q.lock.Unlock() 141 | 142 | q.head = nil 143 | q.tail = nil 144 | q.count = 0 145 | q.lenOfCmds = 0 146 | } 147 | 148 | func (q *Queue) DebugStr() string { 149 | q.lock.Lock() 150 | defer q.lock.Unlock() 151 | 152 | str := "" 153 | if q.head != nil { 154 | str += q.head.id + ":" + strings.Replace(q.head.data, "\n", "\\n", -1) 155 | 156 | n := q.head.next 157 | for n != nil { 158 | str += ", " + n.id + ":" + strings.Replace(n.data, "\n", "\\n", -1) 159 | n = n.next 160 | } 161 | } 162 | 163 | return str 164 | } 165 | -------------------------------------------------------------------------------- /queue_tid.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 queueTidNode struct { 34 | data string 35 | id string // their id so we can regurgitate 36 | tid int // our transaction id that tinyg will send back to us so we can match 37 | next *queueTidNode 38 | } 39 | 40 | // A go-routine safe FIFO (first in first out) data stucture. 41 | type QueueTid struct { 42 | head *queueTidNode 43 | tail *queueTidNode 44 | count int 45 | lock *sync.Mutex 46 | lenOfCmds int 47 | } 48 | 49 | // Creates a new pointer to a new queue. 50 | func NewQueueTid() *QueueTid { 51 | q := &QueueTid{} 52 | q.lock = &sync.Mutex{} 53 | return q 54 | } 55 | 56 | // Returns the number of elements in the queue (i.e. size/length) 57 | // go-routine safe. 58 | func (q *QueueTid) Len() int { 59 | q.lock.Lock() 60 | defer q.lock.Unlock() 61 | return q.count 62 | } 63 | 64 | // Returns the length of the data (gcode cmd) in the queue (i.e. size/length) 65 | // go-routine safe. 66 | func (q *QueueTid) LenOfCmds() int { 67 | q.lock.Lock() 68 | defer q.lock.Unlock() 69 | return q.lenOfCmds 70 | } 71 | 72 | // Pushes/inserts a value at the end/tail of the queue. 73 | // Note: this function does mutate the queue. 74 | // go-routine safe. 75 | func (q *QueueTid) Push(item string, id string, tid int) { 76 | q.lock.Lock() 77 | defer q.lock.Unlock() 78 | 79 | n := &queueTidNode{data: item, id: id, tid: tid} 80 | 81 | if q.tail == nil { 82 | q.tail = n 83 | q.head = n 84 | } else { 85 | q.tail.next = n 86 | q.tail = n 87 | } 88 | q.count++ 89 | q.lenOfCmds += len(item) 90 | } 91 | 92 | // Shifts/inserts a value at the front of the queue. 93 | // Note: this function does mutate the queue. 94 | // go-routine safe. 95 | func (q *QueueTid) Shift(item string, id string, tid int) { 96 | q.lock.Lock() 97 | defer q.lock.Unlock() 98 | 99 | n := &queueTidNode{data: item, id: id, tid: tid} 100 | 101 | n.next = q.head // make the current node at front of queue now be the "next" for our new node 102 | q.head = n // make the head be our newly defined node 103 | 104 | if q.tail == nil { 105 | q.tail = n.next // if the tail was empty, make our old head node be the tail 106 | //q.head = n 107 | } else { 108 | // do nothing?? 109 | //q.tail.next = n 110 | //q.tail = n 111 | } 112 | q.count++ 113 | q.lenOfCmds += len(item) 114 | } 115 | 116 | // Returns the value at the front of the queue. 117 | // i.e. the oldest value in the queue. 118 | // Note: this function does mutate the queue. 119 | // go-routine safe. 120 | func (q *QueueTid) Poll() (string, string, int) { 121 | q.lock.Lock() 122 | defer q.lock.Unlock() 123 | 124 | if q.head == nil { 125 | return "", "", -1 126 | } 127 | 128 | n := q.head 129 | q.head = n.next 130 | 131 | if q.head == nil { 132 | q.tail = nil 133 | } 134 | q.count-- 135 | q.lenOfCmds -= len(n.data) 136 | 137 | return n.data, n.id, n.tid 138 | } 139 | 140 | // Returns a read value at the front of the queue. 141 | // i.e. the oldest value in the queue. 142 | // Note: this function does NOT mutate the queue. 143 | // go-routine safe. 144 | func (q *QueueTid) Peek() (string, string, int) { 145 | q.lock.Lock() 146 | defer q.lock.Unlock() 147 | 148 | n := q.head 149 | if n == nil || n.data == "" { 150 | return "", "", -1 151 | } 152 | 153 | return n.data, n.id, n.tid 154 | } 155 | 156 | // Returns a read value at the front of the queue. 157 | // i.e. the oldest value in the queue. 158 | // Note: this function does NOT mutate the queue. 159 | // go-routine safe. 160 | func (q *QueueTid) Delete() { 161 | q.lock.Lock() 162 | defer q.lock.Unlock() 163 | 164 | q.head = nil 165 | q.tail = nil 166 | q.count = 0 167 | q.lenOfCmds = 0 168 | } 169 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export GITHUB_USER=chilipeppr 4 | 5 | export GITHUB_REPO=serial-port-json-server 6 | 7 | echo "About to create a Github release for Serial Port JSON Server" 8 | if [ "$1" = "" ]; then 9 | echo "You need to pass in the version number as the first parameter like ./release 1.87" 10 | exit 11 | fi 12 | 13 | echo "" 14 | echo "Before creating release" 15 | bin/github-release info 16 | 17 | bin/github-release release \ 18 | --tag v$1 \ 19 | --name "Serial Port JSON Server" \ 20 | --description "A server for the Internet of Things. Lets you serve up serial ports to websockets so you can write front-end apps for your IoT devices in the browser." \ 21 | 22 | echo "" 23 | echo "After creating release" 24 | bin/github-release info 25 | 26 | echo "" 27 | echo "Uploading binaries" 28 | 29 | # upload a file, for example the OSX/AMD64 binary of my gofinance app 30 | bin/github-release upload \ 31 | --tag v$1 \ 32 | --name "serial-port-json-server-$1_linux_amd64.tar.gz" \ 33 | --file snapshot/serial-port-json-server-$1_linux_amd64.tar.gz 34 | bin/github-release upload \ 35 | --tag v$1 \ 36 | --name "serial-port-json-server-$1_linux_386.tar.gz" \ 37 | --file snapshot/serial-port-json-server-$1_linux_386.tar.gz 38 | bin/github-release upload \ 39 | --tag v$1 \ 40 | --name "serial-port-json-server-$1_linux_arm.tar.gz" \ 41 | --file snapshot/serial-port-json-server-$1_linux_arm.tar.gz 42 | bin/github-release upload \ 43 | --tag v$1 \ 44 | --name "serial-port-json-server-$1_windows_386.zip" \ 45 | --file snapshot/serial-port-json-server-$1_windows_386.zip 46 | bin/github-release upload \ 47 | --tag v$1 \ 48 | --name "serial-port-json-server-$1_windows_amd64.zip" \ 49 | --file snapshot/serial-port-json-server-$1_windows_amd64.zip 50 | 51 | echo "" 52 | echo "Done" -------------------------------------------------------------------------------- /seriallist.go: -------------------------------------------------------------------------------- 1 | // Supports Windows, Linux, Mac, and Raspberry Pi 2 | 3 | package main 4 | 5 | import ( 6 | "encoding/xml" 7 | "strings" 8 | //"fmt" 9 | "github.com/facchinm/go-serial" 10 | //"io/ioutil" 11 | "log" 12 | //"os" 13 | "regexp" 14 | ) 15 | 16 | type OsSerialPort struct { 17 | Name string 18 | FriendlyName string 19 | RelatedNames []string // for some devices there are 2 or more ports, i.e. TinyG v9 has 2 serial ports 20 | SerialNumber string 21 | DeviceClass string 22 | Manufacturer string 23 | Product string 24 | IdProduct string 25 | IdVendor string 26 | } 27 | 28 | func GetList() ([]OsSerialPort, error) { 29 | 30 | //log.Println("Doing GetList()") 31 | 32 | ports, err := serial.GetPortsList() 33 | 34 | arrPorts := []OsSerialPort{} 35 | for _, element := range ports { 36 | friendly := strings.Replace(element, "/dev/", "", -1) 37 | arrPorts = append(arrPorts, OsSerialPort{Name: element, FriendlyName: friendly}) 38 | } 39 | 40 | // see if we should filter the list 41 | if len(*regExpFilter) > 0 { 42 | // yes, user asked for a filter 43 | reFilter := regexp.MustCompile("(?i)" + *regExpFilter) 44 | 45 | newarrPorts := []OsSerialPort{} 46 | for _, element := range arrPorts { 47 | // if matches regex, include 48 | if reFilter.MatchString(element.Name) { 49 | newarrPorts = append(newarrPorts, element) 50 | } else if reFilter.MatchString(element.FriendlyName) { 51 | newarrPorts = append(newarrPorts, element) 52 | } else { 53 | log.Printf("serial port did not match. port: %v\n", element) 54 | } 55 | 56 | } 57 | arrPorts = newarrPorts 58 | } 59 | 60 | //log.Printf("Done doing GetList(). arrPorts:%v\n", arrPorts) 61 | 62 | return arrPorts, err 63 | } 64 | 65 | func GetMetaList() ([]OsSerialPort, error) { 66 | metaportlist, err := getMetaList() 67 | if err.Err != nil { 68 | return nil, err.Err 69 | } 70 | return metaportlist, err.Err 71 | } 72 | 73 | func GetFriendlyName(portname string) string { 74 | log.Println("GetFriendlyName from base class") 75 | return "" 76 | } 77 | 78 | type Dict struct { 79 | Keys []string `xml:"key"` 80 | Arrays []Dict `xml:"array"` 81 | Strings []string `xml:"string"` 82 | Dicts []Dict `xml:"dict"` 83 | } 84 | 85 | type Result struct { 86 | XMLName xml.Name `xml:"plist"` 87 | //Strings []string `xml:"dict>string"` 88 | Dict `xml:"dict"` 89 | //Phone string 90 | //Groups []string `xml:"Group>Value"` 91 | } 92 | 93 | /* 94 | func GetDarwinMeta() { 95 | xmlFile, err := os.Open("out.xml") 96 | if err != nil { 97 | fmt.Println("Error opening file:", err) 98 | return 99 | } 100 | defer xmlFile.Close() 101 | 102 | XMLdata, _ := ioutil.ReadAll(xmlFile) 103 | 104 | var v Result 105 | //v := Result{} 106 | xml.Unmarshal(XMLdata, &v) 107 | log.Printf("Result:%v", len(v.Dicts[0].Arrays)) //, v.Dict.Dicts[0].Keys) // Dicts[0] .Keys[0]) 108 | log.Printf("Result:%v", v.Dicts[0].Arrays[1].Dicts[0].Keys[0]) // Dicts[0] .Keys[0]) 109 | log.Printf("Result:%v", v.Dicts[0].Keys[0]) // Dicts[0] .Keys[0]) 110 | 111 | } 112 | */ 113 | -------------------------------------------------------------------------------- /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 | //"encoding/xml" 14 | "io/ioutil" 15 | ) 16 | 17 | func getMetaList() ([]OsSerialPort, os.SyscallError) { 18 | //return getListViaWmiPnpEntity() 19 | return getListViaTtyList() 20 | 21 | // query the out.xml file for now, but in real life 22 | // we would run the ioreg -a -p IOUSB command to get the output 23 | // and then parse it 24 | 25 | } 26 | 27 | func getListViaTtyList() ([]OsSerialPort, os.SyscallError) { 28 | var err os.SyscallError 29 | 30 | log.Println("getting serial list on darwin") 31 | 32 | // make buffer of 100 max serial ports 33 | // return a slice 34 | list := make([]OsSerialPort, 100) 35 | 36 | files, _ := ioutil.ReadDir("/dev/") 37 | ctr := 0 38 | for _, f := range files { 39 | if strings.HasPrefix(f.Name(), "tty.") { 40 | // it is a legitimate serial port 41 | list[ctr].Name = "/dev/" + f.Name() 42 | list[ctr].FriendlyName = f.Name() 43 | log.Println("Added serial port to list: ", list[ctr]) 44 | ctr++ 45 | } 46 | // stop-gap in case going beyond 100 (which should never happen) 47 | // i mean, really, who has more than 100 serial ports? 48 | if ctr > 99 { 49 | ctr = 99 50 | } 51 | //fmt.Println(f.Name()) 52 | //fmt.Println(f.) 53 | } 54 | /* 55 | list := make([]OsSerialPort, 3) 56 | list[0].Name = "tty.serial1" 57 | list[0].FriendlyName = "tty.serial1" 58 | list[1].Name = "tty.serial2" 59 | list[1].FriendlyName = "tty.serial2" 60 | list[2].Name = "tty.Bluetooth-Modem" 61 | list[2].FriendlyName = "tty.Bluetooth-Modem" 62 | */ 63 | 64 | return list[0:ctr], err 65 | } 66 | -------------------------------------------------------------------------------- /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 getMetaList() ([]OsSerialPort, os.SyscallError) { 24 | 25 | //return getListViaTtyList() 26 | return getAllPortsViaManufacturer() 27 | } 28 | 29 | func getListViaTtyList() ([]OsSerialPort, os.SyscallError) { 30 | var err os.SyscallError 31 | 32 | //log.Println("getting serial list on darwin") 33 | 34 | // make buffer of 1000 max serial ports 35 | // return a slice 36 | list := make([]OsSerialPort, 1000) 37 | 38 | files, _ := ioutil.ReadDir("/dev/") 39 | ctr := 0 40 | for _, f := range files { 41 | if strings.HasPrefix(f.Name(), "tty") { 42 | // it is a legitimate serial port 43 | list[ctr].Name = "/dev/" + f.Name() 44 | list[ctr].FriendlyName = f.Name() 45 | 46 | // see if we can get a better friendly name 47 | //friendly, ferr := getMetaDataForPort(f.Name()) 48 | //if ferr == nil { 49 | // list[ctr].FriendlyName = friendly 50 | //} 51 | 52 | //log.Println("Added serial port to list: ", list[ctr]) 53 | ctr++ 54 | } 55 | // stop-gap in case going beyond 1000 (which should never happen) 56 | // i mean, really, who has more than 1000 serial ports? 57 | if ctr > 999 { 58 | ctr = 999 59 | } 60 | //fmt.Println(f.Name()) 61 | //fmt.Println(f.) 62 | } 63 | /* 64 | list := make([]OsSerialPort, 3) 65 | list[0].Name = "tty.serial1" 66 | list[0].FriendlyName = "tty.serial1" 67 | list[1].Name = "tty.serial2" 68 | list[1].FriendlyName = "tty.serial2" 69 | list[2].Name = "tty.Bluetooth-Modem" 70 | list[2].FriendlyName = "tty.Bluetooth-Modem" 71 | */ 72 | 73 | return list[0:ctr], err 74 | } 75 | 76 | type deviceClass struct { 77 | BaseClass int 78 | Description string 79 | } 80 | 81 | func getDeviceClassList() { 82 | // TODO: take list from http://www.usb.org/developers/defined_class 83 | // and create mapping. 84 | } 85 | 86 | func getAllPortsViaManufacturer() ([]OsSerialPort, os.SyscallError) { 87 | var err os.SyscallError 88 | var list []OsSerialPort 89 | 90 | // LOOK FOR THE WORD MANUFACTURER 91 | // search /sys folder 92 | files := findFiles("/sys", "^manufacturer$") 93 | 94 | // LOOK FOR THE WORD PRODUCT 95 | filesFromProduct := findFiles("/sys", "^product$") 96 | 97 | // append both arrays so we have one (then we'll have to de-dupe) 98 | files = append(files, filesFromProduct...) 99 | 100 | // Now get directories from each file 101 | re := regexp.MustCompile("/(manufacturer|product)$") 102 | var mapfile map[string]int 103 | mapfile = make(map[string]int) 104 | for _, element := range files { 105 | // make this directory be a key so it's unique. increment int so we know 106 | // for debug how many times this directory appeared 107 | mapfile[re.ReplaceAllString(element, "")]++ 108 | } 109 | 110 | // sort the directory keys 111 | mapfilekeys := make([]string, len(mapfile)) 112 | i := 0 113 | for key, _ := range mapfile { 114 | mapfilekeys[i] = key 115 | i++ 116 | } 117 | sort.Strings(mapfilekeys) 118 | log.Printf("The list of directories with serial port device data:%v", mapfilekeys) 119 | 120 | //reRemoveManuf, _ := regexp.Compile("/manufacturer$") 121 | reNewLine, _ := regexp.Compile("\n") 122 | 123 | // loop on unique directories 124 | for _, directory := range mapfilekeys { 125 | 126 | if len(directory) == 0 { 127 | continue 128 | } 129 | 130 | // search folder that had manufacturer file in it 131 | log.Printf("\tDirectory searching: %v", directory) 132 | 133 | // for each manufacturer or product file, we need to read the val from the file 134 | // but more importantly find the tty ports for this directory 135 | 136 | // for example, for the TinyG v9 which creates 2 ports, the cmd: 137 | // find /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/ -name tty[AU]* -print 138 | // will result in: 139 | /* 140 | /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3:1.0/tty/ttyACM0 141 | /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3:1.2/tty/ttyACM1 142 | */ 143 | 144 | // figure out the directory 145 | //directory := reRemoveManuf.ReplaceAllString(element, "") 146 | 147 | // read the device class so we can remove stuff we don't want like hubs 148 | deviceClassBytes, errRead4 := ioutil.ReadFile(directory + "/bDeviceClass") 149 | deviceClass := "" 150 | if errRead4 != nil { 151 | // there must be a permission issue or the file doesn't exist 152 | //log.Printf("Problem reading in serial number text file. Permissions maybe? err:%v", errRead3) 153 | //return nil, err 154 | } 155 | deviceClass = string(deviceClassBytes) 156 | deviceClass = reNewLine.ReplaceAllString(deviceClass, "") 157 | 158 | if deviceClass == "09" || deviceClass == "9" || deviceClass == "09h" { 159 | log.Printf("This is a hub, so skipping. %v", directory) 160 | continue 161 | } 162 | 163 | // read the manufacturer 164 | manufBytes, errRead := ioutil.ReadFile(directory + "/manufacturer") 165 | manuf := "" 166 | if errRead != nil { 167 | // the file could possibly just not exist, which is normal 168 | log.Printf("Problem reading in manufacturer text file. It does not exist or Permissions maybe? err:%v", errRead) 169 | //return nil, err 170 | //continue 171 | } 172 | manuf = string(manufBytes) 173 | manuf = reNewLine.ReplaceAllString(manuf, "") 174 | 175 | // read the product 176 | productBytes, errRead2 := ioutil.ReadFile(directory + "/product") 177 | product := "" 178 | if errRead2 != nil { 179 | // the file could possibly just not exist, which is normal 180 | //log.Printf("Problem reading in product text file. Permissions maybe? err:%v", errRead2) 181 | //return nil, err 182 | } 183 | product = string(productBytes) 184 | product = reNewLine.ReplaceAllString(product, "") 185 | 186 | // read the serial number 187 | serialNumBytes, errRead3 := ioutil.ReadFile(directory + "/serial") 188 | serialNum := "" 189 | if errRead3 != nil { 190 | // the file could possibly just not exist, which is normal 191 | //log.Printf("Problem reading in serial number text file. Permissions maybe? err:%v", errRead3) 192 | //return nil, err 193 | } 194 | serialNum = string(serialNumBytes) 195 | serialNum = reNewLine.ReplaceAllString(serialNum, "") 196 | 197 | // read idvendor 198 | idVendorBytes, _ := ioutil.ReadFile(directory + "/idVendor") 199 | idVendor := "" 200 | idVendor = reNewLine.ReplaceAllString(string(idVendorBytes), "") 201 | 202 | // read idProduct 203 | idProductBytes, _ := ioutil.ReadFile(directory + "/idProduct") 204 | idProduct := "" 205 | idProduct = reNewLine.ReplaceAllString(string(idProductBytes), "") 206 | 207 | log.Printf("%v : %v (%v) DevClass:%v", manuf, product, serialNum, deviceClass) 208 | 209 | // -name tty[AU]* -print 210 | filesTty := findDirs(directory, "^tty(A|U).*") 211 | 212 | // generate a unique list of tty ports below 213 | //var ttyPorts []string 214 | var m map[string]int 215 | m = make(map[string]int) 216 | for _, fileTty := range filesTty { 217 | if len(fileTty) == 0 { 218 | continue 219 | } 220 | log.Printf("\t%v", fileTty) 221 | ttyPort := regexp.MustCompile("^.*/").ReplaceAllString(fileTty, "") 222 | ttyPort = reNewLine.ReplaceAllString(ttyPort, "") 223 | m[ttyPort]++ 224 | //ttyPorts = append(ttyPorts, ttyPort) 225 | } 226 | log.Printf("\tlist of ports on this. map:%v\n", m) 227 | log.Printf("\t.") 228 | //sort.Strings(ttyPorts) 229 | 230 | // create order array of ttyPorts so they're in order when 231 | // we send back via json. this makes for more human friendly reading 232 | // cuz anytime you do a hash map you can get out of order 233 | ttyPorts := []string{} 234 | for key, _ := range m { 235 | ttyPorts = append(ttyPorts, key) 236 | } 237 | sort.Strings(ttyPorts) 238 | 239 | // we now have a very nice list of ttyports for this device. many are just 1 port 240 | // however, for some advanced devices there are 2 or more ports associated and 241 | // we have this data correct now, so build out the final OsSerialPort list 242 | for _, key := range ttyPorts { 243 | listitem := OsSerialPort{ 244 | Name: "/dev/" + key, 245 | FriendlyName: manuf, // + " " + product, 246 | SerialNumber: serialNum, 247 | DeviceClass: deviceClass, 248 | Manufacturer: manuf, 249 | Product: product, 250 | IdVendor: idVendor, 251 | IdProduct: idProduct, 252 | } 253 | if len(product) > 0 { 254 | listitem.FriendlyName += " " + product 255 | } 256 | 257 | listitem.FriendlyName += " (" + key + ")" 258 | listitem.FriendlyName = friendlyNameCleanup(listitem.FriendlyName) 259 | 260 | // append related tty ports 261 | for _, keyRelated := range ttyPorts { 262 | if key == keyRelated { 263 | continue 264 | } 265 | listitem.RelatedNames = append(listitem.RelatedNames, "/dev/"+keyRelated) 266 | } 267 | list = append(list, listitem) 268 | } 269 | 270 | } 271 | 272 | // sort ports by item.Name 273 | sort.Sort(ByName(list)) 274 | 275 | log.Printf("Final port list: %v", list) 276 | return list, err 277 | } 278 | 279 | func findFiles(rootpath string, regexpstr string) []string { 280 | 281 | var matchedFiles []string 282 | re := regexp.MustCompile(regexpstr) 283 | numScanned := 0 284 | filepath.Walk(rootpath, func(path string, fi os.FileInfo, _ error) error { 285 | numScanned++ 286 | 287 | if fi.IsDir() == false && re.MatchString(fi.Name()) == true { 288 | matchedFiles = append(matchedFiles, path) 289 | } 290 | return nil 291 | }) 292 | log.Printf("Rootpath:%v, Numscanned:%v\nMatchedfiles:\n%v", rootpath, numScanned, strings.Join(matchedFiles, "\n")) 293 | return matchedFiles 294 | } 295 | 296 | func findDirs(rootpath string, regexpstr string) []string { 297 | 298 | var matchedFiles []string 299 | re := regexp.MustCompile(regexpstr) 300 | numScanned := 0 301 | filepath.Walk(rootpath, func(path string, fi os.FileInfo, _ error) error { 302 | numScanned++ 303 | 304 | if fi.IsDir() == true && re.MatchString(fi.Name()) == true { 305 | matchedFiles = append(matchedFiles, path) 306 | } 307 | return nil 308 | }) 309 | log.Printf("Rootpath:%v, Numscanned:%v\nMatcheddirs:\n%v", rootpath, numScanned, strings.Join(matchedFiles, "\n")) 310 | return matchedFiles 311 | } 312 | 313 | // ByAge implements sort.Interface for []Person based on 314 | // the Age field. 315 | type ByName []OsSerialPort 316 | 317 | func (a ByName) Len() int { return len(a) } 318 | func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 319 | func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } 320 | 321 | func friendlyNameCleanup(fnin string) (fnout string) { 322 | // This is an industry intelligence method to just cleanup common names 323 | // out there so we don't get ugly friendly names back 324 | fnout = regexp.MustCompile("\\(www.arduino.cc\\)").ReplaceAllString(fnin, "") 325 | fnout = regexp.MustCompile("Arduino\\s+Arduino").ReplaceAllString(fnout, "Arduino") 326 | fnout = regexp.MustCompile("\\s+").ReplaceAllString(fnout, " ") // multi space to single space 327 | fnout = regexp.MustCompile("^\\s+|\\s+$").ReplaceAllString(fnout, "") // trim 328 | return fnout 329 | } 330 | 331 | func getMetaDataForPort(port string) (string, error) { 332 | // search the folder structure on linux for this port name 333 | 334 | // search /sys folder 335 | oscmd := exec.Command("find", "/sys/devices", "-name", port, "-print") //, "2>", "/dev/null") 336 | 337 | // Stdout buffer 338 | cmdOutput := &bytes.Buffer{} 339 | // Attach buffer to command 340 | oscmd.Stdout = cmdOutput 341 | 342 | err := oscmd.Start() 343 | if err != nil { 344 | log.Fatal(err) 345 | } 346 | log.Printf("Waiting for command to finish... %v", oscmd) 347 | 348 | err = oscmd.Wait() 349 | 350 | if err != nil { 351 | log.Printf("Command finished with error: %v", err) 352 | } else { 353 | log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) 354 | // analyze stdin 355 | 356 | } 357 | 358 | return port + "coolio", nil 359 | } 360 | 361 | func getMetaDataForPortOld(port string) (string, error) { 362 | // search the folder structure on linux for this port name 363 | 364 | // search /sys folder 365 | oscmd := exec.Command("find", "/sys/devices", "-name", port, "-print") //, "2>", "/dev/null") 366 | 367 | // Stdout buffer 368 | cmdOutput := &bytes.Buffer{} 369 | // Attach buffer to command 370 | oscmd.Stdout = cmdOutput 371 | 372 | err := oscmd.Start() 373 | if err != nil { 374 | log.Fatal(err) 375 | } 376 | log.Printf("Waiting for command to finish... %v", oscmd) 377 | 378 | err = oscmd.Wait() 379 | 380 | if err != nil { 381 | log.Printf("Command finished with error: %v", err) 382 | } else { 383 | log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) 384 | // analyze stdin 385 | 386 | } 387 | 388 | return port + "coolio", nil 389 | } 390 | -------------------------------------------------------------------------------- /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/go-ole/go-ole" 9 | "github.com/go-ole/go-ole/oleutil" 10 | //"github.com/tarm/goserial" 11 | //"github.com/johnlauer/goserial" 12 | "log" 13 | "os" 14 | "strings" 15 | 16 | "github.com/facchinm/go-serial" 17 | //"encoding/binary" 18 | "strconv" 19 | "sync" 20 | //"syscall" 21 | "regexp" 22 | ) 23 | 24 | var ( 25 | serialListWindowsWg sync.WaitGroup 26 | isSerialListWait bool 27 | ) 28 | 29 | func getMetaList() ([]OsSerialPort, os.SyscallError) { 30 | // use a queue to do this to avoid conflicts 31 | // we've been getting crashes when this getList is requested 32 | // too many times too fast. i think it's something to do with 33 | // the unsafe syscalls overwriting memory 34 | 35 | // see if we are in a waitGroup and if so exit cuz it was 36 | // causing a crash 37 | if isSerialListWait { 38 | var err os.SyscallError 39 | list := make([]OsSerialPort, 0) 40 | return list, err 41 | } 42 | 43 | // this will only block if waitgroupctr > 0. so first time 44 | // in shouldn't block 45 | serialListWindowsWg.Wait() 46 | isSerialListWait = true 47 | 48 | serialListWindowsWg.Add(1) 49 | arr, sysCallErr := getListViaWmiPnpEntity() 50 | isSerialListWait = false 51 | serialListWindowsWg.Done() 52 | //arr = make([]OsSerialPort, 0) 53 | 54 | // see if array has any data, if not fallback to the traditional 55 | // com port list model 56 | /* 57 | if len(arr) == 0 { 58 | // assume it failed 59 | arr, sysCallErr = getListViaOpen() 60 | } 61 | */ 62 | 63 | // see if array has any data, if not fallback to looking at 64 | // the registry list 65 | /* 66 | arr = make([]OsSerialPort, 0) 67 | if len(arr) == 0 { 68 | // assume it failed 69 | arr, sysCallErr = getListViaRegistry() 70 | } 71 | */ 72 | 73 | return arr, sysCallErr 74 | } 75 | 76 | func getListSynchronously() { 77 | 78 | } 79 | 80 | func getListViaWmiPnpEntity() ([]OsSerialPort, os.SyscallError) { 81 | 82 | //log.Println("Doing getListViaWmiPnpEntity()") 83 | 84 | // this method panics a lot and i'm not sure why, just catch 85 | // the panic and return empty list 86 | defer func() { 87 | if e := recover(); e != nil { 88 | // e is the interface{} typed-value we passed to panic() 89 | log.Println("Got panic: ", e) // Prints "Whoops: boom!" 90 | } 91 | }() 92 | 93 | var err os.SyscallError 94 | 95 | //var friendlyName string 96 | 97 | // init COM, oh yeah 98 | ole.CoInitialize(0) 99 | defer ole.CoUninitialize() 100 | 101 | unknown, _ := oleutil.CreateObject("WbemScripting.SWbemLocator") 102 | defer unknown.Release() 103 | 104 | wmi, _ := unknown.QueryInterface(ole.IID_IDispatch) 105 | defer wmi.Release() 106 | 107 | // service is a SWbemServices 108 | serviceRaw, _ := oleutil.CallMethod(wmi, "ConnectServer") 109 | service := serviceRaw.ToIDispatch() 110 | defer service.Release() 111 | 112 | // result is a SWBemObjectSet 113 | //pname := syscall.StringToUTF16("SELECT * FROM Win32_PnPEntity where Name like '%" + "COM35" + "%'") 114 | pname := "SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0 and Name like '%(COM%'" 115 | //pname := "SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0" 116 | resultRaw, err2 := oleutil.CallMethod(service, "ExecQuery", pname) 117 | //log.Println("Got result from oleutil.CallMethod") 118 | if err2 != nil { 119 | // we got back an error or empty list 120 | log.Printf("Got an error back from oleutil.CallMethod. err:%v", err2) 121 | return nil, err 122 | } 123 | 124 | result := resultRaw.ToIDispatch() 125 | defer result.Release() 126 | 127 | countVar, _ := oleutil.GetProperty(result, "Count") 128 | count := int(countVar.Val) 129 | 130 | list := make([]OsSerialPort, count) 131 | 132 | for i := 0; i < count; i++ { 133 | 134 | // items we're looping thru look like below and 135 | // thus we can query for any of these names 136 | /* 137 | __GENUS : 2 138 | __CLASS : Win32_PnPEntity 139 | __SUPERCLASS : CIM_LogicalDevice 140 | __DYNASTY : CIM_ManagedSystemElement 141 | __RELPATH : Win32_PnPEntity.DeviceID="USB\\VID_1D50&PID_606D&MI_02\\6&2F09EA14&0&0002" 142 | __PROPERTY_COUNT : 24 143 | __DERIVATION : {CIM_LogicalDevice, CIM_LogicalElement, CIM_ManagedSystemElement} 144 | __SERVER : JOHN-ATIV 145 | __NAMESPACE : root\cimv2 146 | __PATH : \\JOHN-ATIV\root\cimv2:Win32_PnPEntity.DeviceID="USB\\VID_1D50&PID_606D&MI_02\\6&2F09EA14 147 | &0&0002" 148 | Availability : 149 | Caption : TinyG v2 (Data Channel) (COM12) 150 | ClassGuid : {4d36e978-e325-11ce-bfc1-08002be10318} 151 | CompatibleID : {USB\Class_02&SubClass_02&Prot_01, USB\Class_02&SubClass_02, USB\Class_02} 152 | ConfigManagerErrorCode : 0 153 | ConfigManagerUserConfig : False 154 | CreationClassName : Win32_PnPEntity 155 | Description : TinyG v2 (Data Channel) 156 | DeviceID : USB\VID_1D50&PID_606D&MI_02\6&2F09EA14&0&0002 157 | ErrorCleared : 158 | ErrorDescription : 159 | HardwareID : {USB\VID_1D50&PID_606D&REV_0097&MI_02, USB\VID_1D50&PID_606D&MI_02} 160 | InstallDate : 161 | LastErrorCode : 162 | Manufacturer : Synthetos (www.synthetos.com) 163 | Name : TinyG v2 (Data Channel) (COM12) 164 | PNPDeviceID : USB\VID_1D50&PID_606D&MI_02\6&2F09EA14&0&0002 165 | PowerManagementCapabilities : 166 | PowerManagementSupported : 167 | Service : usbser 168 | Status : OK 169 | StatusInfo : 170 | SystemCreationClassName : Win32_ComputerSystem 171 | SystemName : JOHN-ATIV 172 | PSComputerName : JOHN-ATIV 173 | */ 174 | 175 | // item is a SWbemObject, but really a Win32_Process 176 | itemRaw, _ := oleutil.CallMethod(result, "ItemIndex", i) 177 | item := itemRaw.ToIDispatch() 178 | defer item.Release() 179 | 180 | asString, _ := oleutil.GetProperty(item, "Name") 181 | 182 | //log.Println(asString.ToString()) 183 | 184 | // get the com port 185 | //if false { 186 | s := strings.Split(asString.ToString(), "(COM")[1] 187 | s = "COM" + s 188 | s = strings.Split(s, ")")[0] 189 | list[i].Name = s 190 | list[i].FriendlyName = asString.ToString() 191 | //} 192 | 193 | // get the deviceid so we can figure out related ports 194 | // it will look similar to 195 | // USB\VID_1D50&PID_606D&MI_00\6&2F09EA14&0&0000 196 | deviceIdStr, _ := oleutil.GetProperty(item, "DeviceID") 197 | devIdItems := strings.Split(deviceIdStr.ToString(), "&") 198 | log.Printf("DeviceId elements:%v", devIdItems) 199 | if len(devIdItems) > 3 { 200 | list[i].SerialNumber = devIdItems[3] 201 | //list[i].IdProduct = strings.Replace(devIdItems[1], "PID_", "", 1) 202 | //list[i].IdVendor = strings.Replace(devIdItems[0], "USB\\VID_", "", 1) 203 | } else { 204 | list[i].SerialNumber = deviceIdStr.ToString() 205 | } 206 | 207 | pidMatch := regexp.MustCompile("PID_(....)").FindAllStringSubmatch(deviceIdStr.ToString(), -1) 208 | if len(pidMatch) > 0 { 209 | if len(pidMatch[0]) > 1 { 210 | list[i].IdProduct = pidMatch[0][1] 211 | } 212 | } 213 | vidMatch := regexp.MustCompile("VID_(....)").FindAllStringSubmatch(deviceIdStr.ToString(), -1) 214 | if len(vidMatch) > 0 { 215 | if len(vidMatch[0]) > 1 { 216 | list[i].IdVendor = vidMatch[0][1] 217 | } 218 | } 219 | 220 | manufStr, _ := oleutil.GetProperty(item, "Manufacturer") 221 | list[i].Manufacturer = manufStr.ToString() 222 | descStr, _ := oleutil.GetProperty(item, "Description") 223 | list[i].Product = descStr.ToString() 224 | //classStr, _ := oleutil.GetProperty(item, "CreationClassName") 225 | //list[i].DeviceClass = classStr.ToString() 226 | 227 | } 228 | 229 | for index, element := range list { 230 | 231 | log.Printf("index:%v, name:%v, friendly:%v ", index, element.Name, element.FriendlyName) 232 | log.Printf("pid:%v, vid:%v", element.IdProduct, element.IdVendor) 233 | 234 | for index2, element2 := range list { 235 | if index == index2 { 236 | continue 237 | } 238 | if element.SerialNumber == element2.SerialNumber { 239 | log.Printf("Found related element1:%v, element2:%v", element, element2) 240 | list[index].RelatedNames = append(list[index].RelatedNames, element2.Name) 241 | } 242 | } 243 | 244 | } 245 | 246 | return list, err 247 | } 248 | 249 | func getListViaOpen() ([]OsSerialPort, os.SyscallError) { 250 | 251 | log.Println("Doing getListViaOpen(). Will try to open COM1 to COM99.") 252 | var err os.SyscallError 253 | list := make([]OsSerialPort, 100) 254 | var igood int = 0 255 | for i := 0; i < 100; i++ { 256 | prtname := "COM" + strconv.Itoa(i) 257 | //conf := &serial.Config{Name: prtname, Baud: 1200} 258 | mode := &serial.Mode{ 259 | BaudRate: 1200, 260 | Vmin: 0, 261 | Vtimeout: 10, 262 | } 263 | sp, err := serial.OpenPort(prtname, mode) 264 | //log.Println("Just tried to open port", prtname) 265 | if err == nil { 266 | //log.Println("Able to open port", prtname) 267 | list[igood].Name = prtname 268 | sp.Close() 269 | list[igood].FriendlyName = prtname 270 | //list[igood].FriendlyName = getFriendlyName(prtname) 271 | igood++ 272 | } 273 | } 274 | for index, element := range list[:igood] { 275 | log.Println("index ", index, " element ", element.Name+ 276 | " friendly ", element.FriendlyName) 277 | } 278 | return list[:igood], err 279 | } 280 | 281 | /* 282 | func getListViaRegistry() ([]OsSerialPort, os.SyscallError) { 283 | 284 | log.Println("Doing getListViaRegistry()") 285 | var err os.SyscallError 286 | var root win.HKEY 287 | rootpath, _ := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM") 288 | log.Println(win.RegOpenKeyEx(win.HKEY_LOCAL_MACHINE, rootpath, 0, win.KEY_READ, &root)) 289 | 290 | var name_length uint32 = 72 291 | var key_type uint32 292 | var lpDataLength uint32 = 72 293 | var zero_uint uint32 = 0 294 | name := make([]uint16, 72) 295 | lpData := make([]byte, 72) 296 | 297 | var retcode int32 298 | retcode = 0 299 | for retcode == 0 { 300 | retcode = win.RegEnumValue(root, zero_uint, &name[0], &name_length, nil, &key_type, &lpData[0], &lpDataLength) 301 | log.Println("Retcode:", retcode) 302 | log.Println("syscall name: "+syscall.UTF16ToString(name[:name_length-2])+"---- name_length:", name_length) 303 | log.Println("syscall lpdata:"+string(lpData[:lpDataLength-2])+"--- lpDataLength:", lpDataLength) 304 | //log.Println() 305 | zero_uint++ 306 | } 307 | win.RegCloseKey(root) 308 | win.RegOpenKeyEx(win.HKEY_LOCAL_MACHINE, rootpath, 0, win.KEY_READ, &root) 309 | 310 | list := make([]OsSerialPort, zero_uint) 311 | var i uint32 = 0 312 | for i = 0; i < zero_uint; i++ { 313 | win.RegEnumValue(root, i-1, &name[0], &name_length, nil, &key_type, &lpData[0], &lpDataLength) 314 | //name := string(lpData[:lpDataLength]) 315 | //name = name[:strings.Index(name, '\0')] 316 | //nameb := []byte(strings.TrimSpace(string(lpData[:lpDataLength]))) 317 | //list[i].Name = string(nameb) 318 | //list[i].Name = string(name[:strings.Index(name, "\0")]) 319 | //list[i].Name = fmt.Sprintf("%s", string(lpData[:lpDataLength-1])) 320 | pname := make([]uint16, (lpDataLength-2)/2) 321 | pname = convertByteArrayToUint16Array(lpData[:lpDataLength-2], lpDataLength-2) 322 | list[i].Name = syscall.UTF16ToString(pname) 323 | log.Println("The length of the name is:", len(list[i].Name)) 324 | log.Println("list[i].Name=" + list[i].Name + "---") 325 | //list[i].FriendlyName = getFriendlyName(list[i].Name) 326 | list[i].FriendlyName = getFriendlyName("COM34") 327 | } 328 | win.RegCloseKey(root) 329 | return list, err 330 | } 331 | */ 332 | 333 | func convertByteArrayToUint16Array(b []byte, mylen uint32) []uint16 { 334 | 335 | log.Println("converting. len:", mylen) 336 | var i uint32 337 | ret := make([]uint16, mylen/2) 338 | for i = 0; i < mylen; i += 2 { 339 | //ret[i/2] = binary.LittleEndian.Uint16(b[i : i+1]) 340 | ret[i/2] = uint16(b[i]) | uint16(b[i+1])<<8 341 | } 342 | return ret 343 | } 344 | 345 | func getFriendlyName(portname string) string { 346 | 347 | // this method panics a lot and i'm not sure why, just catch 348 | // the panic and return empty list 349 | defer func() { 350 | if e := recover(); e != nil { 351 | // e is the interface{} typed-value we passed to panic() 352 | log.Println("Got panic: ", e) // Prints "Whoops: boom!" 353 | } 354 | }() 355 | 356 | var friendlyName string 357 | 358 | // init COM, oh yeah 359 | ole.CoInitialize(0) 360 | defer ole.CoUninitialize() 361 | 362 | unknown, _ := oleutil.CreateObject("WbemScripting.SWbemLocator") 363 | defer unknown.Release() 364 | 365 | wmi, _ := unknown.QueryInterface(ole.IID_IDispatch) 366 | defer wmi.Release() 367 | 368 | // service is a SWbemServices 369 | serviceRaw, _ := oleutil.CallMethod(wmi, "ConnectServer") 370 | service := serviceRaw.ToIDispatch() 371 | defer service.Release() 372 | 373 | // result is a SWBemObjectSet 374 | //pname := syscall.StringToUTF16("SELECT * FROM Win32_PnPEntity where Name like '%" + "COM35" + "%'") 375 | pname := "SELECT * FROM Win32_PnPEntity where Name like '%" + portname + "%'" 376 | resultRaw, _ := oleutil.CallMethod(service, "ExecQuery", pname) 377 | result := resultRaw.ToIDispatch() 378 | defer result.Release() 379 | 380 | countVar, _ := oleutil.GetProperty(result, "Count") 381 | count := int(countVar.Val) 382 | 383 | for i := 0; i < count; i++ { 384 | // item is a SWbemObject, but really a Win32_Process 385 | itemRaw, _ := oleutil.CallMethod(result, "ItemIndex", i) 386 | item := itemRaw.ToIDispatch() 387 | defer item.Release() 388 | 389 | asString, _ := oleutil.GetProperty(item, "Name") 390 | 391 | println(asString.ToString()) 392 | friendlyName = asString.ToString() 393 | } 394 | 395 | return friendlyName 396 | } 397 | -------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------