diff --git a/README.md b/README.md index 9c41f8a30..5e4004ded 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -serial-port-json-server +arduino-create-agent ======================= -Get the latest version of the Bridge for all supported platforms: +Version X.XX + +Get the latest version of the Agent for all supported platforms: [Linux x64](http://downloads.arduino.cc/CreateBridge/ArduinoCreateAgent-1.0-linux-x64-installer.run) @@ -9,349 +11,7 @@ Get the latest version of the Bridge for all supported platforms: [Windows](http://downloads.arduino.cc/CreateBridge/ArduinoCreateAgent-1.0-windows-installer.exe) -Version 1.82 - -A serial port JSON websocket & web server that runs from the command line on Windows, Mac, Linux, Raspberry Pi, or Beagle Bone that lets you communicate with your serial port from a web application. This enables web apps to be written that can communicate with your local serial device such as an Arduino, CNC controller, or any device that communicates over the serial port. Since version 1.82 you can now also program your Arduino by uploading a hex file. - -The app is written in Go. It has an embedded web server and websocket server. The server runs on the standard port of localhost:8989. You can connect to it locally with your browser to interact by visiting http://localhost:8989. The websocket is technically running at ws://localhost/ws. You can of course connect to your websocket from any other computer to bind in remotely. For example, just connect to ws://192.168.1.10/ws if you are on a remote host where 192.168.1.10 is your devices actual IP address. - -The app is one executable with everything you need and is available ready-to-go for every major platform. It is a multi-threaded app that uses all of the cool techniques available in Go including extensive use of channels (threads) to create a super-responsive app. - -If you are a web developer and want to write a web application that connects to somebody's local or remote serial port server, then you simply need to create a websocket connection to the localhost or remote host and you will be directly interacting with that user's serial port. - -For example, if you wanted to create a Gcode Sender web app to enable people to send 3D print or milling commands from your site, this would be a perfect use case. Or if you've created an oscilloscope web app that connects to an Arduino, it would be another great use case. Finally you can write web apps that interact with a user's local hardware. - -Thanks go to gary.burd.info for the websocket example in Go. Thanks also go to tarm/goserial for the serial port base implementation. Thanks go to Jarret Luft at well for building the Grbl buffer and helping on global code changes to make everything better. - -Example Use Case ---------- -Here is a screenshot of the Serial Port JSON Server being used inside the ChiliPeppr Serial Port web console app. -http://chilipeppr.com/serialport -<img src="http://chilipeppr.com/img/screenshots/serialportjsonserver2.png"> - -This is the Serial Port JSON Server being used inside the TinyG workspace in ChiliPeppr. -http://chilipeppr.com/tinyg -<img src="http://chilipeppr.com/img/screenshots/serialportjsonserver3.png"> - -There is also a JSFiddle you can fork to create your own interface to the Serial Port JSON Server for your own project. -http://jsfiddle.net/chilipeppr/vetj5fvx/ -<img src="http://chilipeppr.com/img/screenshots/serialportjsonserver_jsfiddle.png"> - - -Running ---------- -From the command line issue the following command: -- Mac/Linux -`./serial-port-json-server` -- Windows -`serial-port-json-server.exe` - -Verbose logging mode: -- Mac/Linux -`./serial-port-json-server -v` -- Windows -`serial-port-json-server.exe -v` - -Running on alternate port: -- Mac/Linux -`./serial-port-json-server -addr :8000` -- Windows -`serial-port-json-server.exe -addr :8000` - -Filter the serial port list so it has relevant ports in the list: -- Mac/Linux -`./serial-port-json-server -regex usb|acm` -- Windows -`serial-port-json-server.exe -regex com8|com9|com2[0-5]|tinyg` - -Garbage collect mode (deprecated): -- Mac/Linux -`./serial-port-json-server -gc std` -- Windows -`serial-port-json-server.exe -gc max` - -Override the default hostname: -- Mac/Linux -`./serial-port-json-server -hostname myMacSpjs` -- Windows -`serial-port-json-server.exe -hostname meWindowsBox` - - -Here's a screenshot of a successful run on Windows x64. Make sure you allow the firewall to give access to Serial Port JSON Server or you'll wonder why it's not working. -<img src="http://chilipeppr.com/img/screenshots/serialportjsonserver_running.png"> - -Binaries for Download ---------- -Version 1.80 -Build date: Mar 8, 2015 -Build has new garbage collection, "broadcast" tag, and "hostname" tag support. - -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_windows_386.zip">Windows x32</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_windows_amd64.zip">Windows x64</a> -- <a class="list-group-item" target="_blank" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_macosx_v1.80.zip">Mac OS X x64 (Thanks to Riley Porter)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_linux_386.tar.gz">Linux x32</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_linux_amd64.tar.gz">Linux x64</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_1.80_linux_armv6.tar.gz">Raspberry Pi 1 (Linux ARMv6)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_1.80_linux_armv7.tar.gz">Raspberry Pi 2 (Linux ARMv7)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_1.80_linux_armv7.tar.gz">Beagle Bone Black (Linux ARMv7)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_1.80_linux_armv8.tar.gz">Linux ARMv8 (AppliedMicro X-Gene)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.80/serial-port-json-server_linux_amd64.tar.gz">Intel Edison (Linux x64)</a> - -Version 1.77 -Build date: Feb 1, 2015 -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_windows_386.zip">Windows x32</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_windows_amd64.zip">Windows x64</a> -- <a class="list-group-item" target="_blank" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server-v1.77-osx.zip">Mac OS X x64 (Thanks to Jarret Luft for build)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_linux_386.tar.gz">Linux x32</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_linux_amd64.tar.gz">Linux x64</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_linux_arm.tar.gz">Raspberry Pi (Linux ARM)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_linux_arm.tar.gz">Beagle Bone Black (Linux ARM)</a> -- <a class="list-group-item" href="http://chilipeppr.com/downloads/v1.77/serial-port-json-server_linux_amd64.tar.gz">Intel Edison (Linux x64)</a> - -How to Build ---------- -You do not need to build this. Binaries are available above. However, if you still want to build... - -Video tutorial of building SPJS on a Mac: https://www.youtube.com/watch?v=4Hou06bOuHc - -1. Install Go (http://golang.org/doc/install) -2. If you're on a Mac, install Xcode from the Apple Store because you'll need gcc to compile the native code for a Mac. If you're on Windows, Linux, Raspberry Pi, or Beagle Bone you are all set. -3. Get go into your path so you can run "go" from any directory: - On Linux, Mac, Raspberry Pi, Beagle Bone Black - export PATH=$PATH:/usr/local/go/bin - On Windows, use the Environment Variables dialog by right-click My Computer -4. Define your GOPATH variable and create the folder to match. This is your personal working folder for all yourGo code. This is important because you will be retrieving several projects from Github and Go needs to know where to download all the files and where to build the directory structure. On my Windows computer I created a folder called C:\Users\John\go and set GOPATH=C:\Users\John\go - On Mac - export GOPATH=/Users/john/go - On Linux, Raspberry Pi, Beagle Bone Black, Intel Edison - export GOPATH=/home/john/go - On Windows, use the Environment Variables dialog by right-click My Computer to create GOPATH -5. Change directory into your GOPATH -6. Type "go get github.com/johnlauer/serial-port-json-server". This will retrieve this Github project and all dependent projects. It takes some time to run this. -7. Then change direcory into src\github.com\johnlauer\serial-port-json-server. -8. Type "go build" when you're inside that directory and it will create a binary called serial-port-json-server -9. Run it by typing ./serial-port-json-server or on Windows run serial-port-json-server.exe -10. If you have a firewall on the computer running the serial-port-json-server you must allow port 8989 in the firewall. - -Supported Commands -------- - -Command | Example | Description -------- | ------- | ------- -list | | Lists all available serial ports on your device -open portName baudRate [bufferAlgorithm] | open /dev/ttyACM0 115200 tinyg | Opens a serial port. The comPort should be the Name of the port inside the list response such as COM2 or /dev/ttyACM0. The baudrate should be a rate from the baudrates command or a typical baudrate such as 9600 or 115200. A bufferAlgorithm can be optionally specified such as "tinyg" (or in the future "grbl" if somebody writes it) or write your own. -sendjson {} | {"P":"COM22","Data":[{"D":"!~\n","Id":"234"},{"D":"{\"sr\":\"\"}\n","Id":"235"}]} | See Wiki page at https://github.com/johnlauer/serial-port-json-server/wiki -send portName data | send /dev/ttyACM0 G1 X10.5 Y2 F100\n | Send your data to the serial port. Remember to send a newline in your data if your serial port expects it. -sendnobuf portName data | send COM22 {"qv":0}\n | Send your data and bypass the bufferFlowAlgorithm if you specified one. -close portName | close COM1 | Close out your serial port -bufferalgorithms | | List the available bufferAlgorithms on the server. You will get a list such as "default, tinyg" -baudrates | | List common baudrates such as 2400, 9600, 115200 -restart | | Restart the serial port JSON server -exit | | Exit the serial port JSON server -memstats | | Send back data on the memory usage and garbage collection performance -broadcast string | broadcast my data | Send in this command and you will get a message reflected back to all connected endpoints. This is useful for communicating with all connected clients, i.e. in a CNC scenario is a pendant wants to ask the main workspace if there are any settings it should know about. For example send in "broadcast this is my custom cmd" and get this reflected back to all connected sockets {"Cmd":"Broadcast","Msg":"this is my custom cmd\n"} -version | | Get the software version of SPJS that is running -hostname | | Get the hostname of the current SPJS instance -program port core:architecture:name $path/to/filename/without/extension | programfromurl com3 arduino:avr:uno c:\myfiles\grbl_v0_9i_atmega328p_16mhz_115200.hex | Send a hex file to your Arduino board to program it. -programfromurl port core:architecture:name url | programfromurl com3 arduino:avr:uno https://raw.githubusercontent.com/grbl/grbl-builds/master/builds/grbl_v0_9i_atmega328p_16mhz_115200.hex | Download a hex file from a URL and then send it to your Arduino board to program it. - -Garbage collection -------- -On slower devices like Raspberry Pis (not the new Raspberry Pi 2) it is evident that the slowness of the CPU can cause some issues. In particular, on a Tinyg so much data can flow back from the serial device that it can overwhelm the Raspberry Pi such that serial data is lost if the Pi can't process it quick enough. This usually isn't a problem until a garbage collection process is triggered by golang for SPJS. - -Garbage collection does a "stop the world" technique which on the Raspi is so slow that SPJS may be unresponsive for 5 or even 10 seconds. This is long enough that data starts spilling off the serial port buffer inside the TinyG. On faster hosts like Windows or Mac this doesn't happen. Therefore some additional tricks have been added to SPJS to try to alleviate this problem from rearing it's ugly head. - -SPJS by default will start in gc=std mode. This means SPJS will simply use the default garbage collection from Golang. You could instead try gc=max. This means SPJS will forcibly garbage collect non-stop on each receive and send on the serial port. This essentially doubles or triples SPJS's CPU usage, but it reduces the chance for the stopping of the world. It is recommended to keep gc=std as the default, but you could try your own settings including trying gc=off which means all garbage collection is turned off and thus you'll eventually run out of memory. You can send in a "gc" into SPJS via the websocket to force manual garbage collection in this instance. - -Broadcast Command -------- -There is a growing need for end-clients of SPJS to be able to chat with eachother. Therefore a new command has been added called "broadcast". It's not a very sophisticated feature because it simply regurgitates out whatever is after the broadcast command back to all connected clients. This simplistic approach means any user can implement any command they would like via the broadcast command and create unique solutions via SPJS. - -For example, if a pendant controller for your CNC is connected to SPJS and trying to figure out if the ChiliPeppr main workspace has some stored settings for your pendant, it could send out a command like: -`broadcast get-settings` - -And SPJS would regurgitate the command to all connected sockets like: -`{"Cmd":"Broadcast","Msg":"get-settings\n"}` - -And if the ChiliPeppr workspace were listening for all incoming {"Cmd":"Broadcast","Msg":...} signals and specifically the "get-settings" command then it could respond with something like: -`broadcast settings x:1, y:10, z:4` - -Interesting Branches of SPJS ------------ -https://github.com/benjamind/gpio-json-server/ - -This is a very interesting branch on this project where Ben took the basic code layout, websocket, and command structure and created a GPIO server version of this app. It's such an interesting and awesome project, it makes me want to combine his code into SPJS to make a full-blown version of serving up hardware ports via JSON and websockets--whether they're serial ports or GPIO ports. Something about that just feels right. The only downside is that no Windows or Mac machines have GPIO, so it would be a very Raspberry Pi specific feature. - -FAQ -------- -- Q: There are several Node.js serial port servers. Why not write this in Node.js instead of Go? - -- A: Because Go is a better solution for several reasons. - - Easier to install on your computer. Just download and run binary. (Node requires big install) - - It is multi-threaded which is key for a serial port websocket server (Node is single-threaded) - - It has a tiny memory footprint using about 3MB of RAM - - It is one clean compiled executable with no dependencies - - It makes very efficient use of RAM with amazing garbage collection - - It is super fast when running - - It launches super quick - - It is essentially C code without the pain of C code. Go has insanely amazing threading support called Channels. Node.js is single-threaded, so you can't take full advantage of the CPU's threading capabilities. Go lets you do this easily. A serial port server needs several threads. 1) Websocket thread for each connection. 2) Serial port thread for each serial device. Serial Port JSON Server allows you to bind as many serial port devices in parallel as you want. 3) A writer and reader thread for each serial port. 4) A buffering thread for each incoming message from the browser into the websocket 5) A buffering thread for messages back out from the server to the websocket to the browser. To achieve this in Node requires lots of callbacks. You also end up talking natively anyway to the serial port on each specific platform you're on, so you have to deal with the native code glued to Node. - -Startup Script for Linux -------- - -Here's a really lightweight /etc/init.d startup script for use on Linux like with a Raspberry Pi, Beable Bone Black, Odroid, Intel Edison, etc. - -Create a text file inside /etc/init.d called serial-port-json-server, for example: - -`sudo nano /etc/init.d/serial-port-json-server` - -Then make sure the file contents contain the following script, but make sure to update the path to your serial-port-json-server binary. This example has the binary in /home/pi but yours may differ. -<pre> -#! /bin/sh -### BEGIN INIT INFO -# Provides: serial-port-json-server -# Required-Start: $all -# Required-Stop: -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Manage my cool stuff -### END INIT INFO - -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin - -. /lib/init/vars.sh -. /lib/lsb/init-functions -# If you need to source some other scripts, do it here - -case "$1" in - start) - log_begin_msg "Starting Serial Port JSON Server service" -# do something - /home/pi/serial-port-json-server_linux_arm/serial-port-json-server -regex usb & - log_end_msg $? - exit 0 - ;; - stop) - log_begin_msg "Stopping the Serial Port JSON Server" - - # do something to kill the service or cleanup or nothing - killall serial-port-json-server - log_end_msg $? - exit 0 - ;; - *) - echo "Usage: /etc/init.d/serial-port-json-server {start|stop}" - exit 1 - ;; -esac -</pre> - -Then you need to run the following command to setup your /etc/init.d script so it starts on boot up of your computer... - -`sudo update-rc.d serial-port-json-server defaults` - -And of course to manually start/stop the service: - -<pre> -sudo service serial-port-json-server stop -sudo service serial-port-json-server start -</pre> - -Revisions -------- -Changes in 1.82 -- Thanks go to https://github.com/facchinm from Arduino.cc for the changes in 1.82. -- You can now program your Arduino by using the program command in SPJS. -- Avrdude and Bossac are now included in the binary distributions for each platform. -- The serial library has been replaced with one from https://github.com/cmaglie to solve some long-standing bugs including a connection handshake and ports not closing correctly on all platforms. - -Changes in 1.81 -- On Linux, SPJS now tries to grab the Manufacturer and Name of the serial port to give you pretty names for your connected devices. Arduinos and TinyGs show up with nicely descriptive names now instead of just ttyUSB0 or ttyACM0. - -Changes in 1.80 -- "Broadcast" command added which simply regurgitates out to all clients whatever is sent in. Allows for end-client to end-client communication via SPJS. -- "Hostname" was added whereby SPJS now tries to figure out the hostname of the machine running SPJS and pass it back to the end-clients. This helps to differentiate multiple SPJS's on your network. You can set this from the command line as well on launch. -- Garbage collection improvement. Golang 1.4 got some big garbage collection improvements. This is the first time SPJS was built with this new version of golang for the binaries made publicly available. - -Changes in 1.77 -- Completely fixed stalled jobs. This was due to garbage collection doing a "stop the world" so the fix was to force garbage collection on key events. - -Changes in 1.76 -- Somewhat fixed stalled jobs (they're not perfect yet, but you can simply hit the ~ in ChiliPeppr to resume the job if it stalls) whereby the serial buffer from the serial device to Serial Port JSON Server could overflow because SPJS was handling blocking websocket send operations. The sending back of data to the client is now de-coupled from the incoming serial stream via a buffered golang channel. Prior to this change it was an unbuffered channel, so it was a different thread, but it could block on write across the boundary. -- Added restart and exit commands -- Added serial port list readout on startup -- Added ability to filter list based on regular expression by adding -regexp myfilter to the command line - -Changes in 1.75 -- Tweaked the order of operations for pausing/unpausing the buffer in Grbl and TinyG to account for rare cases where a deadlock could occur. This should guarantee no dead-locking. -- Jarret Luft added an artificial % buffer wipe to Grbl buffer to mimic to some degree the buffer wiping available on TinyG. - -Changes in 1.7 -- sendjson now supported. Will give back onQueue, onWrite, onComplete -- Moved TinyG buffer to serial byte counting. - -Changes in 1.6 -- Logging is now off by default so Raspberry Pi runs cleaner. The immense amount of logging was dragging the Raspi down. Should help on BeagleBone Black as well. Makes SPJS run more efficient on powerful systems too like Windows, Mac, and Linux. You can turn on logging by issuing a -v on the command line. This fix by Jarret Luft. -- Added EOF extra checking for Linux serial ports that seem to return an EOF on a new connect and thus the port was prematurely closing. Thanks to Yiannis Mandravellos for finding the bug and fixing it. -- Added a really nice Grbl bufferAlgorithm which was written by Jarret Luft who is the creator of the Grbl workspace in ChiliPeppr. - - The buffer counts each line of gcode being sent to Grbl up to 127 bytes and then doesn't send anymore data to Grbl until it sees an OK or ERROR response from Grbl indicating the command was processed. For each OK|ERROR the buffer decrements the counter to see how much more room is avaialble. If the next Gcode command can fit it is sent immediately in. - - This new Grbl buffer should mirror the stream.py example code from Sonny Jeon who maintains Grbl. This Serial Port JSON Server should now be able to execute the commands faster than anything out there since it's written in Go (which is C) and is compiled and super-fast. - - Position requests occur inside this buffer where a ? is sent every 250ms to Grbl such that you should see a position just come back on demand non-stop from Grbl. It could be possible in a future version to only queue these position reports up during actual Gcode commands being sent so that when idle there are not a ton of position updates being sent back that aren't necessary. - - Soft resets (Ctrl-x) now wipe the buffer. - - !~? will skip ahead of all other commands now. This is important for jogging or using ! as a quick stop of your controller since you can have 25,000 lines of gcode queued to SPJS now and of course you would want these commands to skip in front of that queue. - - Feedhold pauses the buffer inside SPJS now. - - Cycle resume ~ unpauses the buffer inside SPJS now. - - When using this buffer data is sent back in a per line mode rather than as characters are received so there is more efficiency on the websocket. - - Checks for the grbl init line indicating the arduino is ready to accept commands - -Changes in 1.5 -- For TinyG buffer, moved to slot counter approach. The buffer planner approach was causing G2/G3 commands to overflow the buffer because the round-trip time was too off with reading QR responses. So, moved to a 4 slot buffer approach. Jogging is still a bit rough in this approach, but that can get tweaked. The new slot approach is more like counting serial buffer queue items. SPJS sends up to 4 commands and then waits for a r:{} json response. It has intelligence to know if certain commands won't get a response like !~% or newlines, so it doesn't look for slot responses and just blindly sends. The only danger is if there are 4 really long lines of Gcode that surpass the 254 bytes in the serial buffer then we could overflow. Could add trapping for that. - -Changes in 1.4 -- Added reporting on Queuing so you know what the state of the Serial Port JSON Server Queue is doing. The reason for this is to ensure your serial port commands don't get out of order you will want to make sure you write to the websocket and then wait for the {"Cmd":"Queued"} response. Then write your next command. This is necessary because when sending different frames across a websocket over the Internet, you can get packet retransmissions, and although you'll never lose your data, your serial commands could arrive at the server out of order. By watching that your command is queued, you are safe to send the next command. However, this can also slow things down, so now you can simply gang up multiple commands into one send and the Serial Port JSON Server will split them into separate sub-commands and tell you that it did in the queue and write reports. - - For example, a typical queue report looks like {"Cmd":"Queued","QCnt":61,"Type":["Buf"],"D":["{\"sr\":\"\"}\n"],"Port":"COM22"}. - - If you send something like: send COM22 {"sr":""}\n{"qr":""}\n{"sr":""}\n{"qr":""}\n. You will get back a queue report like {"Cmd":"Queued","QCnt":4,"Type":["Buf","Buf","Buf","Buf"],"D":["{\"sr\":\"\"}\n","{\"qr\":\"\"}\n","{\"sr\":\"\"}\n","{\"qr\":\"\"}\n"],"Port":"COM22"} - - When two queue items are written to the serial port you will get back something like {"Cmd":"Write","QCnt":1,"D":"{\"qr\":\"\"}\n","Port":"COM22"}{"Cmd":"Write","QCnt":0,"D":"{\"sr\":\"\"}\n","Port":"COM22"} -- Fixed analysis of incoming serial data due to some serial ports sending fragmented data. -- Added bufferalgorithms and baudrates commands -- A new command called sendnobuf was added so you can bypass the bufferflow algorithm. This command only is worth using if you specified a bufflerFlowAlgorithm when you opened the serial port. You use it by sending "sendnobuf com4 G0 X0 Y0" and it will jump ahead of the queue and go diretly to the serial port without hesitation. -- TinyG Bufferflow algorithm. - - Looks for qr responses and if they are too low on the planner buffer will trigger a pause on send. - - Looks for qr responses and if they are high enough to send again the bufferflow is unblocked. - - If you pause with ! then the bufferflow also pauses. - - If you resume with ~ then the bufferflow also resumes. - - If you wipe the buffer with % then the bufferflow also wipes. - - When you send !~% it automatically is sent to TinyG without buffering so it essentially skips ahead of all other buffered commands. This mimics what TinyG does internally. - - If you ask qr reports to be turned off with a $qv=0 or {"qv":0} then bypassmode is entered whereby no blocking occurs on sending serial port commands. - - If you ask qr reports to be turned back on with $qv=1 (or 2 or 3) or {"qv":1} (or 2 or 3) then bypassmode is turned off. - - If a qr reponse is seen from TinyG then BypassMode is turned off automatically. -Changes in 1.3 -- Added ability for buffer flow plugins. There is a new buffer flow plugin - for TinyG that watches the {"qr":NN} response. When it sees the qr value - go below 12 it pauses its own sending and queues up whatever is still coming - in on the Websocket. This is fine because we've got plenty of RAM on the - websocket server. The {"qr":NN} value is still sent back on the websocket as - soon as it was before, so the host application should see no real difference - as to how it worked before. The difference now though is that the serial sending - knows to check if sending is paused to the serial port and queue. This makes - sure no buffer overflows ever occur. The reason this was becoming important is - that the lag time between the qr response and the sending of Gcode was too distant - and this buffer flow needs resolution around 5ms. Normal latency on the Internet - is like 20ms to 200ms, so it just wasn't fast enough. If the Javascript hosting - the websocket was busy processing other events, then this lag time became even - worse. So, now the Serial Port JSON Server simply helps out by lots of extra - buffering. Go ahead and pound it even harder with more serial commands and see - it fly. +arduino-create-agent is a fork of @johnlauer's serial-port-json-server (which we really want to thank for his kindness and great work) -Changes in 1.2 -- Added better error handling -- Removed forcibly adding a newline to the serial data being sent to the port. This - means apps must send in a newline if the serial port expects it. -- Embedded the home.html file inside the binary so there is no longer a dependency - on an external file. -- TODO: Closing a port on Beagle Bone seems to hang. Only solution now is to kill - the process and restart. -- TODO: Mac implementation seems to have trouble on writing data after a while. Mac - gray screen of death can appear. Mac version uses CGO, so it is in unsafe mode. - May have to rework Mac serial port to use pure golang code. +The history has been rewritten to keep the repo small (thus removing all binaries committed in the past) \ No newline at end of file diff --git a/altbuild.sh b/altbuild.sh deleted file mode 100644 index 5234953d4..000000000 --- a/altbuild.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -echo "About to cross-compile Serial Port JSON Server" -#echo '$0 = ' $0 -#echo '$1 = ' $1 -#echo '$2 = ' $2 - -if [ "$1" = "" ]; then - echo "You need to pass in the version number as the first parameter." - exit -fi - -# turn on echo -set -x -#set -v - -# Windows x32 and x64, Linux -goxc -bc=windows,linux -d="." -pv=$1 -tasks-=pkg-build default -GOARM=6 - -# Rename arm to arm6 -#set +x -FILE=$1'/serial-port-json-server_'$1'_linux_arm.tar.gz' -FILE2=$1'/serial-port-json-server_'$1'_linux_armv6.tar.gz' -#set -x -mv $FILE $FILE2 - -# Special build for armv7 for BBB and Raspi2 -goxc -bc=linux,arm -d="." -pv=$1 -tasks-=pkg-build default -GOARM=7 -FILE3=$1'/serial-port-json-server_'$1'_linux_armv7.tar.gz' -mv $FILE $FILE3 - -# Special build for armv8 -goxc -bc=linux,arm -d="." -pv=$1 -tasks-=pkg-build default -GOARM=8 -FILE4=$1'/serial-port-json-server_'$1'_linux_armv8.tar.gz' -mv $FILE $FILE4 - diff --git a/bufferflow.go b/bufferflow.go index 7812226e1..d5059989b 100644 --- a/bufferflow.go +++ b/bufferflow.go @@ -5,7 +5,7 @@ import ( //"time" ) -var availableBufferAlgorithms = []string{"default", "tinyg", "dummypause", "grbl"} +var availableBufferAlgorithms = []string{"default"} type BufferMsg struct { Cmd string diff --git a/bufferflow_default.go b/bufferflow_default.go index 7cf9e2d48..4294fb824 100644 --- a/bufferflow_default.go +++ b/bufferflow_default.go @@ -1,10 +1,7 @@ package main import ( - "log" - //"regexp" - //"strconv" - //"time" + log "github.com/Sirupsen/logrus" ) type BufferflowDefault struct { diff --git a/bufferflow_dummypause.go b/bufferflow_dummypause.go deleted file mode 100644 index 92f103a49..000000000 --- a/bufferflow_dummypause.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "log" - "time" -) - -type BufferflowDummypause struct { - Name string - Port string - NumLines int - Paused bool -} - -func (b *BufferflowDummypause) Init() { -} - -func (b *BufferflowDummypause) BlockUntilReady(cmd string, id string) (bool, bool) { - log.Printf("BlockUntilReady() start. numLines:%v\n", b.NumLines) - log.Printf("buffer:%v\n", b) - //for b.Paused { - log.Println("We are paused for 3 seconds. Yeilding send.") - time.Sleep(3000 * time.Millisecond) - //} - log.Printf("BlockUntilReady() end\n") - return true, false -} - -func (b *BufferflowDummypause) OnIncomingData(data string) { - log.Printf("OnIncomingData() start. data:%v\n", data) - b.NumLines++ - //time.Sleep(3000 * time.Millisecond) - log.Printf("OnIncomingData() end. numLines:%v\n", b.NumLines) -} - -// Clean out b.sem so it can truly block -func (b *BufferflowDummypause) ClearOutSemaphore() { -} - -func (b *BufferflowDummypause) BreakApartCommands(cmd string) []string { - return []string{cmd} -} - -func (b *BufferflowDummypause) Pause() { - return -} - -func (b *BufferflowDummypause) Unpause() { - return -} - -func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDummypause) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { - return false -} - -func (b *BufferflowDummypause) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { - /* - // remove comments - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - if match := b.reNoResponse.MatchString(cmd); match { - log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) - return true - } - */ - return false -} - -func (b *BufferflowDummypause) ReleaseLock() { -} - -func (b *BufferflowDummypause) IsBufferGloballySendingBackIncomingData() bool { - return false -} - -func (b *BufferflowDummypause) Close() { -} diff --git a/bufferflow_grbl.go b/bufferflow_grbl.go deleted file mode 100644 index b20d205a2..000000000 --- a/bufferflow_grbl.go +++ /dev/null @@ -1,457 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "regexp" - "strconv" - "strings" - "time" - "sync" -) - -type BufferflowGrbl struct { - Name string - Port string - Paused bool - BufferMax int - q *Queue - - // use thread locking for b.Paused - lock *sync.Mutex - - sem chan int - LatestData string - LastStatus string - version string - quit chan int - parent_serport *serport - - reNewLine *regexp.Regexp - ok *regexp.Regexp - err *regexp.Regexp - initline *regexp.Regexp - qry *regexp.Regexp - rpt *regexp.Regexp -} - -func (b *BufferflowGrbl) Init() { - b.lock = &sync.Mutex{} - b.SetPaused(false, 1) - - log.Println("Initting GRBL buffer flow") - b.BufferMax = 127 //max buffer size 127 bytes available - - b.q = NewQueue() - - //create channels - b.sem = make(chan int) - - //define regex - b.reNewLine, _ = regexp.Compile("\\r{0,1}\\n{1,2}") //\\r{0,1} - b.ok, _ = regexp.Compile("^ok") - b.err, _ = regexp.Compile("^error") - b.initline, _ = regexp.Compile("^Grbl") - b.qry, _ = regexp.Compile("\\?") - b.rpt, _ = regexp.Compile("^<") - - //initialize query loop - b.rptQueryLoop(b.parent_serport) -} - -func (b *BufferflowGrbl) BlockUntilReady(cmd string, id string) (bool, bool) { - log.Printf("BlockUntilReady() start\n") - - b.q.Push(cmd, id) - - log.Printf("New line length: %v, buffer size increased to:%v\n", len(cmd), b.q.LenOfCmds()) - log.Println(b.q) - - if b.q.LenOfCmds() >= b.BufferMax { - b.SetPaused(true, 0) - log.Printf("Buffer Full - Will send this command when space is available") - } - - if b.GetPaused() { - log.Println("It appears we are being asked to pause, so we will wait on b.sem") - // We are being asked to pause our sending of commands - - // clear all b.sem signals so when we block below, we truly block - b.ClearOutSemaphore() - - log.Println("Blocking on b.sem until told from OnIncomingData to go") - unblockType, ok := <-b.sem // will block until told from OnIncomingData to go - - log.Printf("Done blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) - - // we get an unblockType of 1 for normal unblocks - // we get an unblockType of 2 when we're being asked to wipe the buffer, i.e. from a % cmd - if unblockType == 2 { - log.Println("This was an unblock of type 2, which means we're being asked to wipe internal buffer. so return false.") - // returning false asks the calling method to wipe the serial send once - // this function returns - return false, false - } - - log.Printf("BlockUntilReady(cmd:%v, id:%v) end\n", cmd, id) - } - return true, true -} - -func (b *BufferflowGrbl) OnIncomingData(data string) { - log.Printf("OnIncomingData() start. data:%q\n", data) - - b.LatestData += data - - //it was found ok was only received with status responses until the grbl buffer is full. - //b.LatestData = regexp.MustCompile(">\\r\\nok").ReplaceAllString(b.LatestData, ">") //remove oks from status responses - - arrLines := b.reNewLine.Split(b.LatestData, -1) - log.Printf("arrLines:%v\n", arrLines) - - if len(arrLines) > 1 { - // that means we found a newline and have 2 or greater array values - // so we need to analyze our arrLines[] lines but keep last line - // for next trip into OnIncomingData - log.Printf("We have data lines to analyze. numLines:%v\n", len(arrLines)) - - } else { - // we don't have a newline yet, so just exit and move on - // we don't have to reset b.LatestData because we ended up - // without any newlines so maybe we will next time into this method - log.Printf("Did not find newline yet, so nothing to analyze\n") - return - } - - // if we made it here we have lines to analyze - // so analyze all of them except the last line - for index, element := range arrLines[:len(arrLines)-1] { - log.Printf("Working on element:%v, index:%v", element, index) - - //check for 'ok' or 'error' response indicating a gcode line has been processed - if b.ok.MatchString(element) || b.err.MatchString(element){ - if b.q.Len() > 0 { - doneCmd, id := b.q.Poll() - - if b.ok.MatchString(element){ - // Send cmd:"Complete" back - m := DataCmdComplete{"Complete", id, b.Port, b.q.LenOfCmds(), doneCmd} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - } else if b.err.MatchString(element){ - // Send cmd:"Error" back - log.Printf("Error Response Received:%v, id:%v", doneCmd, id) - m := DataCmdComplete{"Error", id, b.Port, b.q.LenOfCmds(), doneCmd} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - } - - log.Printf("Buffer decreased to itemCnt:%v, lenOfBuf:%v\n", b.q.Len(), b.q.LenOfCmds()) - } else { - 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!!!!") - } - - if b.q.LenOfCmds() < b.BufferMax { - - log.Printf("Grbl just completed a line of gcode\n") - - // if we are paused, tell us to unpause cuz we have clean buffer room now - if b.GetPaused() { b.SetPaused(false, 1) } - } - - //check for the grbl init line indicating the arduino is ready to accept commands - //could also pull version from this string, if we find a need for that later - } else if b.initline.MatchString(element) { - //grbl init line received, clear anything from current buffer and unpause - b.LocalBufferWipe(b.parent_serport) - - //unpause buffer but wipe the command in the queue as grbl has restarted. - if b.GetPaused() { b.SetPaused(false, 2) } - - b.version = element //save element in version - - //Check for report output, compare to last report output, if different return to client to update status; otherwise ignore status. - } else if b.rpt.MatchString(element){ - if(element == b.LastStatus){ - log.Println("Grbl status has not changed, not reporting to client") - continue //skip this element as the cnc position has not changed, and move on to the next element. - } - - b.LastStatus = element //if we make it here something has changed with the status string and laststatus needs updating - } - - // handle communication back to client - m := DataPerLine{b.Port, element + "\n"} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - - } // for loop - - // now wipe the LatestData to only have the last line that we did not analyze - // because we didn't know/think that was a full command yet - b.LatestData = arrLines[len(arrLines)-1] - - //time.Sleep(3000 * time.Millisecond) - log.Printf("OnIncomingData() end.\n") -} - -// Clean out b.sem so it can truly block -func (b *BufferflowGrbl) ClearOutSemaphore() { - ctr := 0 - - keepLooping := true - for keepLooping { - select { - case d, ok := <-b.sem: - log.Printf("Consuming b.sem queue to clear it before we block. ok:%v, d:%v\n", ok, string(d)) - ctr++ - if ok == false { - keepLooping = false - } - default: - keepLooping = false - log.Println("Hit default in select clause") - } - } - log.Printf("Done consuming b.sem queue so we're good to block on it now. ctr:%v\n", ctr) - // ok, all b.sem signals are now consumed into la-la land -} - -func (b *BufferflowGrbl) BreakApartCommands(cmd string) []string { - - // add newline after !~% - log.Printf("Command Before Break-Apart: %q\n", cmd) - - cmds := strings.Split(cmd, "\n") - finalCmds := []string{} - for _, item := range cmds { - //remove comments and whitespace from item - item = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(item, "") - item = regexp.MustCompile(";.*").ReplaceAllString(item, "") - item = strings.Replace(item," ","",-1) - - if item == "*init*"{ //return init string to update grbl widget when already connected to grbl - m := DataPerLine{b.Port, b.version + "\n"} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - } else if item == "*status*"{ //return status when client first connects to existing open port - m := DataPerLine{b.Port, b.LastStatus + "\n"} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - } else if item == "?"{ - log.Printf("Query added without newline: %q\n", item) - finalCmds = append(finalCmds, item) //append query request without newline character - } else if item == "%" { - log.Printf("Wiping Grbl BufferFlow") - b.LocalBufferWipe(b.parent_serport) - //dont add this command to the list of finalCmds - } else if item != "" { - log.Printf("Re-adding newline to item:%v\n", item) - s := item + "\n" - finalCmds = append(finalCmds, s) - log.Printf("New cmd item:%v\n", s) - } - - } - log.Printf("Final array of cmds after BreakApartCommands(). finalCmds:%v\n", finalCmds) - - return finalCmds - //return []string{cmd} //do not process string -} - -func (b *BufferflowGrbl) Pause() { - b.SetPaused(true, 0) - //b.BypassMode = false // turn off bypassmode in case it's on - log.Println("Paused buffer on next BlockUntilReady() call") -} - -func (b *BufferflowGrbl) Unpause() { - //unpause buffer by setting paused to false and passing a 1 to b.sem - b.SetPaused(false, 1) - log.Println("Unpaused buffer inside BlockUntilReady() call") -} - -func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { - // remove comments - //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") - //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[!~\\?]|(\u0018)", cmd); match { - log.Printf("Found cmd that should skip buffer. cmd:%v\n", cmd) - return true - } - return false -} - -func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { - // remove comments - //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") - //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[!]", cmd); match { - log.Printf("Found cmd that should pause buffer. cmd:%v\n", cmd) - return true - } - return false -} - -func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { - - //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") - //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[~]", cmd); match { - log.Printf("Found cmd that should unpause buffer. cmd:%v\n", cmd) - return true - } - return false -} - -func (b *BufferflowGrbl) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { - - //cmd = regexp.MustCompile("\\(.*?\\)").ReplaceAllString(cmd, "") - //cmd = regexp.MustCompile(";.*").ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("(\u0018)", cmd); match { - log.Printf("Found cmd that should wipe out and reset buffer. cmd:%v\n", cmd) - - //b.q.Delete() //delete tracking queue, all buffered commands will be wiped. - - //log.Println("Buffer variables cleared for new input.") - return true - } - return false -} - -func (b *BufferflowGrbl) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { - /* - // remove comments - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - if match := b.reNoResponse.MatchString(cmd); match { - log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) - return true - } - */ - return false -} - -func (b *BufferflowGrbl) ReleaseLock() { - log.Println("Lock being released in GRBL buffer") - - b.q.Delete() - - log.Println("ReleaseLock(), so we will send signal of 2 to b.sem to unpause the BlockUntilReady() thread") - - //release lock, send signal 2 to b.sem - b.SetPaused(false, 2) -} - -func (b *BufferflowGrbl) IsBufferGloballySendingBackIncomingData() bool { - //telling json server that we are handling client responses - return true -} - -//Use this function to open a connection, write directly to serial port and close connection. -//This is used for sending query requests outside of the normal buffered operations that will pause to wait for room in the grbl buffer -//'?' is asynchronous to the normal buffer load and does not need to be paused when buffer full -func (b *BufferflowGrbl) rptQueryLoop(p *serport){ - b.parent_serport = p //make note of this port for use in clearing the buffer later, on error. - ticker := time.NewTicker(250 * time.Millisecond) - b.quit = make(chan int) - go func() { - for { - select { - case <- ticker.C: - - n2, err := p.portIo.Write([]byte("?")) - - log.Print("Just wrote ", n2, " bytes to serial: ?") - - if err != nil { - errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." - log.Print(errstr) - h.broadcastSys <- []byte(errstr) - ticker.Stop() //stop query loop if we can't write to the port - break - } - case <- b.quit: - ticker.Stop() - return - } - } - }() -} - -func (b *BufferflowGrbl) Close() { - //stop the status query loop when the serial port is closed off. - log.Println("Stopping the status query loop") - b.quit <- 1 -} - -// Gets the paused state of this buffer -// go-routine safe. -func (b *BufferflowGrbl) GetPaused() bool { - b.lock.Lock() - defer b.lock.Unlock() - return b.Paused -} - -// Sets the paused state of this buffer -// go-routine safe. -func (b *BufferflowGrbl) SetPaused(isPaused bool, semRelease int) { - b.lock.Lock() - defer b.lock.Unlock() - b.Paused = isPaused - - //if we are unpausing the buffer, we need to send a signal to release the channel - if isPaused == false{ - go func() { - // sending a 2 asks BlockUntilReady() to cancel the send - b.sem <- semRelease - defer func() { - log.Printf("Unpause Semaphore just got consumed by the BlockUntilReady()\n") - }() - }() - } -} - -//local version of buffer wipe loop needed to handle pseudo clear buffer (%) without passing that value on to -func (b *BufferflowGrbl) LocalBufferWipe(p *serport){ - log.Printf("Pseudo command received to wipe grbl buffer but *not* send on to grbl controller.") - - // consume all stuff queued - func() { - ctr := 0 - - keepLooping := true - for keepLooping { - select { - case d, ok := <-p.sendBuffered: - log.Printf("Consuming sendBuffered queue. ok:%v, d:%v, id:%v\n", ok, string(d.data), string(d.id)) - ctr++ - - p.itemsInBuffer-- - if ok == false { - keepLooping = false - } - default: - keepLooping = false - log.Println("Hit default in select clause") - } - } - log.Printf("Done consuming sendBuffered cmds. ctr:%v\n", ctr) - }() - - b.ReleaseLock() - - // let user know we wiped queue - log.Printf("itemsInBuffer:%v\n", p.itemsInBuffer) - h.broadcastSys <- []byte("{\"Cmd\":\"WipedQueue\",\"QCnt\":" + strconv.Itoa(p.itemsInBuffer) + ",\"Port\":\"" + p.portConf.Name + "\"}") -} diff --git a/bufferflow_tinyg.go b/bufferflow_tinyg.go deleted file mode 100755 index 11adc9ef9..000000000 --- a/bufferflow_tinyg.go +++ /dev/null @@ -1,926 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "regexp" - //"strconv" - "strings" - "sync" - //"time" - "errors" - "fmt" - "runtime/debug" - "time" -) - -type BufferflowTinyg struct { - Name string - Port string - Paused bool - //StopSending int - //StartSending int - //PauseOnEachSend time.Duration // Amount of milliseconds to pause on each send to give TinyG time to send us a qr report - sem chan int // semaphore to wait on until given release - LatestData string // this holds the latest data across multiple serial reads so we can analyze it for qr responses - //BypassMode bool // this means don't actually watch for qr responses until we know tinyg is in qr response mode - //wg sync.WaitGroup - - quit chan int - parent_serport *serport - - re *regexp.Regexp - reNewLine *regexp.Regexp - reQrOff *regexp.Regexp - reQrOn *regexp.Regexp - reNoResponse *regexp.Regexp - reComment *regexp.Regexp - reComment2 *regexp.Regexp - rePutBackInJsonMode *regexp.Regexp - reJsonVerbositySetTo0 *regexp.Regexp - reCrLfSetTo1 *regexp.Regexp - reRxResponse *regexp.Regexp - reFlowChar *regexp.Regexp - - // slot counter approach - reSlotDone *regexp.Regexp // the r:null cmd to look for back from tinyg indicating line processed - //reCmdsWithNoRResponse *regexp.Regexp // since we're using slot approach, we expect an r:{} response, but some commands don't give that so just don't expect it - //SlotMax int // queue into tinyg using slot approach - //SlotCtr int // queue into tinyg using slot approach - - //lock *sync.Mutex // use a lock/unlock instead of sem chan int - - // do buffer size counting approach instead - BufferMax int - //BufferSize int - //BufferSizeArray []int - //BufferCmdArray []string - q *Queue - - // use thread locking for b.Paused - lock *sync.Mutex - - // use more thread locking for b.semLock - semLock *sync.Mutex -} - -type GcodeCmd struct { - Cmd string - Id string -} - -type BufFlowCmd struct { - Cmd string - Gcode string - Resp string - Id string - HowMuchWeThinkWeShouldRemove int - HowMuchTinyTellsUsToRemove int - IsMatchOnBufDecreaseCnt bool - IsErr bool - Err string - //TotalInBufPerSpjs int - //TotalInBufPerTinyG int -} - -type BufFlowRx struct { - Cmd string - Resp string - IsMatchOnTotalBuf bool - IsErr bool - Err string - TotalInBufPerSpjs int - TotalInBufPerTinyG int -} - -// RawString is a raw encoded JSON object. -// It implements Marshaler and Unmarshaler and can -// be used to delay JSON decoding or precompute a JSON encoding. -type RawString string - -// MarshalJSON returns *m as the JSON encoding of m. -func (m *RawString) MarshalJSON() ([]byte, error) { - return []byte(*m), nil -} - -// UnmarshalJSON sets *m to a copy of data. -func (m *RawString) UnmarshalJSON(data []byte) error { - if m == nil { - return errors.New("RawString: UnmarshalJSON on nil pointer") - } - *m += RawString(data) - return nil -} - -type RespMsg struct { - R RawString `json:"r",sql:"type:json"` - F []int `json:"f"` -} - -type RespRxMsg struct { - R RxMsg `json:"r"` - F []int `json:"f"` -} - -type RxMsg struct { - Rx int `json:"rx"` -} - -func (b *BufferflowTinyg) Init() { - - b.Paused = false - b.lock = &sync.Mutex{} - b.semLock = &sync.Mutex{} - //b.SetPaused(false, 2) - - /* Slot Approach */ - //b.SlotMax = 4 // at most queue up 2 slots, i.e. 2 gcode commands - //b.SlotCtr = 0 // 0 indicates no gcode lines have been queued into tinyg - // the regular expression to turn off the pause - // this regexp will find the r:null response which indicates - // a line of gcode was processed and thus we can send the next one - // {"r":{},"f":[1,0,33,134]} - // when we see this, decrement the b.SlotCtr - b.reSlotDone, _ = regexp.Compile("{\"r\":{") - // when we see the response to an rx query so we know how many chars - // are sitting in the serial buffer - b.reRxResponse, _ = regexp.Compile("{\"rx\":") - b.reFlowChar, _ = regexp.Compile("\u0011|\u0013") - - //b.reCmdsWithNoRResponse, _ = regexp.Compile("[!~%]") - //log.Printf("Using slot approach for TinyG buffering. slotMax:%v, slotCtr:%v\n", b.SlotMax, b.SlotCtr) - - /* End Slot Approach Items */ - - /* Start Buffer Size Approach Items */ - b.BufferMax = 200 //max buffer size 254 bytes available - //b.BufferSize = 0 //initialize buffer at zero bytes - b.q = NewQueue() - //b.lock = sync.Mutex - /* End Buffer Size Approach */ - - //b.StartSending = 20 - //b.StopSending = 18 - //b.PauseOnEachSend = 0 * time.Millisecond - - // make buffered channel big enough we won't overflow it - // meaning we get told b.sem on incoming data, so at most this could - // be the size of 1 character and the TinyG only allows 255, so just - // go high to make sure it's high enough to never block - // buffered - b.sem = make(chan int, 1000) - // non-buffered - //b.sem = make(chan int) - - // start tinyg out in bypass mode because we don't really - // know if user put tinyg into qr response mode. what we'll - // do is watch for our first qr response and then assume we're - // in active mode, i.e. b.BypassMode should then be set to false - // the reason for this is if we think tinyg is going to send qr - // responses and we don't get them, we end up holding up all data - // and essentially break everything. so gotta really watch for this. - //b.BypassMode = true - // looking like bypassmode isn't very helpful - //b.BypassMode = false - - // the regular expression to find the qr value - // this regexp will find qr when in json mode or non-json mode on tinyg - b.re, _ = regexp.Compile("\"{0,1}qr\"{0,1}:(\\d+)") - - //reWipeToQr, _ = regexp.Compile("(?s)^.*?\"qr\":\\d+") - - // we split the incoming data on newline using this regexp - // tinyg seems to only send \n but look for \n\r optionally just in case - b.reNewLine, _ = regexp.Compile("\\r{0,1}\\n") - - // Look for qr's being turned off by user to auto turn-on BypassMode - /* - $qv - [qv] queue report verbosity 2 [0=off,1=single,2=triple] - $qv=0 - [qv] queue report verbosity 0 [0=off,1=single,2=triple] - {"qv":""} - {"r":{"qv":0},"f":[1,0,10,5788]} - */ - b.reQrOff, _ = regexp.Compile("{\"qv\":0}|\\[qv\\]\\s+queue report verbosity\\s+0") - - // Look for qr's being turned ON by user to auto turn-off BypassMode - /* - $qv - [qv] queue report verbosity 3 [0=off,1=single,2=triple] - {"qv":""} - {"r":{"qv":3},"f":[1,0,10,5066]} - */ - b.reQrOn, _ = regexp.Compile("{\"qv\":[1-9]}|\\[qv\\]\\s+queue report verbosity\\s+[1-9]") - - // this regexp catches !, ~, %, \n, $ by itself, or $$ by itself and indicates - // no r:{} response will come back so don't expect it - b.reNoResponse, _ = regexp.Compile("^[!~%\n$?]") - - // if we get a cmd with a $ at the start or a ? at start, append - // a new command that will put tinyg back in json mode - b.rePutBackInJsonMode, _ = regexp.Compile("^[$?]") - - // see if they tried to turn off json verbosity, which will break things - b.reJsonVerbositySetTo0, _ = regexp.Compile("(\\$jv\\=0|\\{\"jv\"\\:0\\})") - - // see if they tried to turn on CRLF, which will break things - b.reCrLfSetTo1, _ = regexp.Compile("(\\$ec\\=1|\\{\"ec\"\\:1\\})") - - b.reComment, _ = regexp.Compile("\\(.*?\\)") - b.reComment2, _ = regexp.Compile(";.*") - - //initialize query loop - //b.rxQueryLoop(b.parent_serport) -} - -// Serial buffer size approach -func (b *BufferflowTinyg) BlockUntilReady(cmd string, id string) (bool, bool) { - log.Printf("BlockUntilReady(cmd:%v, id:%v) start\n", cmd, id) - - // Since BlockUntilReady is in the writer thread, lock so the reader - // thread doesn't get messed up from all the bufferarray counting we're doing - //b.lock.Lock() - //defer b.lock.Unlock() - - // Here we add the length of the new command to the buffer size and append the length - // to the buffer array. Check if buffersize > buffermax and if so we pause and await free space before - // sending the command to grbl. - - // Only increment if cmd is something we'll get an r:{} response to - isReturnsNoResponse := b.SeeIfSpecificCommandsReturnNoResponse(cmd) - if isReturnsNoResponse == false { - - b.q.Push(cmd, id) - /* - log.Printf("Going to lock inside BlockUntilReady to up the BufferSize and Arrays\n") - b.lock.Lock() - b.BufferSize += len(cmd) - b.BufferSizeArray = append(b.BufferSizeArray, len(cmd)) - b.BufferCmdArray = append(b.BufferCmdArray, cmd) - b.lock.Unlock() - log.Printf("Done locking inside BlockUntilReady to up the BufferSize and Arrays\n") - */ - } else { - // this is sketchy. could we overrun the buffer by not counting !~%\n - // so to give extra room don't actually allow full serial buffer to - // be used in b.BufferMax - //log.Printf("Not incrementing buffer size for cmd:%v\n", cmd) - - } - - log.Printf("New line length: %v, buffer size increased to:%v\n", len(cmd), b.q.LenOfCmds()) - //log.Println(b.BufferSizeArray) - //log.Println(b.BufferCmdArray) - - //b.lock.Lock() - if b.q.LenOfCmds() >= b.BufferMax { - b.SetPaused(true, 0) // b.Paused = true - log.Printf("It looks like the buffer is over the allowed size, so we are going to paused. Then when some incoming responses come in a check will occur to see if there's room to send this command. Pausing...") - } - //b.lock.Lock() - - if b.GetPaused() { - log.Println("It appears we are being asked to pause, so we will wait on b.sem") - // We are being asked to pause our sending of commands - - // clear all b.sem signals so when we block below, we truly block - b.ClearOutSemaphore() - - log.Println("Blocking on b.sem until told from OnIncomingData to go") - unblockType, ok := <-b.sem // will block until told from OnIncomingData to go - - log.Printf("Done blocking cuz got b.sem semaphore release. ok:%v, unblockType:%v\n", ok, unblockType) - - // we get an unblockType of 1 for normal unblocks - // we get an unblockType of 2 when we're being asked to wipe the buffer, i.e. from a % cmd - if unblockType == 2 { - log.Println("This was an unblock of type 2, which means we're being asked to wipe internal buffer. so return false.") - // returning false asks the calling method to wipe the serial send once - // this function returns - return false, false - } - } - - // we will get here when we're done blocking and if we weren't cancelled - // if this cmd returns no response, we need to generate a fake "Complete" - // so do it now - willHandleCompleteResponse := true - if isReturnsNoResponse == true { - willHandleCompleteResponse = false - } - - log.Printf("BlockUntilReady(cmd:%v, id:%v) end\n", cmd, id) - - return true, willHandleCompleteResponse -} - -// Serial buffer size approach -func (b *BufferflowTinyg) OnIncomingData(data string) { - //log.Printf("OnIncomingData() start. data:%q\n", data) - - // Since OnIncomingData is in the reader thread, lock so the writer - // thread doesn't get messed up from all the bufferarray counting we're doing - //b.lock.Lock() - //defer b.lock.Unlock() - - b.LatestData += data - - //it was found ok was only received with status responses until the grbl buffer is full. - //b.LatestData = regexp.MustCompile(">\\r\\nok").ReplaceAllString(b.LatestData, ">") //remove oks from status responses - - arrLines := b.reNewLine.Split(b.LatestData, -1) - //js, _ := json.Marshal(arrLines) - //log.Printf("cnt:%v, arrLines:%v\n", len(arrLines), string(js)) - - if len(arrLines) > 1 { - // that means we found a newline and have 2 or greater array values - // so we need to analyze our arrLines[] lines but keep last line - // for next trip into OnIncomingData - //log.Printf("We have data lines to analyze. numLines:%v\n", len(arrLines)) - - } else { - // we don't have a newline yet, so just exit and move on - // we don't have to reset b.LatestData because we ended up - // without any newlines so maybe we will next time into this method - //log.Printf("Did not find newline yet, so nothing to analyze\n") - return - } - - // if we made it here we have lines to analyze - // so analyze all of them except the last line - for _, element := range arrLines[:len(arrLines)-1] { - //log.Printf("Working on element:%v, index:%v", element, index) - //log.Printf("Working on element:%v, index:%v", element) - - //check for r:{} response indicating a gcode line has been processed - if b.reSlotDone.MatchString(element) { - - //log.Printf("Going to lock inside OnIncomingData to decrease the BufferSize and reset Arrays\n") - //b.lock.Lock() - - //if b.BufferSizeArray != nil { - // ok, a line has been processed, the if statement below better - // be guaranteed to be true, cuz if its not we did something wrong - if b.q.Len() > 0 { - //b.BufferSize -= b.BufferSizeArray[0] - doneCmd, id := b.q.Poll() - - //doneCmd := b.BufferCmdArray[0] - // Send cmd:"Complete" back - m := DataCmdComplete{"Complete", id, b.Port, b.q.LenOfCmds(), doneCmd} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - - // ok, here's the deal. it seems that sometimes we may miss - // an r:{} coming back to us``` - - /* - if len(b.BufferSizeArray) > 1 { - b.BufferSizeArray = b.BufferSizeArray[1:len(b.BufferSizeArray)] - b.BufferCmdArray = b.BufferCmdArray[1:len(b.BufferCmdArray)] - } else { - b.BufferSizeArray = nil - b.BufferCmdArray = nil - } - */ - - log.Printf("Buffer decreased to itemCnt:%v, lenOfBuf:%v\n", b.q.Len(), b.q.LenOfCmds()) - - if *bufFlowDebugType == "on" { - // let's report on how our buffer is doing - // we need to unmarshall this r:{} response - - // do some initial cleanup to remove \u0011 or \u0013 - // that we're getting likely for flow control that is - // throwing off the unmarshal call - element2 := b.reFlowChar.ReplaceAllString(element, "") - - // unmarshall r:{} json - var rm RespMsg - err2 := json.Unmarshal([]byte(element2), &rm) - - bfc := BufFlowCmd{} - bfc.Cmd = "BufFlowDebug" - bfc.Gcode = doneCmd - bfc.Resp = element - bfc.Id = id - bfc.HowMuchWeThinkWeShouldRemove = len(doneCmd) - bfc.IsErr = false - bfc.IsMatchOnBufDecreaseCnt = false - - if err2 != nil { - log.Printf("Problem decoding json on r:{} response. giving up. json:%v, err:%v\n", element, err2) - spErr(fmt.Sprintf("Problem decoding json on r:{} response. giving up. json:%v, err:%v", element, err2)) - bfc.IsErr = true - bfc.Err = "Problem unmarshalling json which likely means we had dropped characters on the serial buffer. Giving up." - //return - } else { - log.Printf("RespMsg:%v\n", rm) - - if len(rm.F) > 2 { - bfc.HowMuchTinyTellsUsToRemove = rm.F[2] - if rm.F[2] == len(doneCmd) { - bfc.IsMatchOnBufDecreaseCnt = true - } else { - bfc.IsMatchOnBufDecreaseCnt = false - } - } - - } - - bfcm, err3 := json.Marshal(bfc) - if err3 == nil { - h.broadcastSys <- bfcm - } else { - log.Fatal(fmt.Sprintf("Could not marshal the buffer flow debug json response. We should never get here and since we did we are exiting so you can debug me. Giving up. json:%v, err:%v", element, err3)) - } - - // also check for rx value being returned so we can decide - // if our serial buffer value is the same as what TinyG thinks - // it should be. - if b.reRxResponse.MatchString(element) { - var rrxm RespRxMsg - err4 := json.Unmarshal([]byte(element2), &rrxm) - - bfrx := BufFlowRx{} - bfrx.Cmd = "BufFlowRxDebug" - bfrx.Resp = element - bfrx.IsErr = false - bfrx.IsMatchOnTotalBuf = false - bfrx.TotalInBufPerSpjs = b.q.LenOfCmds() - - if err4 != nil { - bfrx.IsErr = true - bfrx.Err = "Could not unmarshall the r:rx json string? huh?" - } else { - bfrx.TotalInBufPerTinyG = 254 - rrxm.R.Rx - - // do they match? - if bfrx.TotalInBufPerSpjs == bfrx.TotalInBufPerTinyG { - bfrx.IsMatchOnTotalBuf = true - } else { - bfrx.IsMatchOnTotalBuf = false - } - } - - bfrxm, err5 := json.Marshal(bfrx) - if err5 == nil { - h.broadcastSys <- bfrxm - } else { - log.Fatal(fmt.Sprintf("Could not marshal the buffer flow debug RX json response. We should never get here and since we did we are exiting so you can debug me. Giving up. json:%v, err:%v", element, err5)) - } - - } - } - - } else { - 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!!!!") - } - - //if b.BufferSize < b.BufferMax { - // We should have our queue dequeued so lets see if we are now below - // the allowed buffer room. If so go ahead and release the block on send - // This if stmt still may not be true here because we could have had a tiny - // cmd just get completed like "G0 X0" and the next cmd is long like "G2 X23.32342 Y23.535355 Z1.04345 I0.243242 J-0.232455" - // So we'll have to wait until the next time in here for this test to pass - if b.q.LenOfCmds() < b.BufferMax { - - log.Printf("tinyg just completed a line of gcode and there is room in buffer so setPaused(false)\n") - - // if we are paused, tell us to unpause cuz we have clean buffer room now - if b.GetPaused() { - b.SetPaused(false, 1) //set paused to false first, then release the hold on the buffer - } - - /* - // if we are paused, tell us to unpause cuz we have clean buffer room now - b.lock.Lock() - if b.Paused { - - b.Paused = false - - // send signal to the OnBlockUntilReady method - // to let it start running again - b.sem <- 1 - // do this in a goroutine because if multiple sends into the channel - // occur then the write into the channel will block. we also want - // to print out debug info when the channel gets consumed so this - // helps us do that. however, this is a bit inefficient, so could - // convert b.sem to a buffered channel and just not get debug output - // or even move to a sync.lock.mutex - - go func() { - gcodeline := element - - // changed b.SetPaused to here per version 1.75 and Jarret's testing - //b.SetPaused(false) //set paused to false first, then release the hold on the buffer - - log.Printf("StartSending Semaphore goroutine created for gcodeline:%v\n", gcodeline) - b.sem <- 1 - - defer func() { - gcodeline := gcodeline - log.Printf("StartSending Semaphore just got consumed by the BlockUntilReady() thread for the gcodeline:%v\n", gcodeline) - }() - }() - - } - b.lock.Unlock() - */ - // let's set that we are no longer paused - // Not running b.SetPaused() here anymore per version 1.75 - //b.SetPaused(false) //b.Paused = false - } - //b.lock.Unlock() - //log.Printf("Done locking inside OnIncomingData\n") - } - - // handle communication back to client - // for base serial data (this is not the cmd:"Write" or cmd:"Complete") - m := DataPerLine{b.Port, element + "\n"} - bm, err := json.Marshal(m) - if err == nil { - h.broadcastSys <- bm - } - - } // for loop - - // now wipe the LatestData to only have the last line that we did not analyze - // because we didn't know/think that was a full command yet - b.LatestData = arrLines[len(arrLines)-1] - - // we are losing incoming serial data because of garbageCollection() - // doing a "stop the world" and all this data queues up back on the - // tinyg and we miss stuff coming in, which gets our serial counter off - // and then causes stalling, so we're going to attempt to force garbageCollection - // each time we get data so that we don't have pauses as long as we were having - if *gcType == "max" { - debug.FreeOSMemory() - } - - //time.Sleep(3000 * time.Millisecond) - //log.Printf("OnIncomingData() end.\n") -} - -// Clean out b.sem so it can truly block -func (b *BufferflowTinyg) ClearOutSemaphore() { - ctr := 0 - - keepLooping := true - for keepLooping { - select { - case d, ok := <-b.sem: - log.Printf("Consuming b.sem queue to clear it before we block. ok:%v, d:%v\n", ok, string(d)) - ctr++ - if ok == false { - keepLooping = false - } - default: - keepLooping = false - log.Println("Hit default in select clause") - } - } - log.Printf("Done consuming b.sem queue so we're good to block on it now. ctr:%v\n", ctr) - // ok, all b.sem signals are now consumed into la-la land - -} - -// break commands into individual commands -// so, for example, break on newlines to separate commands -// or, in the case of ~% break those onto separate commands -func (b *BufferflowTinyg) BreakApartCommands(cmd string) []string { - // add newline after !~% - reSingle := regexp.MustCompile("([!~%])") - cmd = reSingle.ReplaceAllString(cmd, "$1\n") - cmds := strings.Split(cmd, "\n") - //log.Printf("Len of cmds array after split:%v\n", len(cmds)) - //json, _ := json.Marshal(cmds) - //log.Printf("cmds after split:%v\n", json) - finalCmds := []string{} - if len(cmds) == 1 { - item := cmds[0] - // just put cmd back in with newline - if reSingle.MatchString(item) { - //log.Printf("len1. Added cmd back. Not re-adding newline cuz artificially added one earlier. item:'%v'\n", item) - } - finalCmds = append(finalCmds, item) - } else { - for index, item := range cmds { - // since more than 1 cmd, loop thru - if reSingle.MatchString(item) { - //log.Printf("Added cmd back. Not re-adding newline cuz artificially added one earlier. item:'%v'\n", item) - finalCmds = append(finalCmds, item) - } else { - // should we add back our newline? do this if there are elements after us - if index < len(cmds)-1 { - // there are cmds after me, so add newline - //log.Printf("Re-adding newline to item:%v\n", item) - s := item + "\n" - finalCmds = append(finalCmds, s) - //log.Printf("Added cmd back with newline. New cmd item:'%v'\n", s) - } else { - //log.Printf("Skipping adding cmd back cuz just empty newline. item:'%v'\n", item) - //log.Printf("Re-adding item to finalCmds without adding newline:%v\n", item) - //finalCmds = append(finalCmds, item) - } - - } - } - } - - // loop 1 more time to do some rewriting - newFinalCmds := []string{} - for _, item := range finalCmds { - // remove comments - //item = b.reComment.ReplaceAllString(item, "") - //item = b.reComment2.ReplaceAllString(item, "") - - // see if we need to override a cmd to not screw stuff up for us - // if user sets json verbosity to 0, reset it back - if match := b.reJsonVerbositySetTo0.MatchString(item); match { - // they turned off json verbosity, shame on them, override it - // by setting back - newFinalCmds = append(newFinalCmds, "{\"jv\":1}\n") - } else if match := b.reCrLfSetTo1.MatchString(item); match { - // they turned off json verbosity, shame on them, override it - // by setting back - newFinalCmds = append(newFinalCmds, "{\"ec\":0}\n") - - } else { - - // just put the command back into the array without modifying - newFinalCmds = append(newFinalCmds, item) - } - - // see if need to put back in json mode - if match := b.rePutBackInJsonMode.MatchString(item); match { - // yes, this cmd needs to have us put tinyg back in json mode - newFinalCmds = append(newFinalCmds, "{\"ej\":\"\"}\n") - } - } - - //log.Printf("Final array of cmds after BreakApartCommands(). newFinalCmds:%v\n", newFinalCmds) - return newFinalCmds -} - -func (b *BufferflowTinyg) Pause() { - - // Since we're tweaking b.Paused lock all threads - //b.lock.Lock() - //defer b.lock.Unlock() - - b.SetPaused(true, 0) //b.Paused = true - //b.BypassMode = false // turn off bypassmode in case it's on - //log.Println("Paused buffer on next BlockUntilReady() call") -} - -func (b *BufferflowTinyg) Unpause() { - - // Since we're tweaking b.Paused lock all threads - //b.lock.Lock() - //defer b.lock.Unlock() - - b.SetPaused(false, 1) //b.Paused = false - //log.Println("Unpause(), so we will send signal of 1 to b.sem to unpause the BlockUntilReady() thread") - - // do this as go-routine so we don't block on the b.sem <- 1 write - /* - go func() { - - log.Printf("Unpause() Semaphore goroutine created.\n") - // this is an unbuffered channel, so we will - // block here which is why this is a goroutine - - // sending a 1 asks BlockUntilReady() to move forward - b.sem <- 1 - // when we get here that means a BlockUntilReady() - // method consumed the signal, meaning we unblocked them - // which is good because they're allowed to start sending - // again - defer func() { - log.Printf("Unpause() Semaphore just got consumed by the BlockUntilReady()\n") - }() - }() - */ - log.Println("Unpaused buffer inside BlockUntilReady() call") -} - -func (b *BufferflowTinyg) SeeIfSpecificCommandsShouldSkipBuffer(cmd string) bool { - // remove comments - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[!~%]", cmd); match { - log.Printf("Found cmd that should skip buffer. cmd:%v\n", cmd) - return true - } - return false -} - -func (b *BufferflowTinyg) SeeIfSpecificCommandsShouldPauseBuffer(cmd string) bool { - // remove comments - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[!]", cmd); match { - //log.Printf("Found cmd that should pause buffer. cmd:%v\n", cmd) - return true - } - return false -} - -func (b *BufferflowTinyg) SeeIfSpecificCommandsShouldUnpauseBuffer(cmd string) bool { - // remove comments - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[~%]", cmd); match { - //log.Printf("Found cmd that should unpause buffer. cmd:%v\n", cmd) - return true - } - return false -} - -func (b *BufferflowTinyg) SeeIfSpecificCommandsShouldWipeBuffer(cmd string) bool { - // remove comments - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - if match, _ := regexp.MatchString("[%]", cmd); match { - //log.Printf("Found cmd that should wipe out and reset buffer. cmd:%v\n", cmd) - - // Since we're tweaking b.Paused lock all threads - //b.lock.Lock() - //defer b.lock.Unlock() - - //b.BufferSize = 0 - //b.BufferSizeArray = nil - //b.BufferCmdArray = nil - //b.q.Delete() - return true - } - return false -} - -func (b *BufferflowTinyg) SeeIfSpecificCommandsReturnNoResponse(cmd string) bool { - // remove comments - //cmd = b.reComment.ReplaceAllString(cmd, "") - //cmd = b.reComment2.ReplaceAllString(cmd, "") - log.Printf("Checking cmd:%v for no response?", cmd) - if match := b.reNoResponse.MatchString(cmd); match { - //log.Printf("Found cmd that does not get a response from TinyG. cmd:%v\n", cmd) - return true - } - return false -} - -/* -func (b *BufferflowTinyg) RewriteCmd(cmd string) string { - // remove comments from cmd. why bother sending them to tinyg and wasting - // precious serial buffer? - cmd = b.reComment.ReplaceAllString(cmd, "") - cmd = b.reComment2.ReplaceAllString(cmd, "") - // if cmd is $, ?, $x=1, etc then rewrap in json - if match, _ := regexp.MatchString("^[$?]", cmd); match { - log.Printf("Found cmd that should be wrapped in json. cmd:%v\n", cmd) - - } - return cmd -} -*/ - -// This is called if user wiped entire buffer of gcode commands queued up -// which is up to 25,000 of them. So, we need to release the OnBlockUntilReady() -// in a way where the command will not get executed, so send unblockType of 2 -func (b *BufferflowTinyg) ReleaseLock() { - log.Println("Lock being released in TinyG buffer") - - b.q.Delete() - b.SetPaused(false, 2) - /* - // Since we're tweaking b.Paused lock all threads - b.lock.Lock() - - b.Paused = false - b.SlotCtr = 0 - b.BufferSize = 0 - b.BufferSizeArray = nil - b.BufferCmdArray = nil - - b.lock.Unlock() - */ - /* - log.Println("ReleaseLock(), so we will send signal of 2 to b.sem to unpause the BlockUntilReady() thread") - go func() { - - log.Printf("ReleaseLock() Semaphore goroutine created.\n") - // this is an unbuffered channel, so we will - // block here which is why this is a goroutine - - // sending a 2 asks BlockUntilReady() to cancel the send - b.sem <- 2 - // when we get here that means a BlockUntilReady() - // method consumed the signal, meaning we unblocked them - // which is good because they're allowed to start sending - // again - defer func() { - log.Printf("ReleaseLock() Semaphore just got consumed by the BlockUntilReady()\n") - }() - }() - */ -} - -func (b *BufferflowTinyg) IsBufferGloballySendingBackIncomingData() bool { - // we want to send back incoming data as per line data - // rather than having the default spjs implemenation that sends back data - // as it sees it. the reason is that we were getting packets out of order - // on the browser on bad internet connections. that will still happen with us - // sending back per line data, but at least it will allow the browser to parse - // correct json now. - // TODO: The right way to solve this is to watch for an acknowledgement - // from the browser and queue stuff up until the acknowledgement and then - // send the full blast of ganged up data - return true -} - -//Use this function to open a connection, write directly to serial port and close connection. -//This is used for sending query requests outside of the normal buffered operations that will pause to wait for room in the grbl buffer -//'?' is asynchronous to the normal buffer load and does not need to be paused when buffer full -func (b *BufferflowTinyg) rxQueryLoop(p *serport) { - b.parent_serport = p //make note of this port for use in clearing the buffer later, on error. - ticker := time.NewTicker(5000 * time.Millisecond) - b.quit = make(chan int) - go func() { - for { - select { - case <-ticker.C: - - // we'll write a lazy formatted version of json to reduce the amt of chars - // chewed up since we're doing this outside the scope of the serial buffer counter - n2, err := p.portIo.Write([]byte("{rx:n}\n")) - - log.Print("Just wrote ", n2, " bytes to serial: {rx:n}") - - if err != nil { - errstr := "Error writing to " + p.portConf.Name + " " + err.Error() + " Closing port." - log.Print(errstr) - h.broadcastSys <- []byte(errstr) - ticker.Stop() //stop query loop if we can't write to the port - break - } - case <-b.quit: - ticker.Stop() - return - } - } - }() -} - -func (b *BufferflowTinyg) Close() { - //stop the rx query loop when the serial port is closed off. - log.Println("Stopping the RX query loop") - b.ReleaseLock() - b.Unpause() - go func() { - b.quit <- 1 - }() -} - -// Gets the paused state of this buffer -// go-routine safe. -func (b *BufferflowTinyg) GetPaused() bool { - b.lock.Lock() - defer b.lock.Unlock() - return b.Paused -} - -// Sets the paused state of this buffer -// go-routine safe. -func (b *BufferflowTinyg) SetPaused(isPaused bool, semRelease int) { - b.lock.Lock() - defer b.lock.Unlock() - b.Paused = isPaused - - // only release semaphore if we are being told to unpause - if b.Paused == false { - // the BlockUntilReady thread should be sitting waiting - // so when we send this should trigger it - b.sem <- semRelease - - // since the first consuming of the semRelease will occur - // by BlockUntilReady since it's sitting waiting then - // we're good to go ahead and release the rest here - // so our queue doesn't fill up - // that's the theory anyway - //b.ClearOutSemaphore() - } - //go func() { - //log.Printf("StartSending Semaphore goroutine created for gcodeline:%v\n", gcodeline) - //b.sem <- semRelease - - /* - defer func() { - //log.Printf("StartSending Semaphore just got consumed by the BlockUntilReady() thread for the gcodeline:%v\n", gcodeline) - }() - */ - //}() -} diff --git a/compile_webidebridge.sh b/compile_webidebridge.sh index e0349461e..6737c6ec1 100755 --- a/compile_webidebridge.sh +++ b/compile_webidebridge.sh @@ -14,7 +14,7 @@ NC='\e[0m' # No Color extractVersionFromMain() { - VERSION=`grep versionFloat main.go | cut -d "(" -f2 | cut -d ")" -f1` + VERSION=`grep version main.go | cut -d "\"" -f2 | cut -d "\"" -f1 | head -1` } createZipEmbeddableFileArduino() @@ -75,7 +75,7 @@ compilePlatform() fi echo createZipEmbeddableFileArduino $GOOS $GOARCH $NAME createZipEmbeddableFileArduino $GOOS $GOARCH $NAME - #GOOS=$GOOS GOARCH=$GOARCH go-selfupdate $NAME $VERSION + GOOS=$GOOS GOARCH=$GOARCH go-selfupdate $NAME $VERSION rm -rf $NAME* } diff --git a/config.ini b/config.ini index d06b361e8..51c6b209d 100644 --- a/config.ini +++ b/config.ini @@ -1,10 +1,10 @@ addr = :8989 # http service address -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 configUpdateInterval = 0 # Update interval for re-reading config file set via -config flag. Zero disables config file re-reading. 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) hostname = unknown-hostname # Override the hostname we get from the OS ls = false # launch self 5 seconds later regex = usb|acm|com # Regular expression to filter serial port list v = true # show debug logging -appName = Arduino_Create_Bridge -updateUrl = http://localhost/ +appName = CreateBridge +updateUrl = http://downloads.arduino.cc/ +#updateUrl = http://localhost/ diff --git a/conn.go b/conn.go index 9ffdd2ed3..e6981a75c 100644 --- a/conn.go +++ b/conn.go @@ -3,10 +3,11 @@ package main import ( + log "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" "github.com/googollee/go-socket.io" - "log" "net/http" + "strconv" ) type connection struct { @@ -46,9 +47,26 @@ func uploadHandler(c *gin.Context) { board := c.PostForm("board") if board == "" { c.String(http.StatusBadRequest, "board is required") + log.Error("board is required") return } board_rewrite := c.PostForm("board_rewrite") + + var extraInfo boardExtraInfo + + extraInfo.authdata.UserName = c.PostForm("auth_user") + extraInfo.authdata.Password = c.PostForm("auth_pass") + commandline := c.PostForm("commandline") + extraInfo.use_1200bps_touch, _ = strconv.ParseBool(c.PostForm("use_1200bps_touch")) + extraInfo.wait_for_upload_port, _ = strconv.ParseBool(c.PostForm("wait_for_upload_port")) + extraInfo.networkPort, _ = strconv.ParseBool(c.PostForm("network")) + + if extraInfo.networkPort == false && commandline == "" { + c.String(http.StatusBadRequest, "commandline is required for local board") + log.Error("commandline is required for local board") + return + } + sketch, header, err := c.Request.FormFile("sketch_hex") if err != nil { c.String(http.StatusBadRequest, err.Error()) @@ -60,7 +78,7 @@ func uploadHandler(c *gin.Context) { c.String(http.StatusBadRequest, err.Error()) } - go spProgramRW(port, board, board_rewrite, path) + go spProgramRW(port, board, board_rewrite, path, commandline, extraInfo) } } diff --git a/discovery.go b/discovery.go index f02a2a3da..c59838191 100644 --- a/discovery.go +++ b/discovery.go @@ -29,8 +29,8 @@ package main import ( + log "github.com/Sirupsen/logrus" "github.com/oleksandr/bonjour" - "log" "net" "strings" "time" @@ -53,7 +53,7 @@ func GetNetworkList() ([]OsSerialPort, error) { SavedNetworkPorts = Filter(SavedNetworkPorts, func(port OsSerialPort) bool { any := true for _, p := range newPorts { - if p.Name == port.Name && p.FriendlyName == port.FriendlyName { + if p.Name == port.Name { any = false return any } @@ -125,13 +125,7 @@ func getPorts() ([]OsSerialPort, error) { arrPorts := []OsSerialPort{} go func(results chan *bonjour.ServiceEntry, exitCh chan<- bool) { for e := range results { - var boardInfosSlice []string - for _, element := range e.Text { - if strings.Contains(element, "board=yun") { - boardInfosSlice = append(boardInfosSlice, "arduino:avr:yun") - } - } - arrPorts = append(arrPorts, OsSerialPort{Name: e.AddrIPv4.String(), FriendlyName: e.Instance, NetworkPort: true, RelatedNames: boardInfosSlice}) + arrPorts = append(arrPorts, OsSerialPort{Name: e.AddrIPv4.String(), IdVendor: strings.Join(e.Text[:], " "), NetworkPort: true}) } timeout <- true }(results, resolver.Exit) diff --git a/download.go b/download.go index 2dc2a93ed..52006fec8 100644 --- a/download.go +++ b/download.go @@ -3,18 +3,19 @@ package main import ( "errors" + log "github.com/Sirupsen/logrus" "io" "io/ioutil" - "log" "net/http" "os" "path/filepath" + "runtime" "strings" ) func saveFileonTempDir(filename string, sketch io.Reader) (path string, err error) { // create tmp dir - tmpdir, err := ioutil.TempDir("", "serial-port-json-server") + tmpdir, err := ioutil.TempDir("", "arduino-create-agent") if err != nil { return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?") } @@ -47,7 +48,7 @@ func downloadFromUrl(url string) (filename string, err error) { url = strings.TrimSpace(url) // create tmp dir - tmpdir, err := ioutil.TempDir("", "serial-port-json-server") + tmpdir, err := ioutil.TempDir("", "arduino-create-agent") if err != nil { return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?") } @@ -83,3 +84,21 @@ func downloadFromUrl(url string) (filename string, err error) { return fileName, nil } + +func spDownloadTool(name string, url string) { + + if _, err := os.Stat(tempToolsPath + "/" + name); err != nil { + + fileName, err := downloadFromUrl(url + "/" + name + "-" + runtime.GOOS + "-" + runtime.GOARCH + ".zip") + if err != nil { + log.Error("Could not download flashing tools!") + return + } + Unzip(fileName, tempToolsPath) + } else { + log.Info("Tool already present, skipping download") + } + + // will be something like ${tempfolder}/avrdude/bin/avrdude + globalToolsMap["{runtime.tools."+name+".path}"] = tempToolsPath + "/" + name +} diff --git a/dummy.go b/dummy.go deleted file mode 100644 index 79bb57de6..000000000 --- a/dummy.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" -) - -type dummy struct { - //myvar mytype string -} - -var d = dummy{ -//myvar: make(mytype string), -} - -func (d *dummy) run() { - for { - //h.broadcast <- message - log.Print("dummy data") - //h.broadcast <- []byte("dummy data") - time.Sleep(8000 * time.Millisecond) - h.broadcast <- []byte("list") - - // open com4 (tinyg) - h.broadcast <- []byte("open com4 115200 tinyg") - time.Sleep(1000 * time.Millisecond) - - // send some commands - //h.broadcast <- []byte("send com4 ?\n") - //time.Sleep(3000 * time.Millisecond) - h.broadcast <- []byte("send com4 {\"qr\":\"\"}\n") - h.broadcast <- []byte("send com4 g21 g90\n") // mm - //h.broadcast <- []byte("send com4 {\"qr\":\"\"}\n") - //h.broadcast <- []byte("send com4 {\"sv\":0}\n") - //time.Sleep(3000 * time.Millisecond) - for i := 0.0; i < 10.0; i = i + 0.001 { - h.broadcast <- []byte("send com4 G1 X" + fmt.Sprintf("%.3f", i) + " F100\n") - time.Sleep(10 * time.Millisecond) - } - /* - h.broadcast <- []byte("send com4 G1 X1\n") - h.broadcast <- []byte("send com4 G1 X2\n") - h.broadcast <- []byte("send com4 G1 X3\n") - h.broadcast <- []byte("send com4 G1 X4\n") - h.broadcast <- []byte("send com4 G1 X5\n") - h.broadcast <- []byte("send com4 G1 X6\n") - h.broadcast <- []byte("send com4 G1 X7\n") - h.broadcast <- []byte("send com4 G1 X8\n") - h.broadcast <- []byte("send com4 G1 X9\n") - h.broadcast <- []byte("send com4 G1 X10\n") - h.broadcast <- []byte("send com4 G1 X1\n") - h.broadcast <- []byte("send com4 G1 X2\n") - h.broadcast <- []byte("send com4 G1 X3\n") - h.broadcast <- []byte("send com4 G1 X4\n") - h.broadcast <- []byte("send com4 G1 X5\n") - h.broadcast <- []byte("send com4 G1 X6\n") - h.broadcast <- []byte("send com4 G1 X7\n") - h.broadcast <- []byte("send com4 G1 X8\n") - h.broadcast <- []byte("send com4 G1 X9\n") - h.broadcast <- []byte("send com4 G1 X10\n") - h.broadcast <- []byte("send com4 G1 X1\n") - h.broadcast <- []byte("send com4 G1 X2\n") - h.broadcast <- []byte("send com4 G1 X3\n") - h.broadcast <- []byte("send com4 G1 X4\n") - h.broadcast <- []byte("send com4 G1 X5\n") - h.broadcast <- []byte("send com4 G1 X6\n") - h.broadcast <- []byte("send com4 G1 X7\n") - h.broadcast <- []byte("send com4 G1 X8\n") - h.broadcast <- []byte("send com4 G1 X9\n") - h.broadcast <- []byte("send com4 G1 X10\n") - */ - break - } - log.Println("dummy process exited") -} diff --git a/home.html b/home.html index cc6bb6b44..ebf93768f 100644 --- a/home.html +++ b/home.html @@ -1,6 +1,6 @@ <html> <head> -<title>Serial Port Example</title> +<title>Arduino Create Agent Debug Interface</title> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script type="text/javascript"> $(function() { diff --git a/hub.go b/hub.go index e5e5a0eef..e86b2d6c6 100755 --- a/hub.go +++ b/hub.go @@ -2,8 +2,8 @@ package main import ( "fmt" + log "github.com/Sirupsen/logrus" "github.com/kardianos/osext" - "log" //"os" "os/exec" //"path" @@ -11,6 +11,8 @@ import ( //"runtime" //"debug" "encoding/json" + "io" + "os" "runtime" "runtime/debug" "strconv" @@ -112,6 +114,11 @@ func checkCmd(m []byte) { sl := strings.ToLower(strings.Trim(s, "\n")) + if *hibernate == true { + //do nothing + return + } + if strings.HasPrefix(sl, "open") { // check if user wants to open this port as a secondary port @@ -159,25 +166,6 @@ func checkCmd(m []byte) { go spErr("You did not specify a port to close") } - } else if strings.HasPrefix(sl, "programfromurl") { - - args := strings.Split(s, " ") - if len(args) == 4 { - go spProgramFromUrl(args[1], args[2], args[3]) - } else { - go spErr("You did not specify a port, a board to program and/or a URL") - } - - } else if strings.HasPrefix(sl, "program") { - - args := strings.Split(s, " ") - if len(args) > 3 { - var slice []string = args[3:len(args)] - go spProgram(args[1], args[2], strings.Join(slice, " ")) - } else { - go spErr("You did not specify a port, a board to program and/or a filename") - } - } else if strings.HasPrefix(sl, "sendjson") { // will catch sendjson @@ -192,30 +180,27 @@ func checkCmd(m []byte) { } else if strings.HasPrefix(sl, "list") { go spList(false) go spList(true) - // log.Println("Stack info", runtime.NumGoroutine()) - // if runtime.NumGoroutine() > 30 { - // output := make([]byte, 1<<16) - // runtime.Stack(output, true) - // log.Println(output) - // log.Fatal("Crash because something is going wrong") - // } - //go getListViaWmiPnpEntity() + } else if strings.HasPrefix(sl, "downloadtool") { + args := strings.Split(s, " ") + if len(args) > 2 { + go spDownloadTool(args[1], args[2]) + } } else if strings.HasPrefix(sl, "bufferalgorithm") { go spBufferAlgorithms() + } else if strings.HasPrefix(sl, "log") { + go logAction(sl) } else if strings.HasPrefix(sl, "baudrate") { go spBaudRates() } else if strings.HasPrefix(sl, "broadcast") { go broadcast(s) } else if strings.HasPrefix(sl, "restart") { - restart() + restart("") } else if strings.HasPrefix(sl, "exit") { exit() } else if strings.HasPrefix(sl, "memstats") { memoryStats() } else if strings.HasPrefix(sl, "gc") { garbageCollection() - } else if strings.HasPrefix(sl, "bufflowdebug") { - bufflowdebug(sl) } else if strings.HasPrefix(sl, "hostname") { getHostname() } else if strings.HasPrefix(sl, "version") { @@ -227,15 +212,19 @@ func checkCmd(m []byte) { //log.Print("Done with checkCmd") } -func bufflowdebug(sl string) { - log.Println("bufflowdebug start") - if strings.HasPrefix(sl, "bufflowdebug on") { - *bufFlowDebugType = "on" - } else if strings.HasPrefix(sl, "bufflowdebug off") { - *bufFlowDebugType = "off" +var multi_writer = io.MultiWriter(&logger_ws, os.Stderr) + +func logAction(sl string) { + if strings.HasPrefix(sl, "log on") { + *logDump = "on" + log.SetOutput(multi_writer) + } else if strings.HasPrefix(sl, "log off") { + *logDump = "off" + log.SetOutput(os.Stderr) + } else if strings.HasPrefix(sl, "log show") { + // TODO: send all the saved log to websocket + //h.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *logDump + "\"}") } - h.broadcastSys <- []byte("{\"BufFlowDebug\" : \"" + *bufFlowDebugType + "\"}") - log.Println("bufflowdebug end") } func memoryStats() { @@ -273,7 +262,7 @@ func exit() { } -func restart() { +func restart(path string) { // relaunch ourself and exit // the relaunch works because we pass a cmdline in // that has serial-port-json-server only initialize 5 seconds later @@ -301,14 +290,19 @@ func restart() { // using osext exePath, err3 := osext.Executable() if err3 != nil { - fmt.Printf("Error getting exe path using osext lib. err: %v\n", err3) + log.Printf("Error getting exe path using osext lib. err: %v\n", err3) } - fmt.Printf("exePath using osext: %v\n", exePath) + if path == "" { + log.Printf("exePath using osext: %v\n", exePath) + } else { + exePath = path + } // figure out garbageCollection flag //isGcFlag := "false" var cmd *exec.Cmd + /*if *isGC { //isGcFlag = "true" cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc") @@ -316,7 +310,15 @@ func restart() { cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter) }*/ - cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc", *gcType) + + hiberString := "" + if *hibernate == true { + hiberString = "-hibernate" + } + + cmd = exec.Command(exePath, "-ls", "-addr", *addr, "-regex", *regExpFilter, "-gc", *gcType, hiberString) + + fmt.Println(cmd) //cmd := exec.Command("./serial-port-json-server", "ls") err := cmd.Start() @@ -359,5 +361,4 @@ func broadcast(arg string) { json, _ := json.Marshal(bcmd) log.Printf("bcmd:%v\n", string(json)) h.broadcastSys <- json - } diff --git a/main.go b/main.go index fca1eda94..3bbd34c9e 100755 --- a/main.go +++ b/main.go @@ -5,73 +5,59 @@ package main import ( "flag" - "fmt" - "go/build" - "log" - "os" - "path/filepath" - //"net/http/pprof" - "github.com/kardianos/osext" - //"github.com/sanbornm/go-selfupdate/selfupdate" #included in update.go to change heavily - //"github.com/sanderhahn/gozip" + log "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" "github.com/itsjamie/gin-cors" - "github.com/kardianos/service" + "github.com/kardianos/osext" "github.com/vharitonsky/iniflags" + "os" + "os/user" + "path/filepath" "runtime/debug" "text/template" "time" + //"github.com/sanbornm/go-selfupdate/selfupdate" #included in update.go to change heavily ) var ( - version = "1.83" - versionFloat = float32(1.83) - embedded_autoupdate = false + version = "x.x.x-dev" //don't modify it, Jenkins will take care + git_revision = "xxxxxxxx" //don't modify it, Jenkins will take care + embedded_autoupdate = true embedded_autoextract = false + hibernate = flag.Bool("hibernate", false, "start hibernated") addr = flag.String("addr", ":8989", "http service address") addrSSL = flag.String("addrSSL", ":8990", "https service address") - //assets = flag.String("assets", defaultAssetPath(), "path to assets") - verbose = flag.Bool("v", true, "show debug logging") + verbose = flag.Bool("v", true, "show debug logging") //verbose = flag.Bool("v", false, "show debug logging") - //homeTempl *template.Template isLaunchSelf = flag.Bool("ls", false, "launch self 5 seconds later") - - configIni = flag.String("configFile", "config.ini", "config file path") - // regular expression to sort the serial port list - // typically this wouldn't be provided, but if the user wants to clean - // up their list with a regexp so it's cleaner inside their end-user interface - // such as ChiliPeppr, this can make the massive list that Linux gives back - // to you be a bit more manageable + configIni = flag.String("configFile", "config.ini", "config file path") regExpFilter = flag.String("regex", "usb|acm|com", "Regular expression to filter serial port list") - - // allow garbageCollection() - //isGC = flag.Bool("gc", false, "Is garbage collection on? Off by default.") - //isGC = flag.Bool("gc", true, "Is garbage collection on? Off by default.") - 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)") - - // whether to do buffer flow debugging - 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") - + 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)") + logDump = flag.String("log", "off", "off = (default)") // hostname. allow user to override, otherwise we look it up - hostname = flag.String("hostname", "unknown-hostname", "Override the hostname we get from the OS") - - updateUrl = flag.String("updateUrl", "", "") - appName = flag.String("appName", "", "") + hostname = flag.String("hostname", "unknown-hostname", "Override the hostname we get from the OS") + updateUrl = flag.String("updateUrl", "", "") + appName = flag.String("appName", "", "") + globalToolsMap = make(map[string]string) + tempToolsPath = createToolsDir() ) -var globalConfigMap map[string]interface{} - type NullWriter int func (NullWriter) Write([]byte) (int, error) { return 0, nil } -func defaultAssetPath() string { - //p, err := build.Default.Import("gary.burd.info/go-websocket-chat", "", build.FindOnly) - p, err := build.Default.Import("github.com/johnlauer/serial-port-json-server", "", build.FindOnly) - if err != nil { - return "." - } - return p.Dir +type logWriter struct{} + +func (u *logWriter) Write(p []byte) (n int, err error) { + h.broadcastSys <- p + return 0, nil +} + +var logger_ws logWriter + +func createToolsDir() string { + usr, _ := user.Current() + return usr.HomeDir + "/.arduino-create" } func homeHandler(c *gin.Context) { @@ -79,86 +65,49 @@ func homeHandler(c *gin.Context) { } func launchSelfLater() { - log.Println("Going to launch myself 5 seconds later.") + log.Println("Going to launch myself 2 seconds later.") time.Sleep(2 * 1000 * time.Millisecond) - log.Println("Done waiting 5 secs. Now launching...") -} - -var logger service.Logger - -type program struct{} - -func (p *program) Start(s service.Service) error { - // Start should not block. Do the actual work async. - go p.run() - return nil -} -func (p *program) run() { - startDaemon() -} -func (p *program) Stop(s service.Service) error { - // Stop should not block. Return with a few seconds. - <-time.After(time.Second * 13) - return nil + log.Println("Done waiting 2 secs. Now launching...") } func main() { - svcConfig := &service.Config{ - Name: "ArduinoCreateAgent", - DisplayName: "Arduino Create Agent", - Description: "A bridge that allows Arduino Create to operate on the boards connected to the computer", - } - - prg := &program{} - s, err := service.New(prg, svcConfig) - if err != nil { - log.Fatal(err) - } - if len(os.Args) > 1 { - err = service.Control(s, os.Args[1]) - if err != nil { - log.Fatal(err) - } - return - } - - logger, err = s.Logger(nil) - if err != nil { - log.Fatal(err) - } - - err = s.Run() - if err != nil { - logger.Error(err) - } -} -func startDaemon() { + flag.Parse() go func() { // autoextract self src, _ := osext.Executable() dest := filepath.Dir(src) + os.Mkdir(tempToolsPath, 0777) + hideFile(tempToolsPath) + if embedded_autoextract { // save the config.ini (if it exists) if _, err := os.Stat(dest + "/" + *configIni); os.IsNotExist(err) { - fmt.Println("First run, unzipping self") + log.Println("First run, unzipping self") err := Unzip(src, dest) - fmt.Println("Self extraction, err:", err) + log.Println("Self extraction, err:", err) } if _, err := os.Stat(dest + "/" + *configIni); os.IsNotExist(err) { flag.Parse() - fmt.Println("No config.ini at", *configIni) + log.Println("No config.ini at", *configIni) } else { + flag.Parse() flag.Set("config", dest+"/"+*configIni) iniflags.Parse() } + } else { + flag.Set("config", dest+"/"+*configIni) + iniflags.Parse() } - // setup logging - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + //log.SetFormatter(&log.JSONFormatter{}) + + log.SetLevel(log.InfoLevel) + + log.SetOutput(os.Stderr) // see if we are supposed to wait 5 seconds if *isLaunchSelf { @@ -179,16 +128,8 @@ func startDaemon() { if updater != nil { go updater.BackgroundRun() } - - // data, err := Asset("arduino.zip") - // if err != nil { - // log.Println("arduino tools not found") - // } } - createGlobalConfigMap(&globalConfigMap) - - //getList() f := flag.Lookup("addr") log.Println("Version:" + version) @@ -213,10 +154,8 @@ func startDaemon() { ip := "0.0.0.0" log.Print("Starting server and websocket on " + ip + "" + f.Value.String()) - //homeTempl = template.Must(template.ParseFiles(filepath.Join(*assets, "home.html"))) - log.Println("The Serial Port JSON Server is now running.") - log.Println("If you are using ChiliPeppr, you may go back to it and connect to this server.") + log.Println("The Arduino Create Agent is now running") // see if they provided a regex filter if len(*regExpFilter) > 0 { @@ -225,10 +164,6 @@ func startDaemon() { // list serial ports portList, _ := GetList(false) - /*if errSys != nil { - log.Printf("Got system error trying to retrieve serial port list. Err:%v\n", errSys) - log.Fatal("Exiting") - }*/ log.Println("Your serial ports:") if len(portList) == 0 { log.Println("\tThere are no serial ports to list.") @@ -274,13 +209,13 @@ func startDaemon() { r.Handle("WSS", "/socket.io/", socketHandler) go func() { if err := r.RunTLS(*addrSSL, filepath.Join(dest, "cert.pem"), filepath.Join(dest, "key.pem")); err != nil { - fmt.Printf("Error trying to bind to port: %v, so exiting...", err) + log.Printf("Error trying to bind to port: %v, so exiting...", err) log.Fatal("Error ListenAndServe:", err) } }() if err := r.Run(*addr); err != nil { - fmt.Printf("Error trying to bind to port: %v, so exiting...", err) + log.Printf("Error trying to bind to port: %v, so exiting...", err) log.Fatal("Error ListenAndServe:", err) } }() @@ -303,18 +238,23 @@ const homeTemplateHtml = `<!DOCTYPE html> var socket; var msg = $("#msg"); var log = document.getElementById('log'); + var pause = document.getElementById('myCheck'); var messages = []; + var only_log = true; function appendLog(msg) { - messages.push(msg); - if (messages.length > 100) { - messages.shift(); - } - var doScroll = log.scrollTop == log.scrollHeight - log.clientHeight; - log.innerHTML = messages.join("<br>"); - if (doScroll) { - log.scrollTop = log.scrollHeight - log.clientHeight; - } + + if (!pause.checked && (only_log == false || (!(msg.indexOf("{") == 0) && !(msg.indexOf("list") == 0) && only_log == true))) { + messages.push(msg); + if (messages.length > 100) { + messages.shift(); + } + var doScroll = log.scrollTop == log.scrollHeight - log.clientHeight; + log.innerHTML = messages.join("<br>"); + if (doScroll) { + log.scrollTop = log.scrollHeight - log.clientHeight; + } + } } $("#form").submit(function() { @@ -325,6 +265,8 @@ const homeTemplateHtml = `<!DOCTYPE html> return false; } socket.emit("command", msg.val()); + if (msg.val().indexOf("log on") != -1) {only_log = true;} + if (msg.val().indexOf("log off") != -1) {only_log = false;} msg.val(""); return false }); @@ -389,6 +331,7 @@ body { <form id="form"> <input type="submit" value="Send" /> <input type="text" id="msg" size="64"/> + <input name="pause" type="checkbox" value="pause" id="myCheck"/> Pause <br> </form> </body> </html> diff --git a/programmer.go b/programmer.go index f0a073d68..521b48153 100644 --- a/programmer.go +++ b/programmer.go @@ -5,47 +5,42 @@ import ( "bytes" "encoding/json" "fmt" + log "github.com/Sirupsen/logrus" "github.com/facchinm/go-serial" "github.com/kardianos/osext" + "github.com/mattn/go-shellwords" "io" - "log" "mime/multipart" "net/http" "os" "os/exec" "path/filepath" - "strconv" + "regexp" + "runtime" "strings" "time" ) var compiling = false -// Download the file from URL first, store in tmp folder, then pass to spProgram -func spProgramFromUrl(portname string, boardname string, url string) { - mapB, _ := json.Marshal(map[string]string{"ProgrammerStatus": "DownloadStart", "Url": url}) - h.broadcastSys <- mapB - filename, err := downloadFromUrl(url) - mapB, _ = json.Marshal(map[string]string{"ProgrammerStatus": "DownloadDone", "Filename": filename, "Url": url}) - h.broadcastSys <- mapB - - if err != nil { - spErr(err.Error()) - return - } else { - spProgram(portname, boardname, filename) - } - - // delete file - -} - func colonToUnderscore(input string) string { output := strings.Replace(input, ":", "_", -1) return output } -func spProgramNetwork(portname string, boardname string, filePath string) error { +type basicAuthData struct { + UserName string + Password string +} + +type boardExtraInfo struct { + use_1200bps_touch bool + wait_for_upload_port bool + networkPort bool + authdata basicAuthData +} + +func spProgramNetwork(portname string, boardname string, filePath string, authdata basicAuthData) error { log.Println("Starting network upload") log.Println("Board Name: " + boardname) @@ -91,7 +86,9 @@ func spProgramNetwork(portname string, boardname string, filePath string) error } // Don't forget to set the content type, this will contain the boundary. req.Header.Set("Content-Type", w.FormDataContentType()) - req.SetBasicAuth("root", "arduino") + if authdata.UserName != "" { + req.SetBasicAuth(authdata.UserName, authdata.Password) + } //h.broadcastSys <- []byte("Start flashing with command " + cmdString) log.Printf("Network flashing on " + portname) @@ -109,6 +106,7 @@ func spProgramNetwork(portname string, boardname string, filePath string) error // Check the response if res.StatusCode != http.StatusOK { + log.Errorf("bad status: %s", res.Status) err = fmt.Errorf("bad status: %s", res.Status) } @@ -127,29 +125,43 @@ func spProgramNetwork(portname string, boardname string, filePath string) error return err } -func spProgramLocal(portname string, boardname string, filePath string) { - isFound, flasher, mycmd := assembleCompilerCommand(boardname, portname, filePath) - mapD := map[string]string{"ProgrammerStatus": "CommandReady", "IsFound": strconv.FormatBool(isFound), "Flasher": flasher, "Cmd": strings.Join(mycmd, " ")} - mapB, _ := json.Marshal(mapD) - h.broadcastSys <- mapB +func spProgramLocal(portname string, boardname string, filePath string, commandline string, extraInfo boardExtraInfo) { - if isFound { - spHandlerProgram(flasher, mycmd) - } else { - spErr("Could not find the board " + boardname + " that you were trying to program.") - mapD := map[string]string{"ProgrammerStatus": "Failed", "IsFound": strconv.FormatBool(isFound), "Flasher": flasher, "Cmd": strings.Join(mycmd, " "), "Err": "Could not find the board " + boardname + " that you were trying to program."} - mapB, _ := json.Marshal(mapD) - h.broadcastSys <- mapB + var err error + if extraInfo.use_1200bps_touch { + portname, err = touch_port_1200bps(portname, extraInfo.wait_for_upload_port) + } + + if err != nil { + log.Println("Could not touch the port") return } + + commandline = strings.Replace(commandline, "{build.path}", filepath.Dir(filePath), 1) + commandline = strings.Replace(commandline, "{build.project_name}", strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filepath.Base(filePath))), 1) + commandline = strings.Replace(commandline, "{serial.port}", portname, 1) + commandline = strings.Replace(commandline, "{serial.port.file}", filepath.Base(portname), 1) + + // search for runtime variables and replace with values from globalToolsMap + var runtimeRe = regexp.MustCompile("\\{(.*?)\\}") + runtimeVars := runtimeRe.FindAllString(commandline, -1) + + fmt.Println(runtimeVars) + + for _, element := range runtimeVars { + commandline = strings.Replace(commandline, element, globalToolsMap[element], 1) + } + + z, _ := shellwords.Parse(commandline) + spHandlerProgram(z[0], z[1:]) } -func spProgram(portname string, boardname string, filePath string) { +func spProgram(portname string, boardname string, filePath string, commandline string, extraInfo boardExtraInfo) { - spProgramRW(portname, boardname, "", filePath) + spProgramRW(portname, boardname, "", filePath, commandline, extraInfo) } -func spProgramRW(portname string, boardname string, boardname_rewrite string, filePath string) { +func spProgramRW(portname string, boardname string, boardname_rewrite string, filePath string, commandline string, extraInfo boardExtraInfo) { compiling = true defer func() { @@ -157,23 +169,13 @@ func spProgramRW(portname string, boardname string, boardname_rewrite string, fi compiling = false }() - // check if the port is physical or network - var networkPort bool - myport, exist := findPortByNameRerun(portname, false) - if !exist { - // it could be a network port that has not been found at the second lap - networkPort = true - } else { - networkPort = myport.NetworkPort - } - var err error - if networkPort { + if extraInfo.networkPort { if boardname_rewrite == "" { - err = spProgramNetwork(portname, boardname_rewrite, filePath) + err = spProgramNetwork(portname, boardname_rewrite, filePath, extraInfo.authdata) } else { - err = spProgramNetwork(portname, boardname, filePath) + err = spProgramNetwork(portname, boardname, filePath, extraInfo.authdata) } if err != nil { mapD := map[string]string{"ProgrammerStatus": "Error " + err.Error(), "Msg": "Could not program the board", "Output": "", "Err": err.Error()} @@ -181,7 +183,7 @@ func spProgramRW(portname string, boardname string, boardname_rewrite string, fi h.broadcastSys <- mapB } } else { - spProgramLocal(portname, boardname, filePath) + spProgramLocal(portname, boardname, filePath, commandline, extraInfo) } } @@ -194,8 +196,32 @@ func spHandlerProgram(flasher string, cmdString []string) { // cmdString = append([]string{flasher}, cmdString...) // oscmd = exec.Command(sh, cmdString...) // } else { - oscmd = exec.Command(flasher, cmdString...) - // } + + // remove quotes form flasher command and cmdString + flasher = strings.Replace(flasher, "\"", "", -1) + + for index, _ := range cmdString { + cmdString[index] = strings.Replace(cmdString[index], "\"", "", -1) + } + + extension := "" + if runtime.GOOS == "windows" { + extension = ".exe" + } + + oscmd = exec.Command(flasher+extension, cmdString...) + + stdout, err := oscmd.StdoutPipe() + if err != nil { + return + } + + stderr, err := oscmd.StderrPipe() + if err != nil { + return + } + + multi := io.MultiReader(stderr, stdout) // Stdout buffer //var cmdOutput []byte @@ -206,16 +232,29 @@ func spHandlerProgram(flasher string, cmdString []string) { mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB - cmdOutput, err := oscmd.CombinedOutput() + err = oscmd.Start() + + in := bufio.NewScanner(multi) + + in.Split(bufio.ScanLines) + + for in.Scan() { + log.Info(in.Text()) + mapD := map[string]string{"ProgrammerStatus": "Busy", "Msg": in.Text()} + mapB, _ := json.Marshal(mapD) + h.broadcastSys <- mapB + } + + err = oscmd.Wait() if err != nil { - log.Printf("Command finished with error: %v "+string(cmdOutput), err) - mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board", "Output": string(cmdOutput), "Err": string(cmdOutput)} + log.Printf("Command finished with error: %v", err) + mapD := map[string]string{"ProgrammerStatus": "Error", "Msg": "Could not program the board"} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB } else { - log.Printf("Finished without error. Good stuff. stdout: " + string(cmdOutput)) - mapD := map[string]string{"ProgrammerStatus": "Done", "Flash": "Ok", "Output": string(cmdOutput)} + log.Printf("Finished without error. Good stuff") + mapD := map[string]string{"ProgrammerStatus": "Done", "Flash": "Ok"} mapB, _ := json.Marshal(mapD) h.broadcastSys <- mapB // analyze stdin @@ -275,6 +314,56 @@ func findNewPortName(slice1 []string, slice2 []string) string { return "" } +func touch_port_1200bps(portname string, wait_for_upload_port bool) (string, error) { + initialPortName := portname + log.Println("Restarting in bootloader mode") + + mode := &serial.Mode{ + BaudRate: 1200, + Vmin: 1, + Vtimeout: 0, + } + port, err := serial.OpenPort(portname, mode) + if err != nil { + log.Println(err) + return "", err + } + //port.SetDTR(false) + port.Close() + time.Sleep(time.Second / 2.0) + + timeout := false + go func() { + time.Sleep(2 * time.Second) + timeout = true + }() + + // time.Sleep(time.Second / 4) + // wait for port to reappear + if wait_for_upload_port { + after_reset_ports, _ := serial.GetPortsList() + log.Println(after_reset_ports) + var ports []string + for { + ports, _ = serial.GetPortsList() + log.Println(ports) + time.Sleep(time.Millisecond * 200) + portname = findNewPortName(ports, after_reset_ports) + if portname != "" { + break + } + if timeout { + break + } + } + } + + if portname == "" { + portname = initialPortName + } + return portname, nil +} + func assembleCompilerCommand(boardname string, portname string, filePath string) (bool, string, []string) { // get executable (self)path and use it as base for all other paths diff --git a/serial.go b/serial.go index 6fb94fd4f..c111ee016 100755 --- a/serial.go +++ b/serial.go @@ -3,17 +3,9 @@ package main import ( - //"bufio" "encoding/json" "fmt" - //"path/filepath" - //"github.com/kballard/go-shellquote" - //"github.com/johnlauer/goserial" - //"github.com/mikepb/go-serial" - //"github.com/facchinm/go-serial" - //"github.com/kardianos/osext" - "log" - //"os" + log "github.com/Sirupsen/logrus" "regexp" "runtime/debug" "strconv" @@ -90,18 +82,17 @@ type SpPortList struct { } type SpPortItem struct { - Name string - Friendly string - SerialNumber string - DeviceClass string - IsOpen bool - IsPrimary bool - RelatedNames []string - Baud int - BufferAlgorithm string - AvailableBufferAlgorithms []string - Ver float32 - NetworkPort bool + Name string + SerialNumber string + DeviceClass string + IsOpen bool + IsPrimary bool + Baud int + BufferAlgorithm string + Ver string + NetworkPort bool + VendorID string + ProductID string } // SerialPorts contains the ports attached to the machine @@ -513,21 +504,20 @@ func spListDual(network bool) { DtrOn bool BufferAlgorithm string AvailableBufferAlgorithms []string - Ver float32 + Ver string */ spl.Ports[ctr] = SpPortItem{ - Name: item.Name, - Friendly: item.FriendlyName, - SerialNumber: item.SerialNumber, - DeviceClass: item.DeviceClass, - IsOpen: false, - IsPrimary: false, - RelatedNames: item.RelatedNames, - Baud: 0, - BufferAlgorithm: "", - AvailableBufferAlgorithms: availableBufferAlgorithms, - Ver: versionFloat, - NetworkPort: item.NetworkPort, + Name: item.Name, + SerialNumber: item.SerialNumber, + DeviceClass: item.DeviceClass, + IsOpen: false, + IsPrimary: false, + Baud: 0, + BufferAlgorithm: "", + Ver: version, + NetworkPort: item.NetworkPort, + VendorID: item.IdVendor, + ProductID: item.IdProduct, } // figure out if port is open @@ -552,18 +542,6 @@ func spListDual(network bool) { } } -func spListOld() { - ls := "{\"serialports\" : [\n" - list, _ := getList() - for _, item := range list { - ls += "{ \"name\" : \"" + item.Name + "\", \"friendly\" : \"" + item.FriendlyName + "\" },\n" - } - ls = strings.TrimSuffix(ls, "},\n") - ls += "}\n" - ls += "]}\n" - h.broadcastSys <- []byte(ls) -} - func spErr(err string) { log.Println("Sending err back: ", err) //h.broadcastSys <- []byte(err) diff --git a/seriallist.go b/seriallist.go index c229a69b1..a0ab17ef9 100755 --- a/seriallist.go +++ b/seriallist.go @@ -3,16 +3,13 @@ package main import ( + log "github.com/Sirupsen/logrus" "github.com/facchinm/go-serial" - "log" - //"os" "regexp" ) type OsSerialPort struct { Name string - FriendlyName string - RelatedNames []string // for some devices there are 2 or more ports, i.e. TinyG v9 has 2 serial ports SerialNumber string DeviceClass string Manufacturer string @@ -36,7 +33,7 @@ func GetList(network bool) ([]OsSerialPort, error) { arrPorts := []OsSerialPort{} for _, element := range ports { - arrPorts = append(arrPorts, OsSerialPort{Name: element, FriendlyName: element}) + arrPorts = append(arrPorts, OsSerialPort{Name: element}) } // see if we should filter the list @@ -49,17 +46,15 @@ func GetList(network bool) ([]OsSerialPort, error) { // if matches regex, include if reFilter.MatchString(element.Name) { newarrPorts = append(newarrPorts, element) - } else if reFilter.MatchString(element.FriendlyName) { - newarrPorts = append(newarrPorts, element) } else { - log.Printf("serial port did not match. port: %v\n", element) + log.Debug("serial port did not match. port: %v\n", element) } } arrPorts = newarrPorts } - arrPorts = removeNonArduinoBoards(arrPorts) + arrPorts = associateVidPidWithPort(arrPorts) return arrPorts, err //log.Printf("Done doing GetList(). arrPorts:%v\n", arrPorts) } diff --git a/seriallist_darwin.go b/seriallist_darwin.go index a85164da1..f587d4d03 100644 --- a/seriallist_darwin.go +++ b/seriallist_darwin.go @@ -1,18 +1,8 @@ package main import ( - //"fmt" - //"github.com/tarm/goserial" - "log" - "os" - "strings" - //"encoding/binary" - //"strconv" - //"syscall" - //"fmt" - //"bufio" - "io/ioutil" "os/exec" + "strings" ) // execute system_profiler SPUSBDataType | grep "Vendor ID: 0x2341" -A5 -B2 @@ -23,96 +13,49 @@ import ( // search all board.txt files for map[Product ID] // assign it to name -func removeNonArduinoBoards(ports []OsSerialPort) []OsSerialPort { - usbcmd := exec.Command("system_profiler", "SPUSBDataType") - grepcmd := exec.Command("grep", "0x2341", "-A5", "-B1") +func associateVidPidWithPort(ports []OsSerialPort) []OsSerialPort { + + // prefilter ports + ports = Filter(ports, func(port OsSerialPort) bool { + return !strings.Contains(port.Name, "Blue") && !strings.Contains(port.Name, "/cu") + }) + + for index, _ := range ports { + port_hash := strings.Trim(ports[index].Name, "/dev/tty.usbmodem") + port_hash = strings.Trim(port_hash, "/dev/tty.usbserial-") - cmdOutput, _ := pipe_commands(usbcmd, grepcmd) + usbcmd := exec.Command("system_profiler", "SPUSBDataType") + grepcmd := exec.Command("grep", "Location ID: 0x"+port_hash[:len(port_hash)-1], "-B6") + cmdOutput, _ := pipe_commands(usbcmd, grepcmd) - //log.Println(string(cmdOutput)) - cmdOutSlice := strings.Split(string(cmdOutput), "\n") + if len(cmdOutput) == 0 { + usbcmd = exec.Command("system_profiler", "SPUSBDataType") + grepcmd = exec.Command("grep" /*"Serial Number: "+*/, strings.Trim(port_hash, "0"), "-B3", "-A3") + cmdOutput, _ = pipe_commands(usbcmd, grepcmd) + } - // how many lines is the output? boards attached = lines/8 - for i := 0; i < len(cmdOutSlice)/8; i++ { + if len(cmdOutput) == 0 { + //give up + continue + } - cmdOutSliceN := cmdOutSlice[i*8 : (i+1)*8] + cmdOutSlice := strings.Split(string(cmdOutput), "\n") cmdOutMap := make(map[string]string) - for _, element := range cmdOutSliceN { - if strings.Contains(element, "ID") { + for _, element := range cmdOutSlice { + if strings.Contains(element, "ID") || strings.Contains(element, "Manufacturer") { element = strings.TrimSpace(element) arr := strings.Split(element, ": ") cmdOutMap[arr[0]] = arr[1] } } - - archBoardName, boardName, _ := getBoardName(cmdOutMap["Product ID"]) - - // remove initial 0x and final zeros - ttyHeader := strings.Trim((cmdOutMap["Location ID"]), "0x") - ttyHeader = strings.Split(ttyHeader, " ")[0] - ttyHeader = strings.Trim(ttyHeader, "0") - - for _, port := range ports { - if strings.Contains(port.Name, ttyHeader) { - if !strings.Contains(port.Name, "/cu") { - port.RelatedNames = append(port.RelatedNames, archBoardName) - port.FriendlyName = strings.Trim(boardName, "\n") - } - } - } + ports[index].IdProduct = strings.Split(cmdOutMap["Product ID"], " ")[0] + ports[index].IdVendor = strings.Split(cmdOutMap["Vendor ID"], " ")[0] + ports[index].Manufacturer = cmdOutMap["Manufacturer"] } - - // additional remove phase - ports = Filter(ports, func(port OsSerialPort) bool { - return !strings.Contains(port.Name, "Blue") && !strings.Contains(port.Name, "/cu") - }) - return ports } -func getList() ([]OsSerialPort, os.SyscallError) { - //return getListViaWmiPnpEntity() - return getListViaTtyList() -} - -func getListViaTtyList() ([]OsSerialPort, os.SyscallError) { - var err os.SyscallError - - log.Println("getting serial list on darwin") - - // make buffer of 100 max serial ports - // return a slice - list := make([]OsSerialPort, 100) - - files, _ := ioutil.ReadDir("/dev/") - ctr := 0 - for _, f := range files { - if strings.HasPrefix(f.Name(), "tty.") { - // it is a legitimate serial port - list[ctr].Name = "/dev/" + f.Name() - list[ctr].FriendlyName = f.Name() - log.Println("Added serial port to list: ", list[ctr]) - ctr++ - } - // stop-gap in case going beyond 100 (which should never happen) - // i mean, really, who has more than 100 serial ports? - if ctr > 99 { - ctr = 99 - } - //fmt.Println(f.Name()) - //fmt.Println(f.) - } - /* - list := make([]OsSerialPort, 3) - list[0].Name = "tty.serial1" - list[0].FriendlyName = "tty.serial1" - list[1].Name = "tty.serial2" - list[1].FriendlyName = "tty.serial2" - list[2].Name = "tty.Bluetooth-Modem" - list[2].FriendlyName = "tty.Bluetooth-Modem" - */ - - return list[0:ctr], err +func hideFile(path string) { } diff --git a/seriallist_linux.go b/seriallist_linux.go index d389803d4..c75db591a 100755 --- a/seriallist_linux.go +++ b/seriallist_linux.go @@ -1,459 +1,37 @@ package main import ( - //"fmt" - //"github.com/tarm/goserial" - //"log" - "os" + "fmt" "os/exec" - "strings" - //"encoding/binary" - //"strconv" - //"syscall" - //"fmt" - //"io" - "bytes" - "io/ioutil" - "log" "path/filepath" - "regexp" - "sort" + "strconv" + "strings" ) -func removeNonArduinoBoards(ports []OsSerialPort) []OsSerialPort { - usbcmd := exec.Command("lsusb", "-vvv") - grepcmd := exec.Command("grep", "0x2341", "-A1") - grep2cmd := exec.Command("grep", "idProduct") - //awkcmd := exec.Command("awk", "\'{print $2}\'") - //awkcmd := exec.Command("grep", "-E", "-o", "'0x[[:alnum:]]{4}'") - - cmdOutput, _ := pipe_commands(usbcmd, grepcmd, grep2cmd) - - cmdOutSliceT := strings.Split(string(cmdOutput), "\n") - - re := regexp.MustCompile("0x[[:alnum:]]{4}") - - var cmdOutSlice []string - - for _, element := range cmdOutSliceT { - cmdOutSlice = append(cmdOutSlice, re.FindString(element)) - } - - for _, element := range cmdOutSlice { - - if element == "" { - break - } - - archBoardName, boardName, _ := getBoardName(element) - - for _, port := range ports { - ueventcmd := exec.Command("cat", "/sys/class/tty/"+filepath.Base(port.Name)+"/device/uevent") - grep3cmd := exec.Command("grep", "PRODUCT=") - cutcmd := exec.Command("cut", "-f2", "-d/") - - cmdOutput2, _ := pipe_commands(ueventcmd, grep3cmd, cutcmd) - cmdOutput2S := string(cmdOutput2) - - if strings.Contains(element, strings.Trim(cmdOutput2S, "\n")) && cmdOutput2S != "" { - port.RelatedNames = append(port.RelatedNames, archBoardName) - port.FriendlyName = strings.Trim(boardName, "\n") - } - } - } - - return ports -} - -func getList() ([]OsSerialPort, os.SyscallError) { - - //return getListViaTtyList() - return getAllPortsViaManufacturer() -} - -func getListViaTtyList() ([]OsSerialPort, os.SyscallError) { - var err os.SyscallError - - //log.Println("getting serial list on darwin") - - // make buffer of 1000 max serial ports - // return a slice - list := make([]OsSerialPort, 1000) - - files, _ := ioutil.ReadDir("/dev/") - ctr := 0 - for _, f := range files { - if strings.HasPrefix(f.Name(), "tty") { - // it is a legitimate serial port - list[ctr].Name = "/dev/" + f.Name() - list[ctr].FriendlyName = f.Name() - - // see if we can get a better friendly name - //friendly, ferr := getMetaDataForPort(f.Name()) - //if ferr == nil { - // list[ctr].FriendlyName = friendly - //} - - //log.Println("Added serial port to list: ", list[ctr]) - ctr++ - } - // stop-gap in case going beyond 1000 (which should never happen) - // i mean, really, who has more than 1000 serial ports? - if ctr > 999 { - ctr = 999 - } - //fmt.Println(f.Name()) - //fmt.Println(f.) - } - /* - list := make([]OsSerialPort, 3) - list[0].Name = "tty.serial1" - list[0].FriendlyName = "tty.serial1" - list[1].Name = "tty.serial2" - list[1].FriendlyName = "tty.serial2" - list[2].Name = "tty.Bluetooth-Modem" - list[2].FriendlyName = "tty.Bluetooth-Modem" - */ - - return list[0:ctr], err -} - -type deviceClass struct { - BaseClass int - Description string -} - -func getDeviceClassList() { - // TODO: take list from http://www.usb.org/developers/defined_class - // and create mapping. -} - -func getAllPortsViaManufacturer() ([]OsSerialPort, os.SyscallError) { - var err os.SyscallError - var list []OsSerialPort - - // LOOK FOR THE WORD MANUFACTURER - // search /sys folder - oscmd := exec.Command("find", "/sys/", "-name", "manufacturer", "-print") //, "2>", "/dev/null") - // Stdout buffer - cmdOutput := &bytes.Buffer{} - // Attach buffer to command - oscmd.Stdout = cmdOutput - - errstart := oscmd.Start() - if errstart != nil { - log.Printf("Got error running find cmd. Maybe they don't have it installed? %v:", errstart) - return nil, err - } - //log.Printf("Waiting for command to finish... %v", oscmd) - - errwait := oscmd.Wait() - - if errwait != nil { - log.Printf("Command finished with error: %v", errwait) - return nil, err - } - - //log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) - - // analyze stdout - // we should be able to split on newline to each file - files := strings.Split(string(cmdOutput.Bytes()), "\n") - /*if len(files) == 0 { - return nil, err - }*/ - - // LOOK FOR THE WORD PRODUCT - oscmd2 := exec.Command("find", "/sys/", "-name", "product", "-print") //, "2>", "/dev/null") - cmdOutput2 := &bytes.Buffer{} - oscmd2.Stdout = cmdOutput2 - - oscmd2.Start() - oscmd2.Wait() - - filesFromProduct := strings.Split(string(cmdOutput2.Bytes()), "\n") - - // append both arrays so we have one (then we'll have to de-dupe) - files = append(files, filesFromProduct...) - - // Now get directories from each file - re := regexp.MustCompile("/(manufacturer|product)$") - var mapfile map[string]int - mapfile = make(map[string]int) - for _, element := range files { - // make this directory be a key so it's unique. increment int so we know - // for debug how many times this directory appeared - mapfile[re.ReplaceAllString(element, "")]++ - } - - // sort the directory keys - mapfilekeys := make([]string, len(mapfile)) - i := 0 - for key, _ := range mapfile { - mapfilekeys[i] = key - i++ - } - sort.Strings(mapfilekeys) - - //reRemoveManuf, _ := regexp.Compile("/manufacturer$") - reNewLine, _ := regexp.Compile("\n") - - // loop on unique directories - for _, directory := range mapfilekeys { - - if len(directory) == 0 { - continue - } - - // for each manufacturer or product file, we need to read the val from the file - // but more importantly find the tty ports for this directory - - // for example, for the TinyG v9 which creates 2 ports, the cmd: - // find /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/ -name tty[AU]* -print - // will result in: - /* - /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3:1.0/tty/ttyACM0 - /sys/devices/platform/bcm2708_usb/usb1/1-1/1-1.3/1-1.3:1.2/tty/ttyACM1 - */ +func associateVidPidWithPort(ports []OsSerialPort) []OsSerialPort { - // figure out the directory - //directory := reRemoveManuf.ReplaceAllString(element, "") + for index, _ := range ports { + ueventcmd := exec.Command("cat", "/sys/class/tty/"+filepath.Base(ports[index].Name)+"/device/uevent") + grep3cmd := exec.Command("grep", "PRODUCT=") - // read the device class so we can remove stuff we don't want like hubs - deviceClassBytes, errRead4 := ioutil.ReadFile(directory + "/bDeviceClass") - deviceClass := "" - if errRead4 != nil { - // there must be a permission issue - //log.Printf("Problem reading in serial number text file. Permissions maybe? err:%v", errRead3) - //return nil, err - } - deviceClass = string(deviceClassBytes) - deviceClass = reNewLine.ReplaceAllString(deviceClass, "") + cmdOutput2, _ := pipe_commands(ueventcmd, grep3cmd) + cmdOutput2S := string(cmdOutput2) - if deviceClass == "09" || deviceClass == "9" || deviceClass == "09h" { - log.Printf("This is a hub, so skipping. %v", directory) + if len(cmdOutput2S) == 0 { continue } - // read the manufacturer - manufBytes, errRead := ioutil.ReadFile(directory + "/manufacturer") - manuf := "" - if errRead != nil { - // the file could possibly just not exist, which is normal - log.Printf("Problem reading in manufacturer text file. Permissions maybe? err:%v", errRead) - //return nil, err - //continue - } - manuf = string(manufBytes) - manuf = reNewLine.ReplaceAllString(manuf, "") - - // read the product - productBytes, errRead2 := ioutil.ReadFile(directory + "/product") - product := "" - if errRead2 != nil { - // the file could possibly just not exist, which is normal - //log.Printf("Problem reading in product text file. Permissions maybe? err:%v", errRead2) - //return nil, err - } - product = string(productBytes) - product = reNewLine.ReplaceAllString(product, "") - - // read the serial number - serialNumBytes, errRead3 := ioutil.ReadFile(directory + "/serial") - serialNum := "" - if errRead3 != nil { - // the file could possibly just not exist, which is normal - //log.Printf("Problem reading in serial number text file. Permissions maybe? err:%v", errRead3) - //return nil, err - } - serialNum = string(serialNumBytes) - serialNum = reNewLine.ReplaceAllString(serialNum, "") - - // read idvendor - idVendorBytes, _ := ioutil.ReadFile(directory + "/idVendor") - idVendor := "" - idVendor = reNewLine.ReplaceAllString(string(idVendorBytes), "") - - // read idProduct - idProductBytes, _ := ioutil.ReadFile(directory + "/idProduct") - idProduct := "" - idProduct = reNewLine.ReplaceAllString(string(idProductBytes), "") - - log.Printf("%v : %v (%v) DevClass:%v", manuf, product, serialNum, deviceClass) - - // search folder that had manufacturer file in it - log.Printf("\tDirectory searching: %v", directory) - - // -name tty[AU]* -print - oscmd = exec.Command("find", directory, "-name", "tty[AU]*", "-print") - - // Stdout buffer - cmdOutput = &bytes.Buffer{} - // Attach buffer to command - oscmd.Stdout = cmdOutput - - errstart = oscmd.Start() - if errstart != nil { - log.Printf("Got error running find cmd. Maybe they don't have it installed? %v:", errstart) - //return nil, err - continue - } - //log.Printf("Waiting for command to finish... %v", oscmd) - - errwait = oscmd.Wait() - - if errwait != nil { - log.Printf("Command finished with error: %v", errwait) - //return nil, err - continue - } - - //log.Printf("Finished searching manuf directory without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) - //log.Printf(" \n") - - // we should be able to split on newline to each file - filesTty := strings.Split(string(cmdOutput.Bytes()), "\n") - - // generate a unique list of tty ports below - //var ttyPorts []string - var m map[string]int - m = make(map[string]int) - for _, fileTty := range filesTty { - if len(fileTty) == 0 { - continue - } - log.Printf("\t%v", fileTty) - ttyPort := regexp.MustCompile("^.*/").ReplaceAllString(fileTty, "") - ttyPort = reNewLine.ReplaceAllString(ttyPort, "") - m[ttyPort]++ - //ttyPorts = append(ttyPorts, ttyPort) - } - log.Printf("\tlist of ports on this. map:%v\n", m) - log.Printf("\t.") - //sort.Strings(ttyPorts) - - // create order array of ttyPorts so they're in order when - // we send back via json. this makes for more human friendly reading - // cuz anytime you do a hash map you can get out of order - ttyPorts := []string{} - for key, _ := range m { - ttyPorts = append(ttyPorts, key) - } - sort.Strings(ttyPorts) - - // we now have a very nice list of ttyports for this device. many are just 1 port - // however, for some advanced devices there are 2 or more ports associated and - // we have this data correct now, so build out the final OsSerialPort list - for _, key := range ttyPorts { - listitem := OsSerialPort{ - Name: "/dev/" + key, - FriendlyName: manuf, // + " " + product, - SerialNumber: serialNum, - DeviceClass: deviceClass, - Manufacturer: manuf, - Product: product, - IdVendor: idVendor, - IdProduct: idProduct, - } - if len(product) > 0 { - listitem.FriendlyName += " " + product - } - listitem.FriendlyName += " (" + key + ")" - listitem.FriendlyName = friendlyNameCleanup(listitem.FriendlyName) + infos := strings.Split(cmdOutput2S, "=") - // append related tty ports - for _, keyRelated := range ttyPorts { - if key == keyRelated { - continue - } - listitem.RelatedNames = append(listitem.RelatedNames, "/dev/"+keyRelated) - } - list = append(list, listitem) - } + vid_pid := strings.Split(infos[1], "/") + vid, _ := strconv.ParseInt(vid_pid[0], 16, 32) + pid, _ := strconv.ParseInt(vid_pid[1], 16, 32) + ports[index].IdVendor = fmt.Sprintf("0x%04x", vid) + ports[index].IdProduct = fmt.Sprintf("0x%04x", pid) } - - // sort ports by item.Name - sort.Sort(ByName(list)) - - log.Printf("Final port list: %v", list) - return list, err -} - -// ByAge implements sort.Interface for []Person based on -// the Age field. -type ByName []OsSerialPort - -func (a ByName) Len() int { return len(a) } -func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } - -func friendlyNameCleanup(fnin string) (fnout string) { - // This is an industry intelligence method to just cleanup common names - // out there so we don't get ugly friendly names back - fnout = regexp.MustCompile("\\(www.arduino.cc\\)").ReplaceAllString(fnin, "") - fnout = regexp.MustCompile("Arduino\\s+Arduino").ReplaceAllString(fnout, "Arduino") - fnout = regexp.MustCompile("\\s+").ReplaceAllString(fnout, " ") // multi space to single space - fnout = regexp.MustCompile("^\\s+|\\s+$").ReplaceAllString(fnout, "") // trim - return fnout -} - -func getMetaDataForPort(port string) (string, error) { - // search the folder structure on linux for this port name - - // search /sys folder - oscmd := exec.Command("find", "/sys/devices", "-name", port, "-print") //, "2>", "/dev/null") - - // Stdout buffer - cmdOutput := &bytes.Buffer{} - // Attach buffer to command - oscmd.Stdout = cmdOutput - - err := oscmd.Start() - if err != nil { - log.Fatal(err) - } - log.Printf("Waiting for command to finish... %v", oscmd) - - err = oscmd.Wait() - - if err != nil { - log.Printf("Command finished with error: %v", err) - } else { - log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) - // analyze stdin - - } - - return port + "coolio", nil + return ports } -func getMetaDataForPortOld(port string) (string, error) { - // search the folder structure on linux for this port name - - // search /sys folder - oscmd := exec.Command("find", "/sys/devices", "-name", port, "-print") //, "2>", "/dev/null") - - // Stdout buffer - cmdOutput := &bytes.Buffer{} - // Attach buffer to command - oscmd.Stdout = cmdOutput - - err := oscmd.Start() - if err != nil { - log.Fatal(err) - } - log.Printf("Waiting for command to finish... %v", oscmd) - - err = oscmd.Wait() - - if err != nil { - log.Printf("Command finished with error: %v", err) - } else { - log.Printf("Finished without error. Good stuff. stdout:%v", string(cmdOutput.Bytes())) - // analyze stdin - - } - - return port + "coolio", nil +func hideFile(path string) { } diff --git a/seriallist_windows.go b/seriallist_windows.go index 7fa6a46d3..218680f56 100644 --- a/seriallist_windows.go +++ b/seriallist_windows.go @@ -1,28 +1,21 @@ package main import ( - //"fmt" - //"github.com/lxn/win" + log "github.com/Sirupsen/logrus" "github.com/mattn/go-ole" "github.com/mattn/go-ole/oleutil" - //"github.com/tarm/goserial" - //"github.com/johnlauer/goserial" - "github.com/facchinm/go-serial" - "log" "os" + "regexp" "strings" - //"encoding/binary" - "strconv" "sync" - //"syscall" - "regexp" + "syscall" ) var ( serialListWindowsWg sync.WaitGroup ) -func removeNonArduinoBoards(ports []OsSerialPort) []OsSerialPort { +func associateVidPidWithPort(ports []OsSerialPort) []OsSerialPort { ports, _ = getList() return ports } @@ -68,6 +61,13 @@ func getListSynchronously() { } +func hideFile(path string) { + cpath, cpathErr := syscall.UTF16PtrFromString(path) + if cpathErr != nil { + } + syscall.SetFileAttributes(cpath, syscall.FILE_ATTRIBUTE_HIDDEN) +} + func getListViaWmiPnpEntity() ([]OsSerialPort, os.SyscallError) { //log.Println("Doing getListViaWmiPnpEntity()") @@ -206,13 +206,8 @@ func getListViaWmiPnpEntity() ([]OsSerialPort, os.SyscallError) { } } - if list[i].IdVendor != "2341" { - list[i].FriendlyName = asString.ToString() - } else { - archBoardName, boardName, _ := getBoardName("0x" + list[i].IdProduct) - list[i].RelatedNames = append(list[i].RelatedNames, archBoardName) - list[i].FriendlyName = strings.Trim(boardName, "\n") - } + list[i].IdVendor = "0x" + list[i].IdVendor + list[i].IdProduct = "0x" + list[i].IdProduct manufStr, _ := oleutil.GetProperty(item, "Manufacturer") list[i].Manufacturer = manufStr.ToString() @@ -223,108 +218,8 @@ func getListViaWmiPnpEntity() ([]OsSerialPort, os.SyscallError) { } - for index, element := range list { - - log.Printf("index:%v, name:%v, friendly:%v ", index, element.Name, element.FriendlyName) - - for index2, element2 := range list { - if index == index2 { - continue - } - if element.SerialNumber == element2.SerialNumber { - log.Printf("Found related element1:%v, element2:%v", element, element2) - list[index].RelatedNames = append(list[index].RelatedNames, element2.Name) - } - } - - } - - return list, err -} - -func getListViaOpen() ([]OsSerialPort, os.SyscallError) { - - log.Println("Doing getListViaOpen(). Will try to open COM1 to COM99.") - var err os.SyscallError - list := make([]OsSerialPort, 100) - var igood int = 0 - for i := 0; i < 100; i++ { - prtname := "COM" + strconv.Itoa(i) - //conf := &serial.Config{Name: prtname, Baud: 1200} - mode := &serial.Mode{ - BaudRate: 1200, - Vmin: 0, - Vtimeout: 10, - } - sp, err := serial.OpenPort(prtname, mode) - //log.Println("Just tried to open port", prtname) - if err == nil { - //log.Println("Able to open port", prtname) - list[igood].Name = prtname - sp.Close() - list[igood].FriendlyName = prtname - //list[igood].FriendlyName = getFriendlyName(prtname) - igood++ - } - } - for index, element := range list[:igood] { - log.Println("index ", index, " element ", element.Name+ - " friendly ", element.FriendlyName) - } - return list[:igood], err -} - -/* -func getListViaRegistry() ([]OsSerialPort, os.SyscallError) { - - log.Println("Doing getListViaRegistry()") - var err os.SyscallError - var root win.HKEY - rootpath, _ := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM") - log.Println(win.RegOpenKeyEx(win.HKEY_LOCAL_MACHINE, rootpath, 0, win.KEY_READ, &root)) - - var name_length uint32 = 72 - var key_type uint32 - var lpDataLength uint32 = 72 - var zero_uint uint32 = 0 - name := make([]uint16, 72) - lpData := make([]byte, 72) - - var retcode int32 - retcode = 0 - for retcode == 0 { - retcode = win.RegEnumValue(root, zero_uint, &name[0], &name_length, nil, &key_type, &lpData[0], &lpDataLength) - log.Println("Retcode:", retcode) - log.Println("syscall name: "+syscall.UTF16ToString(name[:name_length-2])+"---- name_length:", name_length) - log.Println("syscall lpdata:"+string(lpData[:lpDataLength-2])+"--- lpDataLength:", lpDataLength) - //log.Println() - zero_uint++ - } - win.RegCloseKey(root) - win.RegOpenKeyEx(win.HKEY_LOCAL_MACHINE, rootpath, 0, win.KEY_READ, &root) - - list := make([]OsSerialPort, zero_uint) - var i uint32 = 0 - for i = 0; i < zero_uint; i++ { - win.RegEnumValue(root, i-1, &name[0], &name_length, nil, &key_type, &lpData[0], &lpDataLength) - //name := string(lpData[:lpDataLength]) - //name = name[:strings.Index(name, '\0')] - //nameb := []byte(strings.TrimSpace(string(lpData[:lpDataLength]))) - //list[i].Name = string(nameb) - //list[i].Name = string(name[:strings.Index(name, "\0")]) - //list[i].Name = fmt.Sprintf("%s", string(lpData[:lpDataLength-1])) - pname := make([]uint16, (lpDataLength-2)/2) - pname = convertByteArrayToUint16Array(lpData[:lpDataLength-2], lpDataLength-2) - list[i].Name = syscall.UTF16ToString(pname) - log.Println("The length of the name is:", len(list[i].Name)) - log.Println("list[i].Name=" + list[i].Name + "---") - //list[i].FriendlyName = getFriendlyName(list[i].Name) - list[i].FriendlyName = getFriendlyName("COM34") - } - win.RegCloseKey(root) return list, err } -*/ func convertByteArrayToUint16Array(b []byte, mylen uint32) []uint16 { @@ -337,56 +232,3 @@ func convertByteArrayToUint16Array(b []byte, mylen uint32) []uint16 { } return ret } - -func getFriendlyName(portname string) string { - - // this method panics a lot and i'm not sure why, just catch - // the panic and return empty list - defer func() { - if e := recover(); e != nil { - // e is the interface{} typed-value we passed to panic() - log.Println("Got panic: ", e) // Prints "Whoops: boom!" - } - }() - - var friendlyName string - - // init COM, oh yeah - ole.CoInitialize(0) - defer ole.CoUninitialize() - - unknown, _ := oleutil.CreateObject("WbemScripting.SWbemLocator") - defer unknown.Release() - - wmi, _ := unknown.QueryInterface(ole.IID_IDispatch) - defer wmi.Release() - - // service is a SWbemServices - serviceRaw, _ := oleutil.CallMethod(wmi, "ConnectServer") - service := serviceRaw.ToIDispatch() - defer service.Release() - - // result is a SWBemObjectSet - //pname := syscall.StringToUTF16("SELECT * FROM Win32_PnPEntity where Name like '%" + "COM35" + "%'") - pname := "SELECT * FROM Win32_PnPEntity where Name like '%" + portname + "%'" - resultRaw, _ := oleutil.CallMethod(service, "ExecQuery", pname) - result := resultRaw.ToIDispatch() - defer result.Release() - - countVar, _ := oleutil.GetProperty(result, "Count") - count := int(countVar.Val) - - for i := 0; i < count; i++ { - // item is a SWbemObject, but really a Win32_Process - itemRaw, _ := oleutil.CallMethod(result, "ItemIndex", i) - item := itemRaw.ToIDispatch() - defer item.Release() - - asString, _ := oleutil.GetProperty(item, "Name") - - println(asString.ToString()) - friendlyName = asString.ToString() - } - - return friendlyName -} diff --git a/serialport.go b/serialport.go index fc90297d2..be0449398 100755 --- a/serialport.go +++ b/serialport.go @@ -3,10 +3,9 @@ package main import ( "bytes" "encoding/json" - //"github.com/johnlauer/goserial" + log "github.com/Sirupsen/logrus" "github.com/facchinm/go-serial" "io" - "log" "strconv" "time" ) @@ -341,35 +340,10 @@ func spHandlerOpen(portname string, baud int, buftype string, isSecondary bool) // we can go up to 256,000 lines of gcode in the buffer p := &serport{sendBuffered: make(chan Cmd, 256000), done: make(chan bool), sendNoBuf: make(chan Cmd), portConf: conf, portIo: sp, BufferType: buftype, IsPrimary: isPrimary, IsSecondary: isSecondary} - // if user asked for a buffer watcher, i.e. tinyg/grbl then attach here - if buftype == "tinyg" { - - bw := &BufferflowTinyg{Name: "tinyg", parent_serport: p} - bw.Init() - bw.Port = portname - p.bufferwatcher = bw - } else if buftype == "dummypause" { - - // this is a dummy pause type bufferflow object - // to test artificially a delay on the serial port write - // it just pauses 3 seconds on each serial port write - bw := &BufferflowDummypause{} - bw.Init() - bw.Port = portname - p.bufferwatcher = bw - } else if buftype == "grbl" { - // grbl bufferflow - // store port as parent_serport for use in intializing a status query loop for '?' - bw := &BufferflowGrbl{Name: "grbl", parent_serport: p} - bw.Init() - bw.Port = portname - p.bufferwatcher = bw - } else { - bw := &BufferflowDefault{} - bw.Init() - bw.Port = portname - p.bufferwatcher = bw - } + bw := &BufferflowDefault{} + bw.Init() + bw.Port = portname + p.bufferwatcher = bw sh.register <- p defer func() { sh.unregister <- p }() @@ -383,35 +357,6 @@ func spHandlerOpen(portname string, baud int, buftype string, isSecondary bool) //<-p.done } -func spHandlerCloseExperimental(p *serport) { - h.broadcastSys <- []byte("Pre-closing serial port " + p.portConf.Name) - p.isClosing = true - //close the port - - p.bufferwatcher.Close() - p.portIo.Close() - h.broadcastSys <- []byte("Bufferwatcher closed") - p.portIo.Close() - //elicit response from hardware to close out p.reader() - //_, _ = p.portIo.Write([]byte("?")) - //p.portIo.Read(nil) - - //close(p.portIo) - h.broadcastSys <- []byte("portIo closed") - close(p.sendBuffered) - h.broadcastSys <- []byte("p.sendBuffered closed") - close(p.sendNoBuf) - h.broadcastSys <- []byte("p.sendNoBuf closed") - - //p.done <- true - - // unregister myself - // we already have a deferred unregister in place from when - // we opened. the only thing holding up that thread is the p.reader() - // so if we close the reader we should get an exit - h.broadcastSys <- []byte("Closing serial port " + p.portConf.Name) -} - func spHandlerClose(p *serport) { // p.isClosing = true p.done <- true diff --git a/trayicon.go b/trayicon.go index 0d2cd58cf..cb93c8734 100644 --- a/trayicon.go +++ b/trayicon.go @@ -31,7 +31,8 @@ package main import ( - "fmt" + log "github.com/Sirupsen/logrus" + "github.com/facchinm/go-serial" "github.com/facchinm/systray" "github.com/facchinm/systray/example/icon" "github.com/skratchdot/open-golang/open" @@ -40,23 +41,56 @@ import ( func setupSysTray() { runtime.LockOSThread() - systray.Run(setupSysTrayReal) + if *hibernate == true { + systray.Run(setupSysTrayHibernate) + } else { + systray.Run(setupSysTrayReal) + } +} + +func addRebootTrayElement() { + reboot_tray := systray.AddMenuItem("Reboot to update", "") + + go func() { + <-reboot_tray.ClickedCh + systray.Quit() + log.Println("Restarting now...") + restart("") + }() } func setupSysTrayReal() { systray.SetIcon(icon.Data) - mUrl := systray.AddMenuItem("Go to Create (staging)", "Arduino Create") - menuVer := systray.AddMenuItem("Agent version "+version, "") - mQuit := systray.AddMenuItem("Quit", "Quit the bridge") + mUrl := systray.AddMenuItem("Go to Arduino Create (staging)", "Arduino Create") + mDebug := systray.AddMenuItem("Open debug console", "Debug console") + menuVer := systray.AddMenuItem("Agent version "+version+"-"+git_revision, "") + mPause := systray.AddMenuItem("Quit Plugin", "") + //mQuit := systray.AddMenuItem("Quit Plugin", "") menuVer.Disable() go func() { - <-mQuit.ClickedCh + <-mPause.ClickedCh + ports, _ := serial.GetPortsList() + for _, element := range ports { + spClose(element) + } systray.Quit() - fmt.Println("Quit now...") - exit() + *hibernate = true + restart("") + }() + + // go func() { + // <-mQuit.ClickedCh + // systray.Quit() + // exit() + // }() + + go func() { + <-mDebug.ClickedCh + open.Run("http://localhost:8989") + logAction("log on") }() // We can manipulate the systray in other goroutines @@ -65,3 +99,22 @@ func setupSysTrayReal() { open.Run("http://create-staging.arduino.cc") }() } + +func setupSysTrayHibernate() { + + systray.SetIcon(icon.DataHibernate) + mOpen := systray.AddMenuItem("Open Plugin", "") + mQuit := systray.AddMenuItem("Kill Plugin", "") + + go func() { + <-mOpen.ClickedCh + *hibernate = false + restart("") + }() + + go func() { + <-mQuit.ClickedCh + systray.Quit() + exit() + }() +} diff --git a/trayicon_linux_arm.go b/trayicon_linux_arm.go index b5147db2e..ac0c9fc73 100644 --- a/trayicon_linux_arm.go +++ b/trayicon_linux_arm.go @@ -32,3 +32,7 @@ func setupSysTray() { //no systray support for arm yet select {} } + +func addRebootTrayElement() { + select {} +} diff --git a/update.go b/update.go index 7d2232874..cfd8e36bc 100644 --- a/update.go +++ b/update.go @@ -37,9 +37,13 @@ import ( "encoding/json" "errors" "fmt" + log "github.com/Sirupsen/logrus" + "github.com/inconshreveable/go-update" + "github.com/kardianos/osext" + "github.com/kr/binarydist" + patch "github.com/sanderhahn/gozip/patchzip" "io" "io/ioutil" - "log" "math/rand" "net/http" "os" @@ -48,13 +52,6 @@ import ( "runtime" "strings" "time" - - "github.com/inconshreveable/go-update" - "github.com/kardianos/osext" - "github.com/kr/binarydist" - "github.com/termie/go-shutil" - - patch "github.com/sanderhahn/gozip/patchzip" ) func IsZip(path string) bool { @@ -241,7 +238,7 @@ func (u *Updater) BackgroundRun() error { os.MkdirAll(u.getExecRelativeDir(u.Dir), 0777) if u.wantUpdate() { if err := up.CanUpdate(); err != nil { - // fail + log.Println(err) return err } //self, err := osext.Executable() @@ -278,6 +275,7 @@ func (u *Updater) update() error { err = u.fetchInfo() if err != nil { + log.Println(err) return err } if u.Info.Version == u.CurrentVersion { @@ -310,6 +308,7 @@ func (u *Updater) update() error { err, errRecover := up.FromStream(bytes.NewBuffer(bin)) if errRecover != nil { + log.Errorf("update and recovery errors: %q %q", err, errRecover) return fmt.Errorf("update and recovery errors: %q %q", err, errRecover) } if err != nil { @@ -317,8 +316,11 @@ func (u *Updater) update() error { } // remove config.ini so at restart the package will extract again - shutil.CopyFile(*configIni, *configIni+".bak", false) - os.Remove(*configIni) + //shutil.CopyFile(*configIni, *configIni+".bak", false) + //os.Remove(*configIni) + + restart(path) + //addRebootTrayElement() // update done, we should decide if we need to restart ASAP (maybe a field in update json?) // BIG issue: the file has been renamed in the meantime @@ -405,6 +407,7 @@ func fetch(url string) (io.ReadCloser, error) { return nil, err } if resp.StatusCode != 200 { + log.Errorf("bad http status from %s: %v", url, resp.Status) return nil, fmt.Errorf("bad http status from %s: %v", url, resp.Status) } return resp.Body, nil diff --git a/utilities.go b/utilities.go index d71620674..e6ef73a85 100644 --- a/utilities.go +++ b/utilities.go @@ -5,80 +5,11 @@ package main import ( "bytes" "crypto/md5" - "encoding/json" - "fmt" - "github.com/kardianos/osext" "io" - "io/ioutil" "os" "os/exec" - "path/filepath" - "strings" ) -type Combo struct { - vendors map[string]Vendor -} - -type Vendor struct { - architectures map[string]Architecture -} - -type Architecture struct { - boards map[string]Board - platform map[string]Platform -} - -type Platform struct { -} - -type Board struct { - name string - vid map[int]int - pid map[int]int - upload Upload - build Build - bootloader Bootloader -} - -type Upload struct { - protocol string - disable_flushing bool - maximum_data_size int - tool string - use_1200bps_touch bool - speed int - maximum_size int - wait_for_upload_port int -} - -type Build struct { - core string - f_cpu int - board string - vid int - pid int - usb_product string - mcu string - extra_flags string - variant string -} - -type Bootloader struct { - extended_fuses int - high_fuses int - file string - low_fuses int - lock_bits int - tool string - unlock_bits int -} - -type ConfigProperty struct { - value string - path string -} - func computeMd5(filePath string) ([]byte, error) { var result []byte file, err := os.Open(filePath) @@ -135,134 +66,6 @@ func pipe_commands(commands ...*exec.Cmd) ([]byte, error) { return outputBuffer.Bytes(), nil } -func getBoardName(pid string) (string, string, error) { - //execPath, _ := osext.Executable() - - //avr := (m["arduino"].(map[string]interface{})["avr"].(map[string]interface{})["boards"]) - //var uno Board - //uno = avr["uno"] - - findAllPIDs(globalConfigMap) - - list, _ := searchFor(globalConfigMap, []string{"pid"}, pid) - - var archBoardNameSlice []string - archBoardName := "" - - if len(list) > 0 { - archBoardNameSlice = strings.Split(list[0].path, ":")[:5] - archBoardName = archBoardNameSlice[1] + ":" + archBoardNameSlice[2] + ":" + archBoardNameSlice[4] - } else { - return "", "", nil - } - - boardPath := append(archBoardNameSlice, "name") - - boardName := getElementFromMapWithList(globalConfigMap, boardPath).(string) - - return archBoardName, boardName, nil -} - -func matchStringWithSlice(str string, match []string) bool { - for _, elem := range match { - if !strings.Contains(str, elem) { - return false - } - } - return true -} - -func recursivelyIterateConfig(m map[string]interface{}, fullpath string, match []string, mapOut *[]ConfigProperty) { - - for k, v := range m { - switch vv := v.(type) { - case string: - if matchStringWithSlice(fullpath+":"+k, match) { - //fmt.Println(k, "is string", vv, "path", fullpath) - if mapOut != nil { - *mapOut = append(*mapOut, ConfigProperty{path: fullpath, value: vv}) - //fmt.Println(getElementFromMapWithList(globalConfigMap, strings.Split(fullpath, ":"))) - } - } - case map[string]interface{}: - //fmt.Println(k, "is a map:", fullpath) - recursivelyIterateConfig(m[k].(map[string]interface{}), fullpath+":"+k, match, mapOut) - default: - //fmt.Println(k, "is of a type I don't know how to handle ", vv) - } - } -} - -func RemoveDuplicates(xs *[]string) { - found := make(map[string]bool) - j := 0 - for i, x := range *xs { - if !found[x] { - found[x] = true - (*xs)[j] = (*xs)[i] - j++ - } - } - *xs = (*xs)[:j] -} - -func findAllVIDs(m map[string]interface{}) []ConfigProperty { - var vidList []ConfigProperty - recursivelyIterateConfig(m, "", []string{"vid"}, &vidList) - //fmt.Println(vidList) - return vidList -} - -func findAllPIDs(m map[string]interface{}) []ConfigProperty { - var pidList []ConfigProperty - recursivelyIterateConfig(m, "", []string{"pid"}, &pidList) - //fmt.Println(pidList) - return pidList -} - -func searchFor(m map[string]interface{}, args []string, element string) ([]ConfigProperty, bool) { - var uList []ConfigProperty - var results []ConfigProperty - recursivelyIterateConfig(m, "", args, &uList) - //fmt.Println(uList) - for _, elm := range uList { - if elm.value == element { - results = append(results, elm) - } - } - return results, len(results) != 0 -} - -func getElementFromMapWithList(m map[string]interface{}, listStr []string) interface{} { - var k map[string]interface{} - k = m - for _, element := range listStr { - switch k[element].(type) { - case string: - return k[element] - default: - if element != "" { - k = k[element].(map[string]interface{}) - } - } - } - return k -} - -func createGlobalConfigMap(m *map[string]interface{}) { - execPath, _ := osext.Executable() - - file, e := ioutil.ReadFile(filepath.Dir(execPath) + "/arduino/boards.json") - - if e != nil { - fmt.Printf("File error: %v\n", e) - os.Exit(1) - } - - //var config Combo - json.Unmarshal(file, m) -} - // Filter returns a new slice containing all OsSerialPort in the slice that satisfy the predicate f. func Filter(vs []OsSerialPort, f func(OsSerialPort) bool) []OsSerialPort { var vsf []OsSerialPort